Merge remote-tracking branch 'origin/ni/1-libnode-loader' into ni/3-hosted-loop

This commit is contained in:
Malte Ubl
2026-10-22 14:17:32 -07:00
809 changed files with 47176 additions and 3492 deletions
+14
View File
@@ -368,6 +368,20 @@ jobs:
target: wasm32-wasi
helper: "false"
- run: pnpm build
# The sanitized seed compiles the compiler's 700 MB module with one clang
# process under AddressSanitizer. It peaks near 14 GB while the Node
# process that emitted the module still holds about 6 GB. Together they
# exceed the runner's 16 GB of memory, so back them with swap.
- name: Enlarge swap for the sanitized seed
if: matrix.flavor == 'san'
run: |
sudo swapoff -a
sudo rm -f /mnt/swapfile
sudo fallocate -l 24G /mnt/swapfile
sudo chmod 600 /mnt/swapfile
sudo mkswap /mnt/swapfile
sudo swapon /mnt/swapfile
free -h
- name: Prepare bootstrap seed
run: node scripts/bootstrap-seed.mjs
env:
+1
View File
@@ -241,6 +241,7 @@
"packages/compiler/src/frontend/comptime-node.ts",
"packages/compiler/src/frontend/ts7/world-check.ts",
"packages/compiler/src/frontend/ts7/source-parser.test.ts",
"packages/compiler/src/frontend/jsx-source.test.ts",
"packages/compiler/src/frontend/npm-static-rewrite.ts",
"packages/compiler/src/frontend/npm-static-bundled-cjs.ts",
"packages/compiler/src/library/semantic-source.ts",
+155 -2
View File
@@ -6,6 +6,161 @@ All notable changes to scriptc will be documented in this file.
<!-- release:start -->
## 0.4.2
### Highlights
- **Faster builds of large programs:**
- scriptc checks with tsc-ts 0.0.7, which answers contextual types and resolved signatures in batches, so lowering asks for them in one request per chunk instead of one per node. A native build of tsc-ts is 7% faster (110.5 → 103.5 s on 8 vCPUs, lowering 12% faster), with an identical executable.
- The native compiler is built with profile-guided optimization, so tsc-ts builds 6.9% faster cold and 10.7% faster after an edit.
- The native compiler's optimization profile is refreshed, so a native build of tsc-ts is a further 10% faster (124.7 → 111.7 s on 16 vCPUs), with lowering 17% faster.
- The class dispatch passes index their receivers by name instead of testing each one, and they reuse their per-function scans while a function's body is unchanged.
- Checker replies resolve objects the compiler already holds from their ids without decoding them.
- **Leaner builds:**
- The frontend of an optimized executable's first build runs in a child process, so the native code generator starts after its memory is released. The cold build of tsc-ts peaks at 2.21 GB instead of 3.30 GB.
- The compiler collects its heap as lowering and LLVM emission grow it.
- **WebCrypto:**
- The `crypto` global, `node:crypto`'s `webcrypto` and `crypto.subtle` are one object.
- `generateKey`, `importKey`, `exportKey`, `encrypt`, `decrypt`, `deriveBits`, `deriveKey`, `wrapKey` and `unwrapKey` cover HMAC, AES-GCM, AES-CBC, AES-CTR, AES-KW, PBKDF2, HKDF and JSON Web Keys.
- `crypto.subtle` signs and verifies with ECDSA, RSASSA-PKCS1-v1_5, RSA-PSS and Ed25519.
- All of it follows Node 24's argument conversion, algorithm normalization and `DOMException`s. jose's symmetric JWE paths run as in Node.
- **JSX:** `.tsx`, `.jsx` and JavaScript files with JSX compile under the project's `jsx` options.
- `react`, `react-jsx` and `react-jsxdev` lower to the factory calls TypeScript emits, with the same keys, fallbacks and source positions.
- `preserve` and `react-native` are refused with a named diagnostic.
- **Faster worker messages:**
- A message is encoded into one block sized to its contents.
- A typed record is posted from its fields.
- A `'message'` listener with a typed parameter builds it from the message, without a checked-dynamic copy.
- Messages between `MessageChannel` ports take 90 ms where they took 250 ms in 0.4.1 (Node: 337 ms), with a peak RSS of 45 MB instead of 115 MB.
- **Loops over typed arrays vectorize:** a loop to a typed array's length keeps one bounds test for every typed array it indexes.
- Index temporaries such as `const j = i * 4` are computed as integers.
- Uint8ClampedArray stores convert inline.
- Row-major index terms such as `r * cols + j` are tested once before the loop.
- Typed-array callback methods call a literal callback directly on an integer counter.
- An 896 × 896 matrix-vector product with four accumulators takes 55 ms instead of 396 ms (Node: 275 ms). `Uint8Array.map` is 4.5 times as fast and `Int32Array.some` 7.8 times as fast as in 0.4.1.
### Features
- **Asymmetric keys and signatures in `node:crypto`:**
- `generateKeyPair`, `generateKeyPairSync`, `createPrivateKey` and `createPublicKey` handle RSA, EC and Ed25519 keys in PEM, DER and JWK.
- `sign`, `verify`, `createSign` and `createVerify` sign and verify with those keys, with Node's output and errors.
- Ed25519 verification matches Node on 2,083 test vectors, including small-order and mixed-order keys.
- **`crypto.createDiffieHellman`, `crypto.getDiffieHellman` and `crypto.createDiffieHellmanGroup`:**
- The modp1, modp2 and modp5 to modp18 groups, explicit primes and generators, and generated safe primes are supported, with Node 24's key widths, `verifyError` bits, key range checks and errors.
- Secrets of 0, 1 and p − 1 are rejected, and so are peer keys outside the RFC 5114 subgroups.
- Primes longer than 8192 bits are refused.
- **TLS:**
- `tls.connect` and `tls.createServer` negotiate `ALPNProtocols`, and both sockets report `alpnProtocol`. `minVersion` and `maxVersion` bound the handshake, and `getProtocol()` reports the version it picked.
- `tls.createServer` takes an `SNICallback` that answers a `SecureContext` synchronously or later.
- `new tls.TLSSocket(socket, { isServer: true, ... })` and `tls.connect({ socket })` run the handshake on an open socket, such as an HTTP `CONNECT` tunnel.
- A client whose peer closes before the handshake completes fails with Node's `ECONNRESET`.
- **Streams:**
- Readable, Writable, Duplex, Transform and PassThrough streams take `objectMode`, `readableObjectMode` and `writableObjectMode`, and an object-mode side carries any value through `push`, `write`, `pipe`, `pipeline`, `for await` and `'data'` listeners.
- `_write` and `_transform` receive the encoding argument as Node passes it.
- `Cipheriv` and `Decipheriv` are Transforms: they can be piped, used as `pipeline()` stages, read and iterated.
- `pipeline()` accepts function stages anywhere among its streams, and an array, a string or a synchronous generator as its source.
- **Packages compiled from their JavaScript:**
- js-yaml and smol-toml compile statically and match Node: ES5 error constructors (`Object.create(Error.prototype)`, `Error.call(this)`, `Error.captureStackTrace`) and classes extending `Date` behave as in Node.
- TypeScript's UMD module wrapper compiles as the module body, and tsc's `__importDefault` and `__importStar` helpers around builtin modules compile as plain `require()` calls.
- Interfaces, type aliases and enums that only a package's `.d.ts` declares keep their types.
- **JavaScript sources:**
- Error constructors can be read as values, so `err.constructor === TypeError` works.
- `workerData` can be destructured.
- Unary `+` converts unions of strings, numbers, booleans, `null` and `undefined` as `Number()` does, so `+process.argv[2]` compiles.
- These compile and behave as in Node:
- chained readline listener registration;
- `Number.parseInt` without a radix;
- `match` and `search` with a possibly undefined regular expression;
- `Buffer.from` over an untyped `ArrayBuffer`, with its byte offset;
- Atomics on untyped arrays.
- Buffer methods run on Buffers held as untyped values.
- **`fs`:**
- The synchronous functions (`readFileSync`, `writeFileSync`, `statSync`, `readdirSync`, `mkdirSync`, `rmSync`, `openSync` and the others) accept `file:` URLs wherever they accept a path, with Node's conversion errors.
- `fs.promises.rm` takes `recursive`, `force`, `maxRetries` and `retryDelay`, and removing a directory without `recursive` throws Node's `ERR_FS_EISDIR`.
- **`TypedArray.of`** creates a typed array of its arguments, and a dynamic iterable spread into `Array.of`, `push` or `TypedArray.of` is drained through the iterator protocol.
- **Global `isFinite` converts its argument with ToNumber,** as `isNaN` does. The Math functions and `String.fromCharCode` evaluate every argument before converting any.
- **`Object.freeze`, `Object.seal` and `Object.preventExtensions` of a function** act on its own properties, and `Object.isFrozen`, `Object.isSealed` and `Object.isExtensible` answer for functions. @noble/hashes' frozen hashers work.
- **EventEmitters used through both typed and untyped references** share one listener list. An untyped `emit` reaches typed listeners, an untyped `emit('error')` follows Node's contract, and `listenerCount`, `eventNames`, `listeners`, `rawListeners`, `removeAllListeners`, `setMaxListeners` and `getMaxListeners` work on untyped values.
- **Template literals, `join`, `Array.prototype.toString` and the default `sort()`** call an object's own or inherited `toString`, as `String()` does, instead of printing `[object Object]`.
- **Classes and generics:**
- A generic factory declared to return a constructor type can return one of its local classes, including an anonymous class expression, and `class X extends factory(...)` extends it.
- Static methods of a function-local class compile.
- Constructor types declared in the program (`new () => unknown`, types that also name static members, construct overloads) hold class values.
- Members typed by a conditional type over a literal class type argument (`Field<string, "required">`) resolve.
- Object literals under an erased generic member may hold callbacks with optional or defaulted parameters.
- **Assignment and update targets wrapped in parentheses or non-null assertions** (`(x) = 1`, `h.sums[k]! += 10`) compile.
- **Untyped values keep what JavaScript code adds:**
- Properties that untyped code adds to a typed object literal stay on it.
- Named properties written to an array through an `unknown` view stay on the array, and `for...in` visits them after the indices.
- An `unknown` value asserted to a structural type with methods keeps its class's methods.
- The type inferred from an empty object literal holds any non-nullish value, as TypeScript's `{}` does.
- **A WebAssembly module the build finds but does not translate** rejects with a `CompileError` that names the reason, for example a proposal outside the translated feature set.
- **SC6101 names the functions that likely run often and use untyped values:**
- It shows up to five, most likely hot first, with why their values are untyped: an `any`, an untyped `JSON.parse` result or an untyped JavaScript file.
- `scriptc coverage --fail-on=dynamic` fails on it, and `--no-dynamic-tier-warning` leaves it out.
### Performance
- **Regular expressions skip start positions that their leading character sets rule out,** testing 16 positions at once. Searching 50 MB of text for nine case-insensitive alternations takes 0.83 s instead of 3.88 s (Node: 4.38 s).
- **Loop counters stay integers in more loops:**
- Counters that step beside a counted loop's counter (`for (let i = 0, p = 0; i < n; i++, p += 4)`) stay integers: a pixel-stride loop takes 114 ms instead of 340 ms (Node: 190 ms).
- So do two counters that close in on each other (`lo++, hi--`), 17% faster on a permutation-reversal loop.
- So do counters of loops bounded by a string's length or by an invariant limit other than an array's length, and while-loop counters that nested scans advance. A non-ASCII lexer is 9.5% faster.
- **Loops that call a known callback** test its closure once before the loop and call the function directly, so captured `let` bindings stay in registers. `forEach` sums with a captured accumulator are 25 to 55% faster.
- **A speculative integer loop whose check fails** finishes the iteration outside the ordinary loop, which stays optimizable, and a loop site that keeps failing stops trying. With the hole-free-prefix test that element reads now take first, a pairwise loop over an array of class instances with number fields is about 1.25 times as fast as in 0.4.1.
- **Typed arrays over a SharedArrayBuffer** read and write elements inline as relaxed atomic accesses. A numeric kernel over shared `Float64Array`s takes 0.78 s instead of 1.56 s (Node: 0.84 s).
- **`Math.pow(2, n)` for integer exponents** builds the result from its bits. A counter that masks with `Math.pow(2, length)` takes 11.0 s instead of 20.4 s.
- **Async calls that never suspend run in place,** and so does `await Promise.all` of such calls, with Node's microtask turns and rejection handling. An async pipeline takes 5.5 ms instead of 9.7 ms.
- **Strings:**
- `Number()` of a short integer string returns the integer directly.
- `split` builds its result in one inline block, and `indexOf` and `slice` on ASCII strings skip the UTF-16 index map. A log summary is 20% faster.
- `charAt` and `s[i]` on ASCII strings read the character from a table.
- Chains of one-character `replace` calls (the HTML-escape idiom) replace a block of 16 bytes at a time, 12.8% faster on a template renderer.
- **`JSON.parse` and `JSON.stringify`** scan whole words and append parsed typed arrays in bulk, 22% faster to parse and 10% faster to stringify records.
- **Smaller arrays, maps and program-lifetime instances:**
- The array header is 32 bytes instead of 48.
- Arrays of up to 16 known elements keep their slots in their own block.
- Arrays of a known final length get exactly that capacity.
- Small map entry arrays grow in finer steps.
- On Linux, `ProgramLifetime` instances take a 32-bit header and are referenced in 4 bytes.
- They lower the peak memory of a large TypeScript type checker: the 4-byte references alone take a large project's check from 4.13 GB to 3.75 GB, and the 32-bit header saves a further 5%.
- **Worker messages queued together are delivered in one batch** before immediates run. 20,000 messages interleaved with a worker's output drain in 0.5 s instead of 5.6 s (Node: 0.57 s).
- **The cycle collector** never buffers instances of classes whose fields hold nothing cycle-capable, and its per-object paths are shorter. A workload of cyclic graphs is about 5% faster.
- **Array helpers specialized for a callback that cannot throw** skip the pending-exception test, 4.5% faster on a functional pipeline.
- **Compile time:**
- A file's function declarations are walked once for their references, so lowering a 10 MB single-module bundle takes 69 s instead of 219 s.
- LLVM emission computes its module facts once, 6.9% faster on tsc-ts.
- AST walks visit subtrees by their node ranges and read node words through a `Uint32Array`.
- Program builds no longer import runtime bitcode, so builds through Node and through the native compiler produce identical executables.
### Fixes
- **Member reads and method calls on an array hole** held in a binding narrowed to one arm of a wide union throw Node's `Cannot read properties of undefined` TypeError.
- **`AsyncLocalStorage`:**
- A store entered before an async function's first await, or in a body that never suspends, is the caller's store after the call.
- Each synchronous generator step runs in its resumer's context, as in Node.
- **Generic arrow and function-expression bindings used before their declarations** behave as in Node, including the `ReferenceError` of a `let` or `const`.
- **Assignment expressions into a module `let` declared without an initializer** (`new Promise((r) => (resolve = r))`) store the value instead of throwing a `ReferenceError`.
- **The `extends` expression of a class defined inside a function** is evaluated each time the definition runs.
- **A `super()` call that leads an `if` condition's comma chain** compiles.
- **A catch binding tests `instanceof`** correctly for values thrown as `unknown`, rejected promises and runtime errors.
- **zlib callbacks receive `undefined` as their result when the operation fails,** as in Node, and a callback may assign a fallback to it.
- **`Readable.push` and `Writable.write` with `string | null` or `Buffer | null` chunks** no longer crash.
- **Object literals keep their key order** through compiler rewrites and on shapes a typed `JSON.parse` also builds, so `JSON.stringify`, `Object.keys` and `console.log` match Node.
- **`console.log` prints class instances with their class name** in programs that test `instanceof` on unknown values.
- **`String`, `Number` and `Boolean` passed to array methods** convert as their direct calls do, and nested array literals in a recursive JSON-style union build.
- **A dynamic method call that the program also reads through a prototype** finds the method when the instance's property bag does not hold it.
- **Storing an element on `Array.prototype`** fails with SC2020, because holes in native arrays do not read inherited elements.
- **A recursive type alias in a generic signature** no longer crashes the compiler.
- **A `Buffer` view of an untyped typed array** compiles.
- **JSX programs checked with tsgo** compile as they do with tsc-ts.
- **Two programs whose entries share a name and build into one directory** no longer share a cached translation unit, so a later cache hit always relinks the right program.
- **A warm build whose cached LLVM exceeds the engine's string limit** reads it in parts instead of failing with `Invalid string length`.
- **The same program compiles to the same executable wherever it sits on disk** and whatever order its functions lower in, and edits outside a declaration do not rename the helpers and closures inside it.
- **Libraries that use untyped byte arrays** link.
<!-- release:end -->
## 0.4.1
### Highlights
@@ -77,8 +232,6 @@ All notable changes to scriptc will be documented in this file.
- **TypeScript enum exports in compiled packages stay `undefined` until they are assigned,** as in Node.
- **Element writes accept a possibly undefined index** (typed-array writes, compound and update writes, element assignments used as values and destructuring targets) and convert it with ToNumber, as element reads already did.
<!-- release:end -->
## 0.4.0
### Breaking changes
+1
View File
@@ -26,6 +26,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
| `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds |
| `template-render` | cpu | template literals, escaping, `map`/`join` |
| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread |
| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray, a row-major `Float32Array` matrix-vector product, and `reduce`/`some`/`map` callbacks |
| `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage |
| `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts |
| `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains |
@@ -0,0 +1,6 @@
{
"entry": "workloads/typed-array-kernels.ts",
"args": [
"1"
]
}
@@ -0,0 +1,144 @@
// Element-wise loops over several typed arrays: each loop runs to one
// array's length and reads or writes the others at the same index. Also a
// row-major matrix-vector product with four accumulators per row.
function axpy(a: number, x: Float64Array, y: Float64Array, out: Float64Array): void {
for (let i = 0; i < out.length; i++) out[i] = a * x[i]! + y[i]!;
}
function blend(t: number, a: Float32Array, b: Float32Array, out: Float32Array): void {
for (let i = 0; i < out.length; i++) out[i] = a[i]! * (1 - t) + b[i]! * t;
}
function mask(data: Uint8Array, key: Uint8Array, out: Uint8Array): void {
for (let i = 0; i < out.length; i++) out[i] = data[i]! ^ key[i]!;
}
function addWrap(a: Int32Array, b: Int32Array, out: Int32Array): void {
for (let i = 0; i < out.length; i++) out[i] = (a[i]! + b[i]!) | 0;
}
function brighten(src: Uint8ClampedArray, gain: number, out: Uint8ClampedArray): void {
for (let i = 0; i < out.length; i++) out[i] = src[i]! * gain + 8;
}
function matvec(w: Float32Array, x: Float32Array, out: Float32Array, cols: number): void {
for (let r = 0; r < out.length; r++) {
const base = r * cols;
let s0 = 0;
let s1 = 0;
let s2 = 0;
let s3 = 0;
for (let j = 0; j < cols; j += 4) {
s0 += w[base + j]! * x[j]!;
s1 += w[base + j + 1]! * x[j + 1]!;
s2 += w[base + j + 2]! * x[j + 2]!;
s3 += w[base + j + 3]! * x[j + 3]!;
}
out[r] = s0 + s1 + s2 + s3;
}
}
// Callback methods over a whole typed array.
function callbacks(samples: Float32Array, limit: number, data: Uint8Array): number {
const energy = samples.reduce((sum, v) => sum + v * v, 0);
const loud = samples.some((v) => v > limit) ? 1 : 0;
const halved = data.map((v) => v >> 1);
return energy / samples.length + loud + halved[halved.length >> 1]!;
}
function gray(rgba: Uint8Array, out: Uint8Array): void {
for (let i = 0; i < out.length; i++) {
const j = i * 4;
out[i] = (rgba[j]! * 77 + rgba[j + 1]! * 150 + rgba[j + 2]! * 29) >> 8;
}
}
const scale = Number(process.argv[2] ?? "1");
const n = 1 << 16;
const x = new Float64Array(n);
const y = new Float64Array(n);
const d = new Float64Array(n);
const fa = new Float32Array(n);
const fb = new Float32Array(n);
const fo = new Float32Array(n);
const data = new Uint8Array(n);
const key = new Uint8Array(n);
const masked = new Uint8Array(n);
const ia = new Int32Array(n);
const ib = new Int32Array(n);
const io = new Int32Array(n);
const pixels = new Uint8ClampedArray(n);
const bright = new Uint8ClampedArray(n);
const rgba = new Uint8Array(n * 4);
const side = 256;
const weights = new Float32Array(side * side);
const vector = new Float32Array(side);
const product = new Float32Array(side);
const luma = new Uint8Array(n);
let state = 2463534242;
const next = (): number => {
state ^= state << 13;
state ^= state >>> 17;
state ^= state << 5;
return state >>> 0;
};
for (let i = 0; i < n; i++) {
x[i] = (next() % 20000) / 100 - 100;
y[i] = (next() % 20000) / 100;
fa[i] = (next() % 1000) / 7;
fb[i] = (next() % 1000) / 9;
data[i] = next() & 255;
key[i] = next() & 255;
ia[i] = next() | 0;
ib[i] = next() | 0;
pixels[i] = next() & 255;
}
for (let i = 0; i < rgba.length; i++) rgba[i] = next() & 255;
for (let i = 0; i < weights.length; i++) weights[i] = (next() % 2001) / 1000 - 1;
for (let i = 0; i < side; i++) vector[i] = (next() % 1001) / 1000;
const rounds = Math.round(1200 * scale);
let f64Sum = 0;
let f32Sum = 0;
let byteSum = 0;
let intSum = 0;
let brightSum = 0;
let lumaSum = 0;
let productSum = 0;
let callbackSum = 0;
for (let r = 0; r < rounds; r++) {
// Change one input element per round so no round repeats the last.
const at = (r * 7919) & (n - 1);
x[at] = r;
fa[at] = r;
data[at] = r & 255;
ia[at] = r * 65537;
pixels[at] = r & 255;
rgba[at * 4] = r & 255;
vector[r & (side - 1)] = (r % 1001) / 1000;
axpy(1.5, x, y, d);
blend((r & 15) / 16, fa, fb, fo);
mask(data, key, masked);
addWrap(ia, ib, io);
brighten(pixels, 0.75 + (r & 7) / 8, bright);
gray(rgba, luma);
matvec(weights, vector, product, side);
if ((r & 3) === 0) callbackSum += callbacks(fo, 140 + (r & 12), masked);
const probe = (r * 104729) & (n - 1);
f64Sum += d[probe]!;
f32Sum += fo[probe]!;
byteSum += masked[probe]!;
intSum = (intSum + io[probe]!) | 0;
brightSum += bright[probe]!;
lumaSum += luma[probe]!;
productSum += product[probe & (side - 1)]!;
}
console.log(`float64 ${f64Sum.toFixed(3)}`);
console.log(`float32 ${f32Sum.toFixed(3)}`);
console.log(`uint8 ${byteSum}`);
console.log(`int32 ${intSum}`);
console.log(`clamped ${brightSum}`);
console.log(`gray ${lumaSum}`);
console.log(`matvec ${productSum.toFixed(3)}`);
console.log(`callbacks ${callbackSum.toFixed(3)}`);
+2
View File
@@ -349,6 +349,8 @@ class Identifier extends SyntaxNode {
}
```
On Linux, except in builds with sanitizers, the instances also take less memory. An executable keeps them in one address range below 32 GiB that it reserves at startup (only the pages in use take memory), and a field typed `C`, `C | undefined` or `C | null`, where `C` is a program-lifetime class, stores the instance's address in 4 bytes instead of 8. In the example, `parent` takes 4 bytes.
scriptc checks that the promise cannot change what the program computes (SC7008):
- the class is the root of its hierarchy, so every value of any class type in the hierarchy is program-lifetime; built-in bases such as `Error`, `EventEmitter` and streams are refused because they hold runtime state that is released with the object;
+1 -1
View File
@@ -79,7 +79,7 @@ The program compiled. Adding types to these functions lets scriptc compile them
Without a profile, a dynamic-tier function counts as likely to run often when a dynamic operation runs on every iteration of one of its loops (a `for` loop's initializer and a `for...of` loop's iterable run once), when a loop calls it, or when it calls itself. A function is named only when its source says why its values are untyped: a value typed `any` (for a `for...of` variable, the value it iterates), an untyped `JSON.parse` result, or an untyped JavaScript file. Dynamic code that runs once, such as setup code that reads an `any` configuration, is not reported, and neither are functions that are dynamic for an operation with no static lowering, which more types would not change.
The warning names at most five functions, most likely to run often first, and gives the total. Dynamic-tier code in npm packages is summarized on one line with each package's function count. A build that comes from the cache repeats the warning. `--no-dynamic-tier-warning` leaves it out, and `--fail-on=dynamic` fails a coverage run on it.
The warning names at most five functions, most likely to run often first, and gives the total. A function expression is named by its own name, else by the variable, property or class field it is assigned to, else as `anonymous function`. Dynamic-tier code in npm packages is summarized on one line with each package's function count. A build that comes from the cache repeats the warning. `--no-dynamic-tier-warning` leaves it out, and `--fail-on=dynamic` fails a coverage run on it.
## Diagnostic codes
+2
View File
@@ -34,6 +34,8 @@ title: "Limitations"
<include>../limitations/language-features/commonjs-require.mdx</include>
<include>../limitations/language-features/jsx.mdx</include>
### Types and class definitions
<include>../limitations/types-and-class-definitions/bigint.mdx</include>
@@ -4,16 +4,42 @@
`copy()` of a Hash whose stream flushed, and stream use of a value after its stream closed, throw a coded SC2020 error.
Through `unknown` and `any` slots and untyped JavaScript, Hash and Hmac values keep their identity and support `update()`, `digest()`, and Hash's `copy()`, called or read as values; their stream and event members there throw a coded SC2020 error.
`timingSafeEqual` compares any view or ArrayBuffer.
Secret KeyObjects (`createSecretKey`, `generateKey` and `generateKeySync` for `hmac` and `aes`) support `type`, `symmetricKeySize`, `export()`, `equals()`, and `instanceof KeyObject`, and createHmac, hkdf, createCipheriv, and createDecipheriv accept them; asymmetric keys, `KeyObject.from()`, and `toCryptoKey()` are not available.
Secret KeyObjects (`createSecretKey`, `generateKey` and `generateKeySync` for `hmac` and `aes`) support `type`, `symmetricKeySize`, `export()`, `equals()`, and `instanceof KeyObject`, and createHmac, hkdf, createCipheriv, and createDecipheriv accept them; `KeyObject.from()` and `toCryptoKey()` are not available.
`randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`.
Scrypt supports its default parameters, numeric options, aliases, and memory limit.
Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers.
`createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, and GCM modes and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers, cipher streams, and wasm32-wasi builds are refused.
`createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, and GCM modes and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers and wasm32-wasi builds are refused.
Cipheriv and Decipheriv values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, iterated with `for await`, and listened to, and their stream properties read as in Node; written chunks go through `update()`, and the end pushes `final()` or reports its error through `'error'`.
Through `unknown` and `any` slots and untyped JavaScript, in a program that uses streams, their stream properties and their `on`, `once`, `off`, `pipe`, `unpipe`, `read`, `pause`, `resume`, `setEncoding`, `write`, `end`, `destroy`, `cork`, and `uncork` members work; `write` and `end` take no encoding or callback there, and the other stream members throw a coded SC2020 error.
Hash, Hmac, Cipheriv, and Decipheriv streams count a written string's bytes in `writableLength`, where Node counts the string's length.
Decrypting chacha20-poly1305 without an authentication tag fails in `final()`; Node 24.22 returns unverified plaintext.
After `update()` fails, for example past the AES-GCM limit of 2^36 - 32 bytes per message, every later call on that cipher throws; Node still lets `final()` run.
An output encoding that is a function or an instance of a class defining `toString` throws a coded SC2020 error.
`createECDH` and `ECDH.convertKey` support prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1, and brainpoolP512r1, on native targets, through `generateKeys()`, `computeSecret()`, `getPublicKey()`, `getPrivateKey()`, `setPrivateKey()`, and `setPublicKey()`, in compressed, uncompressed, and hybrid forms; the other curves `crypto.getCurves()` lists and wasm32-wasi builds are refused.
A call on an ECDH value whose arguments no @types/node overload accepts (`getPublicKey("HEX")`) is refused at compile time, as is `computeSecret(view, encoding)`: Node reads that encoding as the input encoding and answers a Buffer where @types/node declares a string.
ECDH property reads and writes throw a coded SC2020 error.
Other public-key operations remain unsupported.
`createDiffieHellman`, `getDiffieHellman`, and `createDiffieHellmanGroup` support the modp1, modp2, modp5, and modp14 through modp18 groups, explicit primes and generators, and generated safe primes, on native targets, through `generateKeys()`, `computeSecret()`, `getPrime()`, `getGenerator()`, `getPublicKey()`, `getPrivateKey()`, `setPublicKey()`, `setPrivateKey()`, and `verifyError`, with Node's private key lengths, DH_check results, and errors; wasm32-wasi builds are refused.
Primes longer than 8192 bits throw a coded SC2020 error; Node accepts primes up to 10000 bits for key generation and secrets.
`setPublicKey()` and `setPrivateKey()` keys longer than 80000 bytes, not counting leading zero bytes, throw a coded SC2020 error, as does a generator that long with a prime below 512 bits; Node accepts them.
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.
RSA, EC, and Ed25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets.
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.
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.
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.
Its `subtle` object, also node:crypto's `subtle` export, supports `generateKey()` and `exportKey()` for HMAC and AES keys, `importKey()` for HMAC, AES, PBKDF2, and HKDF keys, `encrypt()` and `decrypt()` with AES-GCM, AES-CBC, and AES-CTR and `wrapKey()` and `unwrapKey()` of secret keys with AES-KW or those ciphers on native targets, `deriveBits()` and `deriveKey()` with PBKDF2 and HKDF, HMAC `sign()` and `verify()`, and SHA-1 and SHA-2 `digest()`.
Secret keys import and export in the `raw`, `raw-secret`, and `jwk` formats.
`generateKey()`, `importKey()`, `exportKey()`, `sign()`, and `verify()` also support ECDSA (P-256, P-384, and P-521), Ed25519, RSASSA-PKCS1-v1_5, and RSA-PSS, with the `spki`, `pkcs8`, and `jwk` formats and `raw` for ECDSA and Ed25519 public keys; ECDSA, Ed25519, and RSA are refused in wasm32-wasi builds.
ECDSA, Ed25519, and RSA link when a string literal in the program names the algorithm; a name built only at run time throws a coded SC2020 error.
`wrapKey()` and `unwrapKey()` of ECDSA, Ed25519, and RSA keys throw a coded SC2020 error.
Other SubtleCrypto methods, algorithms, and key formats are refused or throw a coded SC2020 error.
Native derivations finish before their deferred callbacks, so computation blocks the caller.
@@ -1,16 +1,22 @@
- **Streams and readline:** Stream chunks are Buffers or strings.
- **Streams and readline:** Stream chunks are Buffers or strings, and on an object-mode side any value that converts to a checked-dynamic value.
`NodeJS.ReadableStream` and `NodeJS.WritableStream`, and `stream.Readable` and `stream.Writable` under `@types/node`, accept every stream: when a program passes file, zlib, or `node:stream` streams through them, child-process pipes and `process.stdout` and `process.stderr` join those streams as `Readable` and `Writable` instances.
`readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream.
When an untyped value reaches `NodeJS.ReadableStream` or `NodeJS.WritableStream`, those two interfaces hold any value, so JavaScript stream objects (constructor-function streams, plain objects with `on` and `pipe`) pass through TypeScript functions typed with them and come back to JavaScript as the same objects.
Member calls on such a value use the native stream when it is one and call the object's own members otherwise; `readable.pipe()` into a JavaScript destination follows Node's pipe protocol.
A `pipeline` stage and a `for await` source that arrive as untyped values must be native streams at run time; other values throw a not-supported error.
A `pipeline` stage and a `for await` source that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error.
Native streams expose no `emit` to JavaScript, so a JavaScript stream whose own `pipe()` emits on its destination (the legacy `Stream` pipe) cannot pipe into one.
`Readable.from` accepts arrays, Sets, Map and Set iterators, generators, async generators, and class iterables and async iterables whose values are strings or Buffers, and in JavaScript any iterable or async iterable of them; a stream that mixes strings and Buffers is unsupported.
`Readable.from` accepts strings, Buffers, arrays, Sets, Maps, Map and Set iterators, generators, async generators, and class iterables and async iterables, and in JavaScript any iterable or async iterable.
In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported.
Object-mode streams of other values (including `Readable.from` over a `Map`, whose values are entry arrays) are unsupported.
`pipeline` accepts one run of function stages: async generator functions between streams (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), and an async function destination whose value resolves the promise form and reaches the callback's second argument.
The `objectMode` option, and the `readableObjectMode` and `writableObjectMode` options of a `Duplex`, `Transform`, or `PassThrough`, take `true` or `false` literals in an options object literal; options passed as a value are read when the stream is built.
In typed code, a `write` or `transform` callback, or a `_write` or `_transform` method, may declare its chunk with any type that converts to a checked-dynamic value: the chunk converts when the callback runs, and a value of another type throws a TypeError there.
An object-mode write passes its encoding argument to `write` and `transform` as Node does (`undefined` when absent, `null` when a callback took its place); a parameter declared `string` receives `""` for `undefined` and `null`, and one declared `string | null | undefined` receives Node's value.
`readable.read()` on an object-mode stream answers its Buffers; another value throws a not-supported error (read such values with `'data'` listeners, `for await`, or `pipe`).
`node:stream/consumers` and readline interfaces read object-mode streams of strings and Buffers; other values throw a not-supported error.
`pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), and an async function destination whose value resolves the promise form and reaches the callback's second argument.
A stage is written inline, or named by a function declaration or a `const` bound to a function expression that nothing reassigns.
The source may also be an array, a string, a synchronous generator object, or a synchronous generator function without parameters; their elements reach the next stage on the same ticks as in Node.
Stages may take the `{ signal }` argument.
An async generator function as the destination, using the PassThrough that `pipeline()` returns for a function destination, several runs of function stages, and function stages together with pipeline options are unsupported.
An async generator function as the destination, using the PassThrough that `pipeline()` returns for a function destination, and function stages or iterable sources together with pipeline options are unsupported.
`Readable.from` accepts `objectMode` and `highWaterMark` options.
The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options; their callbacks take only the chunk, `map` returns strings or Buffers, and `flatMap` returns strings or arrays of them.
`readable.compose` is unsupported.
@@ -19,6 +25,7 @@
Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream.
A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error.
`node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile.
`on`, `once`, and `addListener` return the interface, so registrations chain.
The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node.
With `terminal: true`, or with no `terminal` option and `process.stdout` as the output when it is a TTY, the interface runs Node's terminal mode: the editing keys, the kill ring, undo and redo, history recording and recall, Tab completion through the completer, Ctrl+C, Ctrl+D, and Ctrl+Z, `TERM=dumb`, and the same escape sequences written to the output.
The output's `'resize'` event does not redraw the line.
@@ -28,7 +28,7 @@ The `captureRejections` option and `events.errorMonitor` listeners are not suppo
The stream calls the object's `_read`, `_write`, `_final`, `_transform`, `_flush`, and `_destroy` with the object as `this`, and the stream prototypes' methods and accessors work on it.
Its listeners live in the native stream rather than in `_events`, and `_readableState` and `_writableState` are read-only views that answer the commonly read fields.
Typed code that receives such an object as a stream class uses its native stream, and piping a native stream into it works.
The options the native streams refuse (`objectMode`, `decodeStrings: false`, `signal`, and others) are refused here too, as are `_writev`, `_construct`, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, strings written in encodings other than UTF-8, and pipes into a destination that overrides `write()` or `end()`.
The options the native streams refuse (`decodeStrings: false`, `signal`, and others) are refused here too, as are `_writev`, `_construct`, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, strings written to a byte-mode stream in encodings other than UTF-8, and pipes into a destination that overrides `write()` or `end()`.
Other class constructors passed to `util.inherits` or `util.deprecate` are refused.
Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported.
A stored `util.deprecate` reports a `length` of 4, where Node reports 3.
@@ -0,0 +1,5 @@
- **JSX:** JSX in `.tsx`, `.jsx` and JavaScript files compiles with the project's `jsx` setting: `"react"` with `jsxFactory`, `jsxFragmentFactory` or `reactNamespace`, and `"react-jsx"` or `"react-jsxdev"` with `jsxImportSource`, as well as the `@jsx`, `@jsxFrag`, `@jsxImportSource` and `@jsxRuntime` pragmas.
The checker types JSX with these settings, and each element becomes the call TypeScript emits for it, importing the automatic runtime's functions from `<jsxImportSource>/jsx-runtime` or `jsx-dev-runtime`.
`"jsx": "preserve"` and `"react-native"`, which keep JSX in the output, are refused.
The call an element becomes must type-check: a factory or runtime function whose parameters do not accept what an element passes is reported at the element.
With `"react-jsxdev"`, `jsxDEV` receives `undefined` as `self` where `this` has no declared type (in a function that is not a method), which is the value such a function observes when called without a receiver.
@@ -1,4 +1,5 @@
- **Local class expressions:** Static members, decorators, private brands, generic members, and references through the class's internal name are unsupported.
- **Local class expressions:** Static fields and static blocks in TypeScript files, decorators, private brands, generic members, and references through the class's internal name are unsupported.
Static methods read the enclosing function's bindings and are inherited by subclasses.
Supported expressions preserve fresh constructor identity and shared captures across constructors, methods, accessors, and arrow fields.
Local inheritance requires a compiled base class.
Constructors retain identity, `typeof`, `name`, and `length` through `unknown`; cast back to their exact type before construction.
+156 -156
View File
@@ -11177,9 +11177,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Supports the modp groups, explicit primes and generators, and generated safe primes up to 8192 bits with Node's argument, generator and modulus errors.",
"verification": "test-backed"
}
},
{
@@ -11200,9 +11200,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Supports the modp groups, explicit primes and generators, and generated safe primes up to 8192 bits with Node's argument, generator and modulus errors.",
"verification": "test-backed"
}
},
{
@@ -11223,9 +11223,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Supports the modp groups, explicit primes and generators, and generated safe primes up to 8192 bits with Node's argument, generator and modulus errors.",
"verification": "test-backed"
}
},
{
@@ -11247,7 +11247,7 @@
},
"static": {
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"detail": "Supports prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1 and brainpoolP512r1 with Node's curve, key, point, format and encoding errors; other curves Node knows are refused.",
"verification": "test-backed"
}
},
@@ -11338,9 +11338,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -11361,9 +11361,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -11407,9 +11407,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -11430,9 +11430,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -11545,9 +11545,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -11568,9 +11568,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -11729,9 +11729,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Supports the modp groups, explicit primes and generators, and generated safe primes up to 8192 bits with Node's argument, generator and modulus errors.",
"verification": "test-backed"
}
},
{
@@ -12327,9 +12327,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -12373,9 +12373,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -12442,9 +12442,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -12465,9 +12465,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -12581,7 +12581,7 @@
},
"static": {
"status": "partial",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.",
"verification": "test-backed"
}
},
@@ -12604,7 +12604,7 @@
},
"static": {
"status": "partial",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.",
"verification": "test-backed"
}
},
@@ -12627,7 +12627,7 @@
},
"static": {
"status": "partial",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.",
"verification": "test-backed"
}
},
@@ -12650,7 +12650,7 @@
},
"static": {
"status": "partial",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.",
"verification": "test-backed"
}
},
@@ -12673,7 +12673,7 @@
},
"static": {
"status": "partial",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.",
"verification": "test-backed"
}
},
@@ -12696,7 +12696,7 @@
},
"static": {
"status": "partial",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.",
"verification": "test-backed"
}
},
@@ -12719,7 +12719,7 @@
},
"static": {
"status": "partial",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.",
"verification": "test-backed"
}
},
@@ -12742,7 +12742,7 @@
},
"static": {
"status": "partial",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.",
"verification": "test-backed"
}
},
@@ -12765,7 +12765,7 @@
},
"static": {
"status": "partial",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.",
"verification": "test-backed"
}
},
@@ -12788,7 +12788,7 @@
},
"static": {
"status": "partial",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.",
"verification": "test-backed"
}
},
@@ -12811,7 +12811,7 @@
},
"static": {
"status": "partial",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.",
"verification": "test-backed"
}
},
@@ -12834,7 +12834,7 @@
},
"static": {
"status": "partial",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.",
"detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.",
"verification": "test-backed"
}
},
@@ -12856,9 +12856,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.",
"verification": "test-backed"
}
},
{
@@ -12879,9 +12879,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.",
"verification": "test-backed"
}
},
{
@@ -12902,9 +12902,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.",
"verification": "test-backed"
}
},
{
@@ -12925,9 +12925,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.",
"verification": "test-backed"
}
},
{
@@ -12948,9 +12948,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.",
"verification": "test-backed"
}
},
{
@@ -12971,9 +12971,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.",
"verification": "test-backed"
}
},
{
@@ -12994,9 +12994,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.",
"verification": "test-backed"
}
},
{
@@ -13017,9 +13017,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.",
"verification": "test-backed"
}
},
{
@@ -13040,9 +13040,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.",
"verification": "test-backed"
}
},
{
@@ -13063,9 +13063,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.",
"verification": "test-backed"
}
},
{
@@ -13086,9 +13086,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.",
"verification": "test-backed"
}
},
{
@@ -13523,9 +13523,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13546,9 +13546,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13684,9 +13684,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13707,9 +13707,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13730,9 +13730,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13753,9 +13753,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13776,9 +13776,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13799,9 +13799,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -18880,9 +18880,9 @@
"anchor": "err_crypto_invalid_keylen",
"nodeStability": null,
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -19006,9 +19006,9 @@
"anchor": "err_crypto_invalid_state",
"nodeStability": null,
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -19276,9 +19276,9 @@
"anchor": "err_crypto_unknown_dh_group",
"nodeStability": null,
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -67806,9 +67806,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -68565,9 +68565,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -79656,9 +79656,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -79680,7 +79680,7 @@
},
"static": {
"status": "supported",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
@@ -79703,7 +79703,7 @@
},
"static": {
"status": "supported",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
@@ -80001,9 +80001,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80024,9 +80024,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80047,9 +80047,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80139,9 +80139,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80162,9 +80162,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80208,9 +80208,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80277,9 +80277,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -80323,9 +80323,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
File diff suppressed because it is too large Load Diff
+14 -4
View File
@@ -582,19 +582,29 @@ function publicDetail(tier) {
if (/^surface-manifest:node-builtin\.crypto\.create(?:Cipheriv|Decipheriv)$/.test(source)) {
return "Supports aes-128/192/256 in cbc, ctr, and gcm modes and chacha20-poly1305 with Node's argument, IV, tag-length, and key-length errors; other OpenSSL ciphers and KeyObject keys are refused.";
}
if (/^surface-manifest:node-builtin\.crypto\.createECDH$/.test(source)) {
return "Supports prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1 and brainpoolP512r1 with Node's curve, key, point, format and encoding errors; other curves Node knows are refused.";
}
if (
/^surface-manifest:node-builtin\.crypto\.(?:createDiffieHellman|createDiffieHellmanGroup|getDiffieHellman)$/.test(
source,
)
) {
return "Supports the modp groups, explicit primes and generators, and generated safe primes up to 8192 bits with Node's argument, generator and modulus errors.";
}
if (source.startsWith("surface-manifest:"))
return "Implemented for the call shapes accepted by the compiler lowering.";
if (/^compiler-dedicated:assert\.doesNot(?:Throw|Reject)$/.test(source)) {
return "Supports callbacks or native promises with messages, Error classes, regular expressions, and validators accepting unknown values; object expectations and complete AssertionError metadata remain unsupported.";
}
if (/^surface-manifest:node-builtin\.crypto\.createECDH$/.test(source)) {
return "Supports prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1 and brainpoolP512r1 with Node's curve, key, point, format and encoding errors; other curves Node knows are refused.";
}
if (/^compiler-dedicated:(?:ECDH|ecdh\.)/.test(source)) {
return "Matches Node's key formats, point encodings, private-key range checks, key-pair checks and errors on the supported curves; ECDH.convertKey is supported, and property reads and writes are refused.";
}
if (/^compiler-dedicated:(?:DiffieHellman|diffieHellman\.)/.test(source)) {
return "Matches Node's private key lengths, DH_check results, public key range checks, secret padding and errors; primes longer than 8192 bits, property writes and property reads other than verifyError are refused.";
}
if (/^compiler-dedicated:(?:Cipheriv|Decipheriv|cipher\.|decipher\.)/.test(source)) {
return "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.";
return "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.";
}
if (source.startsWith("compiler-dedicated:"))
return "Implemented by a dedicated static compiler/runtime path.";
+17 -7
View File
@@ -42,8 +42,10 @@
"node-builtin.child_process": ["tests/corpus/1361-spawn-events.ts", "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", "tests/corpus/2946-child-fork-ipc/main.ts", "tests/corpus/3127-spawn-runtime-options.ts", "tests/corpus/3130-child-generic-streams.ts", "tests/corpus/3131-child-rest-listeners.js", "tests/corpus/3132-child-generic-stream-completion.ts"],
"node-builtin.cluster": ["tests/corpus/957-builtins-namespace.ts"],
"node-builtin.console": ["tests/corpus/console-imports.cjs", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/console-native-value.js", "tests/harness/npm-static.test.ts"],
"node-builtin.crypto.createCipheriv": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/fixtures/node-types/cipher.ts"],
"node-builtin.crypto.createDecipheriv": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/fixtures/node-types/cipher.ts"],
"node-builtin.crypto.createCipheriv": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"],
"node-builtin.crypto.createDecipheriv": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"],
"node-builtin.crypto.createDiffieHellman": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"],
"node-builtin.crypto.createDiffieHellmanGroup": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"],
"node-builtin.crypto.createECDH": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"],
"node-builtin.crypto": ["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"],
"node-builtin.dgram": ["tests/corpus/2597-dgram-send-ladders.cjs"],
@@ -118,6 +120,7 @@
"node-builtin.os.version": ["tests/corpus/5140-os-host-facts.ts"],
"node-builtin.os.constants": ["tests/corpus/5141-os-constants.ts"],
"node-builtin.fs.constants": ["tests/corpus/6207-fs-constants-object.ts", "tests/corpus/6208-fs-constants-require.cjs"],
"node-builtin.crypto.getDiffieHellman": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"],
"node-builtin.crypto.getRandomValues": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"],
"node-builtin.url.parse": ["tests/corpus/5220-url-parse-warnings.ts", "tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"],
"node-builtin.url.format": ["tests/corpus/5224-url-format-objects.ts", "tests/corpus/5225-url-format-whatwg.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"],
@@ -187,16 +190,18 @@
"stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"],
"stdlib.headers": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.request": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.webcrypto.subtle": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"],
"stdlib.webcrypto.CryptoKey": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"],
"stdlib.webcrypto.subtle": ["tests/corpus/7130-webcrypto-wrap-key.ts", "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"],
"stdlib.webcrypto.CryptoKey": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"],
"stdlib.response": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.readable-stream": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.abort": ["tests/harness/fetch-conformance.test.ts"]
},
"features": [
{ "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] },
{ "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.generateKey", "subtle.exportKey", "subtle.encrypt", "subtle.decrypt", "subtle.wrapKey", "subtle.unwrapKey", "subtle.deriveBits", "subtle.deriveKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7130-webcrypto-wrap-key.ts", "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts", "tests/corpus/7081-webcrypto-ed25519.mjs", "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"] },
{ "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
{ "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
{ "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
{ "chapter": "webcrypto", "signatures": ["Class: `Crypto`"], "status": "partial", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6293-webcrypto-digest.ts"] },
{ "chapter": "webcrypto", "symbols": ["crypto.randomUUID", "crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] },
{ "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] },
{ "chapter": "globals", "signatures": ["`userAgent` Type: {string}"], "status": "supported", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "globals", "signatures": ["Class: `CryptoKey`", "Class: `SubtleCrypto`"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
@@ -367,6 +372,7 @@
{ "chapter": "console", "signatures": ["Class: `Console`"], "status": "partial", "evidence": ["tests/corpus/4900-console-instances.ts", "tests/corpus/4901-console-instance-errors.ts"] },
{ "chapter": "tls", "signatures": ["`alpnProtocol` Type: {string|boolean|null}"], "status": "partial", "evidence": ["tests/corpus/6721-tls-alpn-protocols.ts"] },
{ "chapter": "tls", "signatures": ["`tlsSocket.getProtocol()`"], "status": "partial", "evidence": ["tests/corpus/6722-tls-min-max-version.ts"] },
{ "chapter": "tls", "signatures": ["Class: `tls.TLSSocket`", "Event: `'secure'`"], "status": "partial", "evidence": ["tests/corpus/7087-tls-socket-wrap.ts", "tests/corpus/7088-tls-socket-connect-tunnel.ts", "tests/fixtures/node-types/tls-socket-wrap.ts"] },
{ "chapter": "webstreams", "signatures": ["`readableStream.values([options])`"], "status": "partial", "evidence": ["tests/corpus/2862-web-readable-async-iteration.ts"] },
{ "chapter": "webstreams", "signatures": ["`streamConsumers.text(stream)`", "`streamConsumers.json(stream)`", "`streamConsumers.buffer(stream)`"], "status": "partial", "evidence": ["tests/corpus/2629-stream-consumers.ts", "tests/corpus/2630-stream-consumers-errors.ts"] },
{ "chapter": "webstreams", "signatures": ["`streamConsumers.arrayBuffer(stream)`"], "status": "partial", "evidence": ["tests/corpus/4875-stream-consumers-array-buffer.ts"] },
@@ -412,12 +418,16 @@
{ "symbols": ["assert.partialDeepStrictEqual"], "status": "partial", "evidence": ["tests/corpus/5517-assert-partial-deep-equal.js", "tests/corpus/5518-assert-partial-deep-typed.ts", "tests/corpus/5519-assert-partial-deep-typed-edges.ts"] },
{ "symbols": ["assert.deepEqual", "assert.notDeepEqual"], "status": "partial", "evidence": ["tests/corpus/5513-assert-deep-equal-loose.js", "tests/corpus/5514-assert-deep-equal-typed.ts", "tests/corpus/5516-assert-deep-equal-edges.js"] },
{ "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts"] },
{ "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/fixtures/node-types/cipher.ts"] },
{ "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"] },
{ "chapter": "crypto", "symbols": ["crypto.webcrypto", "subtle"], "status": "partial", "evidence": ["tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs"] },
{ "chapter": "crypto", "symbols": ["ECDH", "ecdh.computeSecret", "ecdh.generateKeys", "ecdh.getPrivateKey", "ecdh.getPublicKey", "ecdh.setPrivateKey", "ecdh.setPublicKey"], "status": "partial", "evidence": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"] },
{ "chapter": "crypto", "symbols": ["DiffieHellman", "DiffieHellmanGroup", "diffieHellman.computeSecret", "diffieHellman.generateKeys", "diffieHellman.getGenerator", "diffieHellman.getPrime", "diffieHellman.getPrivateKey", "diffieHellman.getPublicKey", "diffieHellman.setPrivateKey", "diffieHellman.setPublicKey", "diffieHellman.verifyError"], "status": "partial", "evidence": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"] },
{ "chapter": "errors", "symbols": ["ERR_CRYPTO_UNKNOWN_DH_GROUP", "ERR_CRYPTO_INVALID_KEYLEN", "ERR_CRYPTO_INVALID_STATE"], "status": "partial", "evidence": ["tests/corpus/6708-dh-errors.js"] },
{ "chapter": "errors", "symbols": ["ERR_CRYPTO_ECDH_INVALID_FORMAT", "ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY"], "status": "partial", "evidence": ["tests/corpus/6702-ecdh-errors.js"] },
{ "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] },
{ "symbols": ["crypto.randomFill", "crypto.randomFillSync", "crypto.randomUUID", "crypto.randomUUIDv7"], "status": "supported", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/1358-crypto-random.ts"] },
{ "symbols": ["crypto.createSecretKey", "crypto.generateKey", "crypto.generateKeySync", "KeyObject", "keyObject.export", "keyObject.equals", "keyObject.symmetricKeySize", "keyObject.type"], "status": "partial", "evidence": ["tests/corpus/5445-crypto-secret-key-objects.ts", "tests/corpus/5446-crypto-secret-key-errors.js"] },
{ "symbols": ["crypto.createPrivateKey", "crypto.createPublicKey", "crypto.generateKeyPair", "crypto.generateKeyPairSync", "crypto.sign", "crypto.verify", "crypto.createSign", "crypto.createVerify", "Sign", "sign.update", "sign.sign", "Verify", "verify.update", "verify.verify", "asymmetricKeyType", "asymmetricKeyDetails"], "status": "partial", "evidence": ["tests/corpus/7080-crypto-ed25519.mjs", "tests/corpus/7082-crypto-key-objects.ts", "tests/corpus/7083-crypto-sign-verify.ts", "tests/corpus/7084-crypto-key-pairs.cjs", "tests/fixtures/node-types/keys.ts"] },
{ "symbols": ["crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] },
{ "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] },
{ "symbols": ["crypto.randomInt"], "status": "partial", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/2890-crypto-static-utility.ts"] },
+3 -2
View File
@@ -20,7 +20,7 @@
"test:test262": "tsx tests/test262/run.ts",
"test:test262:fetch": "node tests/test262/fetch.mjs",
"test:cache-identity": "node tests/harness/cache-identity.mjs",
"test:lowering-order": "node scripts/lowering-order.mjs --fail-on content",
"test:lowering-order": "node scripts/lowering-order.mjs --fail-on names",
"test:sandbox": "node scripts/sandbox-test.mjs",
"test:sandbox:image": "node scripts/sandbox-image.mjs",
"test:watch": "vitest",
@@ -41,10 +41,11 @@
"@scriptc/types": "workspace:*",
"@types/node": "^24.0.0",
"effect": "3.22.2",
"js-yaml": "4.3.0",
"js-yaml": "4.3.2",
"oxfmt": "0.71.0",
"oxlint": "1.86.0",
"rollup": "4.62.2",
"smol-toml": "1.7.1",
"three": "0.186.1",
"tsx": "^4.19.0",
"typescript": "5.9.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-darwin-arm64",
"version": "0.4.1",
"version": "0.4.2",
"description": "Native scriptc compiler for darwin-arm64",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-darwin-x64",
"version": "0.4.1",
"version": "0.4.2",
"description": "Native scriptc compiler for darwin-x64",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-arm64-gnu",
"version": "0.4.1",
"version": "0.4.2",
"description": "Native scriptc compiler for linux-arm64-gnu",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-arm64-musl",
"version": "0.4.1",
"version": "0.4.2",
"description": "Native scriptc compiler for linux-arm64-musl",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-x64-gnu",
"version": "0.4.1",
"version": "0.4.2",
"description": "Native scriptc compiler for linux-x64-gnu",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-x64-musl",
"version": "0.4.1",
"version": "0.4.2",
"description": "Native scriptc compiler for linux-x64-musl",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-win32-x64-msvc",
"version": "0.4.1",
"version": "0.4.2",
"description": "Native scriptc compiler for win32-x64-msvc",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "scriptc",
"version": "0.4.1",
"version": "0.4.2",
"description": "Compile ordinary TypeScript and JavaScript to small, fast native executables and WebAssembly modules that run without Node.js",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+55 -1
View File
@@ -1,12 +1,13 @@
import { execFile } from "node:child_process";
import { createRequire } from "node:module";
import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
import { chmod, mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { delimiter, dirname, join } from "node:path";
import { promisify } from "node:util";
import { expect, test } from "vitest";
import { precompiledRuntimePackTarget } from "../../compiler/src/startup-cache.js";
import { platformLinkerSupportsPersistentCache } from "../../compiler/src/backend/linker.js";
import { toolchainEnvironmentCachePolicy } from "../../compiler/src/backend/toolchain-environment.js";
const execFileAsync = promisify(execFile);
const require = createRequire(import.meta.url);
@@ -102,6 +103,59 @@ test("exact executable repeats skip lowering while edits and damaged outputs sta
}
}, 120_000);
// Sanitize builds compile the LLVM text with the C driver rather than linking a precompiled
// runtime pack, and that route's native cache proof records the output directory.
test.skipIf(process.platform === "win32")(
"sanitize builds compile uncached and keep the executable on unchanged repeats",
async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-cli-executable-sanitize-"));
const cacheRoot = join(dir, "cache");
const entry = join(dir, "main.ts");
const outPath = join(dir, "program");
const build = async (env: NodeJS.ProcessEnv = {}): Promise<{ stderr: string }> => {
const { stderr } = await execFileAsync(
process.execPath,
["--import", tsxLoader, cliEntry, "build", entry, "--sanitize", "-o", outPath],
{
env: { ...process.env, SCRIPTC_CACHE_DIR: cacheRoot, SCRIPTC_TIMING: "1", ...env },
maxBuffer: 4 * 1024 * 1024,
},
);
return { stderr };
};
const run = async (): Promise<string> =>
(await execFileAsync(outPath, [], { maxBuffer: 1024 * 1024 })).stdout;
try {
await mkdir(cacheRoot, { mode: 0o700 });
await writeFile(entry, 'console.log("sanitized");\n');
// With the native cache off, clang compiles the build's private copy of the LLVM text.
await build({ SCRIPTC_NO_CACHE: "1" });
expect(await run()).toBe("sanitized\n");
expect((await build()).stderr).toContain("scriptc lowering");
expect(await run()).toBe("sanitized\n");
const built = await stat(outPath);
const tuTime = (await stat(join(dir, "main.ll"))).mtimeMs;
// The output directory does not change after the native cache proof is recorded, so an
// unchanged repeat keeps the executable instead of linking it again.
expect((await build()).stderr).not.toContain("scriptc lowering");
if (toolchainEnvironmentCachePolicy(process.env).completeArtifacts) {
const repeated = await stat(outPath);
expect(repeated.ino).toBe(built.ino);
expect(repeated.mtimeMs).toBe(built.mtimeMs);
}
expect(await run()).toBe("sanitized\n");
expect((await stat(join(dir, "main.ll"))).mtimeMs).toBe(tuTime);
expect((await readdir(dir)).sort()).toEqual(["cache", "main.ll", "main.ts", "program"]);
} finally {
await rm(dir, { recursive: true, force: true });
}
},
240_000,
);
test.skipIf(process.platform === "win32")(
"an executable appearing earlier on unchanged PATH invalidates the early binary",
async () => {
+2 -2
View File
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
minimum_os: "14.0",
},
program: { object, entry_symbol: "main" },
runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" },
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
runtime_pack: {
kind: "precompiled",
package: "@scriptc/runtime-darwin-arm64",
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
() => null,
(failure: { stderr?: string }) => failure,
);
expect(error?.stderr).toContain("scr_runtime_abi_v23");
expect(error?.stderr).toContain("scr_runtime_abi_v27");
});
});
+536 -39
View File
@@ -544,12 +544,17 @@ declare var DOMException: {
readonly prototype: DOMException;
};
/* WebCrypto's HMAC slice (scr_webcrypto.c): crypto.subtle's importKey for
* raw HMAC keys, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as
* exactly that surface (the declarations mirror @types/node's webcrypto
* shapes). Every other SubtleCrypto method and crypto member is absent,
* so a use is an honest type error. CryptoKey values are native handles
* whose four members read through the handle. */
/* WebCrypto (scr_webcrypto.c): crypto.subtle's importKey, generateKey and
* exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF
* keys), AES-GCM/CBC/CTR encrypt and decrypt, wrapKey and unwrapKey of
* secret keys (AES-KW or those ciphers), PBKDF2 and HKDF deriveBits and
* deriveKey, HMAC sign/verify, generateKey, importKey, exportKey, sign
* and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and the
* SHA-1/SHA-2 digests, typed as exactly that surface (the declarations
* mirror @types/node's webcrypto shapes), plus the global's randomUUID and
* getRandomValues. Every other SubtleCrypto method and crypto member is
* absent, so a use is an honest type error. CryptoKey values are native
* handles whose four members read through the handle. */
declare var crypto: Crypto;
/* The Crypto and SubtleCrypto constructors (feature probes only) stay
* `unknown`; the values keep this declaration order. */
@@ -582,9 +587,44 @@ interface HmacImportParams extends Algorithm {
hash: HashAlgorithmIdentifier;
length?: number;
}
interface HmacKeyGenParams extends Algorithm {
hash: HashAlgorithmIdentifier;
length?: number;
}
interface AesKeyGenParams extends Algorithm {
length: number;
}
interface AesGcmParams extends Algorithm {
additionalData?: BufferSource;
iv: BufferSource;
tagLength?: number;
}
interface AesCbcParams extends Algorithm {
iv: BufferSource;
}
interface AesCtrParams extends Algorithm {
counter: BufferSource;
length: number;
}
interface Pbkdf2Params extends Algorithm {
hash: HashAlgorithmIdentifier;
iterations: number;
salt: BufferSource;
}
interface HkdfParams extends Algorithm {
hash: HashAlgorithmIdentifier;
info: BufferSource;
salt: BufferSource;
}
interface AesDerivedKeyParams extends Algorithm {
length: number;
}
interface KeyAlgorithm {
name: string;
}
interface AesKeyAlgorithm extends KeyAlgorithm {
length: number;
}
interface HmacKeyAlgorithm extends KeyAlgorithm {
hash: KeyAlgorithm;
length: number;
@@ -623,34 +663,136 @@ interface JsonWebKey {
x?: string;
y?: string;
}
/* The 'jwk' form type-checks like @types/node's; the compiler refuses it
* (the JsonWebKey import is not implemented). */
interface EcKeyGenParams extends Algorithm {
namedCurve: string;
}
interface EcKeyImportParams extends Algorithm {
namedCurve: string;
}
interface EcdsaParams extends Algorithm {
hash: HashAlgorithmIdentifier;
}
interface RsaHashedKeyGenParams extends Algorithm {
modulusLength: number;
publicExponent: Uint8Array;
hash: HashAlgorithmIdentifier;
}
interface RsaHashedImportParams extends Algorithm {
hash: HashAlgorithmIdentifier;
}
interface RsaPssParams extends Algorithm {
saltLength: number;
}
interface EcKeyAlgorithm extends KeyAlgorithm {
namedCurve: string;
}
interface RsaHashedKeyAlgorithm extends KeyAlgorithm {
modulusLength: number;
publicExponent: Uint8Array;
hash: KeyAlgorithm;
}
interface CryptoKeyPair {
privateKey: CryptoKey;
publicKey: CryptoKey;
}
/* HMAC and AES keys generate, import and export as 'raw' and 'jwk'
* (PBKDF2 and HKDF keys import as 'raw'); ECDSA and RSA keys generate,
* import and export as 'spki', 'pkcs8' and 'jwk', and ECDSA public keys as
* 'raw'. */
interface SubtleCrypto {
decrypt(
algorithm: AesGcmParams | AesCbcParams | AesCtrParams,
key: CryptoKey,
data: BufferSource,
): Promise<ArrayBuffer>;
deriveBits(
algorithm: Pbkdf2Params | HkdfParams,
baseKey: CryptoKey,
length?: number | null,
): Promise<ArrayBuffer>;
deriveKey(
algorithm: Pbkdf2Params | HkdfParams,
baseKey: CryptoKey,
derivedKeyType: AlgorithmIdentifier | AesDerivedKeyParams | HmacImportParams,
extractable: boolean,
keyUsages: readonly KeyUsage[],
): Promise<CryptoKey>;
digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise<ArrayBuffer>;
encrypt(
algorithm: AesGcmParams | AesCbcParams | AesCtrParams,
key: CryptoKey,
data: BufferSource,
): Promise<ArrayBuffer>;
exportKey(format: "jwk", key: CryptoKey): Promise<JsonWebKey>;
exportKey(format: Exclude<KeyFormat, "jwk">, key: CryptoKey): Promise<ArrayBuffer>;
generateKey(
algorithm: EcKeyGenParams | RsaHashedKeyGenParams | "Ed25519" | { name: "Ed25519" },
extractable: boolean,
keyUsages: readonly KeyUsage[],
): Promise<CryptoKeyPair>;
generateKey(
algorithm: AesKeyGenParams | HmacKeyGenParams,
extractable: boolean,
keyUsages: readonly KeyUsage[],
): Promise<CryptoKey>;
importKey(
format: "jwk",
keyData: JsonWebKey,
algorithm: AlgorithmIdentifier | HmacImportParams,
algorithm: AlgorithmIdentifier | HmacImportParams | EcKeyImportParams | RsaHashedImportParams,
extractable: boolean,
keyUsages: readonly KeyUsage[],
): Promise<CryptoKey>;
importKey(
format: Exclude<KeyFormat, "jwk">,
keyData: BufferSource,
algorithm: AlgorithmIdentifier | HmacImportParams,
algorithm: AlgorithmIdentifier | HmacImportParams | EcKeyImportParams | RsaHashedImportParams,
extractable: boolean,
keyUsages: readonly KeyUsage[],
): Promise<CryptoKey>;
sign(
algorithm: AlgorithmIdentifier | EcdsaParams | RsaPssParams,
key: CryptoKey,
data: BufferSource,
): Promise<ArrayBuffer>;
unwrapKey(
format: KeyFormat,
wrappedKey: BufferSource,
unwrappingKey: CryptoKey,
unwrapAlgorithm: AlgorithmIdentifier | AesGcmParams | AesCbcParams | AesCtrParams,
unwrappedKeyAlgorithm: AlgorithmIdentifier | HmacImportParams,
extractable: boolean,
keyUsages: readonly KeyUsage[],
): Promise<CryptoKey>;
sign(algorithm: AlgorithmIdentifier, key: CryptoKey, data: BufferSource): Promise<ArrayBuffer>;
verify(
algorithm: AlgorithmIdentifier,
algorithm: AlgorithmIdentifier | EcdsaParams | RsaPssParams,
key: CryptoKey,
signature: BufferSource,
data: BufferSource,
): Promise<boolean>;
wrapKey(
format: KeyFormat,
key: CryptoKey,
wrappingKey: CryptoKey,
wrapAlgorithm: AlgorithmIdentifier | AesGcmParams | AesCbcParams | AesCtrParams,
): Promise<ArrayBuffer>;
}
interface Crypto {
readonly subtle: SubtleCrypto;
getRandomValues<
T extends
| Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| BigInt64Array
| BigUint64Array,
>(
typedArray: T,
): T;
randomUUID(): `${string}-${string}-${string}-${string}-${string}`;
}
/* Capability-gated globals harness code probes behind feature guards
@@ -2693,8 +2835,20 @@ declare module "crypto" {
import { Transform } from "stream";
export type KeyObjectType = "secret" | "public" | "private";
export interface JsonWebKey {
kty?: string;
crv?: string;
d?: string;
dp?: string;
dq?: string;
e?: string;
k?: string;
kty?: string;
n?: string;
p?: string;
q?: string;
qi?: string;
x?: string;
y?: string;
[key: string]: unknown;
}
export interface JwkKeyExportOptions {
format: "jwk";
@@ -2702,16 +2856,183 @@ declare module "crypto" {
export interface KeyExportOptions {
format?: "buffer" | undefined;
}
/** Secret keys only; the asymmetric key types are not available. */
export type KeyFormat = "pem" | "der" | "jwk";
export interface AsymmetricKeyExportOptions<T extends KeyFormat> {
type: "pkcs1" | "spki" | "pkcs8" | "sec1";
format: T;
}
export interface AsymmetricKeyDetails {
modulusLength?: number;
publicExponent?: bigint;
namedCurve?: string;
}
/* Secret keys, and RSA and EC private and public keys. */
export class KeyObject {
private constructor();
type: KeyObjectType;
symmetricKeySize?: number;
asymmetricKeyType?: string;
asymmetricKeyDetails?: AsymmetricKeyDetails;
export(options: AsymmetricKeyExportOptions<"pem">): string | Buffer;
export(options: AsymmetricKeyExportOptions<"der">): Buffer;
export(options?: KeyExportOptions): Buffer;
export(options: JwkKeyExportOptions): JsonWebKey;
equals(otherKeyObject: KeyObject): boolean;
}
export interface PrivateKeyInput {
key: string | Buffer;
format?: "pem" | "der" | undefined;
type?: "pkcs1" | "pkcs8" | "sec1" | undefined;
encoding?: string | undefined;
}
export interface PublicKeyInput {
key: string | Buffer;
format?: "pem" | "der" | undefined;
type?: "pkcs1" | "spki" | undefined;
encoding?: string | undefined;
}
export interface JsonWebKeyInput {
key: JsonWebKey;
format: "jwk";
}
export function createPrivateKey(
key: PrivateKeyInput | string | Buffer | JsonWebKeyInput,
): KeyObject;
export function createPublicKey(
key: PublicKeyInput | string | Buffer | KeyObject | JsonWebKeyInput,
): KeyObject;
export interface KeyPairKeyObjectResult {
publicKey: KeyObject;
privateKey: KeyObject;
}
export interface KeyPairSyncResult<T1 extends string | Buffer, T2 extends string | Buffer> {
publicKey: T1;
privateKey: T2;
}
export interface RSAKeyPairKeyObjectOptions {
modulusLength: number;
publicExponent?: number | undefined;
}
export interface ECKeyPairKeyObjectOptions {
namedCurve: string;
}
export interface RSAKeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
modulusLength: number;
publicExponent?: number | undefined;
publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF };
privateKeyEncoding: { type: "pkcs1" | "pkcs8"; format: PrivF };
}
export interface ECKeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
namedCurve: string;
publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF };
privateKeyEncoding: { type: "sec1" | "pkcs8"; format: PrivF };
}
export function generateKeyPairSync(
type: "rsa",
options: RSAKeyPairOptions<"pem", "pem">,
): KeyPairSyncResult<string, string>;
export function generateKeyPairSync(
type: "rsa",
options: RSAKeyPairOptions<"der", "der">,
): KeyPairSyncResult<Buffer, Buffer>;
export function generateKeyPairSync(
type: "rsa",
options: RSAKeyPairKeyObjectOptions,
): KeyPairKeyObjectResult;
export function generateKeyPairSync(
type: "ec",
options: ECKeyPairOptions<"pem", "pem">,
): KeyPairSyncResult<string, string>;
export function generateKeyPairSync(
type: "ec",
options: ECKeyPairOptions<"der", "der">,
): KeyPairSyncResult<Buffer, Buffer>;
export function generateKeyPairSync(
type: "ec",
options: ECKeyPairKeyObjectOptions,
): KeyPairKeyObjectResult;
export interface ED25519KeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
publicKeyEncoding: { type: "spki"; format: PubF };
privateKeyEncoding: { type: "pkcs8"; format: PrivF };
}
export function generateKeyPairSync(
type: "ed25519",
options: ED25519KeyPairOptions<"pem", "pem">,
): KeyPairSyncResult<string, string>;
export function generateKeyPairSync(
type: "ed25519",
options: ED25519KeyPairOptions<"der", "der">,
): KeyPairSyncResult<Buffer, Buffer>;
export function generateKeyPairSync(type: "ed25519", options?: {}): KeyPairKeyObjectResult;
export function generateKeyPair(
type: "ed25519",
options: {} | undefined,
callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void,
): void;
export function generateKeyPair(
type: "rsa",
options: RSAKeyPairKeyObjectOptions,
callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void,
): void;
export function generateKeyPair(
type: "ec",
options: ECKeyPairKeyObjectOptions,
callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void,
): void;
export interface SigningOptions {
padding?: number | undefined;
saltLength?: number | undefined;
dsaEncoding?: "der" | "ieee-p1363" | undefined;
}
export interface SignPrivateKeyInput extends PrivateKeyInput, SigningOptions {}
export interface SignKeyObjectInput extends SigningOptions {
key: KeyObject;
}
export interface VerifyPublicKeyInput extends PublicKeyInput, SigningOptions {}
export interface VerifyKeyObjectInput extends SigningOptions {
key: KeyObject;
}
export function sign(
algorithm: string | null | undefined,
data: ArrayBufferView,
key: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput,
): Buffer;
export function verify(
algorithm: string | null | undefined,
data: ArrayBufferView,
key: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput,
signature: ArrayBufferView,
): boolean;
/* Sign and Verify accumulate update() data; they are not usable as
* writable streams. */
export class Sign {
private constructor();
update(data: string | ArrayBufferView): this;
update(data: string, inputEncoding: Encoding): this;
sign(
privateKey: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput,
): Buffer;
sign(
privateKey: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput,
outputFormat: BinaryToTextEncoding,
): string;
}
export class Verify {
private constructor();
update(data: string | ArrayBufferView): this;
update(data: string, inputEncoding: Encoding): this;
verify(
object: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput,
signature: ArrayBufferView,
): boolean;
verify(
object: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput,
signature: string,
signatureEncoding?: BinaryToTextEncoding,
): boolean;
}
export function createSign(algorithm: string): Sign;
export function createVerify(algorithm: string): Verify;
export function createSecretKey(key: ArrayBufferView | ArrayBufferLike): KeyObject;
export function createSecretKey(key: string, encoding: BufferEncoding): KeyObject;
export function generateKeySync(type: "hmac" | "aes", options: { length: number }): KeyObject;
@@ -2784,8 +3105,7 @@ declare module "crypto" {
b: ArrayBufferView | ArrayBufferLike,
): boolean;
/* WebCrypto's getRandomValues (also the webcrypto object's): integer
* typed arrays, filled in place and returned. The crypto GLOBAL stays
* unknown in fallback builds. */
* typed arrays, filled in place and returned. */
type RandomValuesArray =
| Int8Array
| Uint8Array
@@ -2797,9 +3117,9 @@ declare module "crypto" {
| BigInt64Array
| BigUint64Array;
export function getRandomValues<T extends RandomValuesArray>(typedArray: T): T;
export const webcrypto: {
getRandomValues<T extends RandomValuesArray>(typedArray: T): T;
};
/* The WebCrypto global's object and its SubtleCrypto. */
export const webcrypto: Crypto;
export const subtle: SubtleCrypto;
export function randomFillSync<T extends ArrayBufferView | ArrayBufferLike>(
buffer: T,
offset?: number,
@@ -2852,7 +3172,8 @@ declare module "crypto" {
): void;
export type BinaryLike = string | ArrayBuffer | ArrayBufferView;
/* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, and gcm modes and
* chacha20-poly1305. The stream.Transform members are not declared. */
* chacha20-poly1305. Both are Transforms: written chunks go through
* update(), and the end pushes final(). */
export type CipherGCMTypes = "aes-128-gcm" | "aes-192-gcm" | "aes-256-gcm";
export type CipherChaCha20Poly1305Types = "chacha20-poly1305";
export type CipherKey = BinaryLike | KeyObject;
@@ -2862,7 +3183,7 @@ declare module "crypto" {
export interface CipherChaCha20Poly1305Options {
authTagLength?: number;
}
export interface Cipheriv {
export interface Cipheriv extends Transform {
update(data: string | ArrayBufferView): Buffer;
update(data: string, inputEncoding: BufferEncoding): Buffer;
update(data: ArrayBufferView, inputEncoding: undefined, outputEncoding: BufferEncoding): string;
@@ -2883,7 +3204,7 @@ declare module "crypto" {
setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this;
getAuthTag(): Buffer;
}
export interface Decipheriv {
export interface Decipheriv extends Transform {
update(data: ArrayBufferView): Buffer;
update(data: string, inputEncoding: BufferEncoding): Buffer;
update(data: ArrayBufferView, inputEncoding: undefined, outputEncoding: BufferEncoding): string;
@@ -2968,6 +3289,68 @@ declare module "crypto" {
setPrivateKey(privateKey: string, encoding: BinaryToTextEncoding): void;
}
export function createECDH(curveName: string): ECDH;
export class DiffieHellman {
private constructor();
generateKeys(): Buffer;
generateKeys(encoding: BinaryToTextEncoding): string;
computeSecret(
otherPublicKey: ArrayBufferView,
inputEncoding?: null,
outputEncoding?: null,
): Buffer;
computeSecret(
otherPublicKey: string,
inputEncoding: BinaryToTextEncoding,
outputEncoding?: null,
): Buffer;
computeSecret(
otherPublicKey: ArrayBufferView,
inputEncoding: null,
outputEncoding: BinaryToTextEncoding,
): string;
computeSecret(
otherPublicKey: string,
inputEncoding: BinaryToTextEncoding,
outputEncoding: BinaryToTextEncoding,
): string;
getPrime(): Buffer;
getPrime(encoding: BinaryToTextEncoding): string;
getGenerator(): Buffer;
getGenerator(encoding: BinaryToTextEncoding): string;
getPublicKey(): Buffer;
getPublicKey(encoding: BinaryToTextEncoding): string;
getPrivateKey(): Buffer;
getPrivateKey(encoding: BinaryToTextEncoding): string;
setPublicKey(publicKey: ArrayBufferView): void;
setPublicKey(publicKey: string, encoding: BufferEncoding): void;
setPrivateKey(privateKey: ArrayBufferView): void;
setPrivateKey(privateKey: string, encoding: BufferEncoding): void;
verifyError: number;
}
export type DiffieHellmanGroup = Omit<DiffieHellman, "setPublicKey" | "setPrivateKey">;
export function createDiffieHellman(primeLength: number, generator?: number): DiffieHellman;
export function createDiffieHellman(
prime: ArrayBuffer | ArrayBufferView,
generator?: number | ArrayBuffer | ArrayBufferView,
): DiffieHellman;
export function createDiffieHellman(
prime: ArrayBuffer | ArrayBufferView,
generator: string,
generatorEncoding: BinaryToTextEncoding,
): DiffieHellman;
export function createDiffieHellman(
prime: string,
primeEncoding: BinaryToTextEncoding,
generator?: number | ArrayBuffer | ArrayBufferView,
): DiffieHellman;
export function createDiffieHellman(
prime: string,
primeEncoding: BinaryToTextEncoding,
generator: string,
generatorEncoding: BinaryToTextEncoding,
): DiffieHellman;
export function getDiffieHellman(groupName: string): DiffieHellmanGroup;
export function createDiffieHellmanGroup(name: string): DiffieHellmanGroup;
export interface ScryptOptions {
N?: number;
r?: number;
@@ -4778,8 +5161,8 @@ declare module "net" {
* unshift — read answers exactly n buffered bytes or null. */
read(size?: number): Buffer | null;
unshift(chunk: Uint8Array): void;
/* secureConnect/session are TLS-socket events declared on the shared
* socket surface (the lowering collapses TLSSocket onto this kind):
/* secureConnect/secure/session are TLS-socket events declared on the
* shared socket surface (the lowering collapses TLSSocket onto this kind):
* the runtime gates on the transport, so a plain socket's
* registration never fires — Node's exact split. 'session' fires
* once with the serialized session (the received-ticket event). */
@@ -4787,7 +5170,15 @@ declare module "net" {
/* After setEncoding the chunk is a string. */
on(event: "data", listener: (chunk: string) => void): void;
on(
event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect",
event:
| "end"
| "connect"
| "ready"
| "timeout"
| "readable"
| "finish"
| "secureConnect"
| "secure",
listener: () => void,
): void;
on(event: "close", listener: (hadError: boolean) => void): void;
@@ -4803,7 +5194,15 @@ declare module "net" {
/* After setEncoding the chunk is a string. */
addListener(event: "data", listener: (chunk: string) => void): void;
addListener(
event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect",
event:
| "end"
| "connect"
| "ready"
| "timeout"
| "readable"
| "finish"
| "secureConnect"
| "secure",
listener: () => void,
): void;
addListener(event: "close", listener: (hadError: boolean) => void): void;
@@ -4818,7 +5217,15 @@ declare module "net" {
/* After setEncoding the chunk is a string. */
once(event: "data", listener: (chunk: string) => void): void;
once(
event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect",
event:
| "end"
| "connect"
| "ready"
| "timeout"
| "readable"
| "finish"
| "secureConnect"
| "secure",
listener: () => void,
): void;
once(event: "close", listener: (hadError: boolean) => void): void;
@@ -4869,9 +5276,10 @@ declare module "net" {
on(event: "timeout", listener: (socket: Socket) => void): void;
on(event: "error", listener: (err: Error) => void): void;
/* The WebSocket handover: fires INSTEAD of 'request' for
* Connection: upgrade requests, with the raw socket + head bytes. */
* Connection: upgrade requests, with the raw socket + head bytes;
* 'connect' is its twin for CONNECT requests (HTTP tunneling). */
on(
event: "upgrade",
event: "upgrade" | "connect",
listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void,
): void;
/* http.createServer(handler)'s event twin (a no-parser net server
@@ -4887,7 +5295,7 @@ declare module "net" {
addListener(event: "timeout", listener: (socket: Socket) => void): void;
addListener(event: "error", listener: (err: Error) => void): void;
addListener(
event: "upgrade",
event: "upgrade" | "connect",
listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void,
): void;
addListener(
@@ -4900,7 +5308,7 @@ declare module "net" {
once(event: "timeout", listener: (socket: Socket) => void): void;
once(event: "error", listener: (err: Error) => void): void;
once(
event: "upgrade",
event: "upgrade" | "connect",
listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void,
): void;
once(
@@ -5371,6 +5779,7 @@ declare module "tls" {
* split — authorizationError is the verify-failure CODE STRING
* (DEPTH_ZERO_SELF_SIGNED_CERT, ...), or null. */
export interface TLSSocket extends Socket {
readonly encrypted: true;
authorized: boolean;
authorizationError: string | null;
/* null before the handshake, then the negotiated protocol or false */
@@ -5382,6 +5791,21 @@ declare module "tls" {
/* whether the handshake resumed a cached session */
isSessionReused(): boolean;
}
/** new tls.TLSSocket(socket, options): the server-mode wrap of an open
* socket (a server connection or an http CONNECT socket). The options
* record reads at runtime; isServer, cert, key, secureContext,
* ALPNProtocols, minVersion and maxVersion are implemented. */
export interface TLSSocketOptions {
isServer?: boolean;
cert?: string | Uint8Array;
key?: string | Uint8Array;
secureContext?: SecureContext;
ALPNProtocols?: string[] | Uint8Array;
minVersion?: string;
maxVersion?: string;
[option: string]: unknown;
}
export const TLSSocket: new (socket: Socket, options: TLSSocketOptions) => TLSSocket;
export interface CipherNameAndProtocol {
name: string;
standardName: string;
@@ -5390,12 +5814,14 @@ declare module "tls" {
export const DEFAULT_MIN_VERSION: string;
export const DEFAULT_MAX_VERSION: string;
/** tls.connect options — the RUNTIME options record (members read at
* runtime; port/host/rejectUnauthorized/ca/servername are the
* runtime; port/host/socket/rejectUnauthorized/ca/servername are the
* honestly-implemented set, every other documented member throws the
* runtime fence — the options-record stance's divergence-66 shape). */
export interface ConnectionOptions {
port?: number;
host?: string;
/* an open socket to run the client handshake over */
socket?: Socket;
rejectUnauthorized?: boolean;
ca?: string | Uint8Array | ReadonlyArray<string | Uint8Array>;
servername?: string;
@@ -6630,11 +7056,10 @@ declare module "node:events" {
* write(chunk, enc, cb) {} })`, Duplex/Transform/PassThrough, push/read,
* flowing vs paused, write/end backpressure + 'drain', pipe/unpipe, and
* destroy — with Node's event orderings. Chunks are BYTES (Buffer), with
* utf8 strings converted at push/write like Node; objectMode and
* setEncoding are declared so uses fence at their sites with their own
* words. `class My extends Readable` (the _read override form) is a
* separate feature and fences at the class declaration. The runtime-fired
* events carry per-class FORCED argument tuples ('data' is one Buffer,
* strings converted at push/write like Node, and any value on an
* object-mode side (objectMode, readableObjectMode, writableObjectMode —
* the object-mode constructor overloads come first). The runtime-fired
* events carry per-class FORCED argument tuples ('data' is one chunk,
* 'error' one Error, 'pipe'/'unpipe' one Readable, the rest empty) — the
* overloads below hand tsc the same contract so unannotated listener
* parameters contextually type in .js sources. */
@@ -6740,6 +7165,51 @@ declare module "stream" {
signal?: AbortSignal;
}
/* Object-mode options (objectMode: true, or a Duplex's or Transform's
* readableObjectMode / writableObjectMode): chunks and completion data
* are any values, and an object-mode write's encoding argument reaches
* write/transform as passed (undefined when absent, null when a
* callback took its place). The constructors take these before the
* byte-mode options, so a byte-mode stream's callbacks keep Buffer
* chunks. */
type ObjectModeEncoding = string | null | undefined;
type ObjectModeFlag =
| { objectMode: true }
| { readableObjectMode: true }
| { writableObjectMode: true };
interface ObjectWritableOptions extends Omit<WritableOptions, "objectMode" | "write"> {
objectMode: true;
write?(
this: Writable,
chunk: any,
encoding: ObjectModeEncoding,
callback: (error?: Error | null) => void,
): void;
}
type ObjectDuplexOptions = Omit<DuplexOptions, "write"> & {
write?(
this: Duplex,
chunk: any,
encoding: ObjectModeEncoding,
callback: (error?: Error | null) => void,
): void;
} & ObjectModeFlag;
type ObjectTransformOptions = Omit<TransformOptions, "transform" | "flush" | "write"> & {
transform?(
this: Transform,
chunk: any,
encoding: ObjectModeEncoding,
callback: (error?: Error | null, data?: any) => void,
): void;
flush?(this: Transform, callback: (error?: Error | null, data?: any) => void): void;
write?(
this: Transform,
chunk: any,
encoding: ObjectModeEncoding,
callback: (error?: Error | null) => void,
): void;
} & ObjectModeFlag;
/* The operators' options: concurrency and highWaterMark bound map's
* in-flight callbacks and queued results. */
interface ArrayOptions {
@@ -6759,7 +7229,10 @@ declare module "stream" {
[Symbol.asyncIterator](): AsyncIterableIterator<any>;
iterator(options?: { destroyOnReturn?: boolean }): AsyncIterableIterator<any>;
push(chunk: Buffer | string | null, encoding?: string): boolean;
/* An object-mode stream's values. */
push(chunk: any, encoding?: string): boolean;
unshift(chunk: Buffer | string, encoding?: string): void;
unshift(chunk: any, encoding?: string): void;
read(size?: number): Buffer | null;
pause(): this;
resume(): this;
@@ -6866,6 +7339,7 @@ declare module "stream" {
}
class Writable extends EventEmitter {
constructor(opts: ObjectWritableOptions);
constructor(opts?: WritableOptions);
/* The underscore-method surface (see Readable). */
_write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void;
@@ -6881,9 +7355,19 @@ declare module "stream" {
encoding: string,
callback?: (error?: Error | null) => void,
): boolean;
/* An object-mode stream's values (the encoding argument reaches
* _write as passed). */
write(chunk: any, callback?: (error?: Error | null) => void): boolean;
write(
chunk: any,
encoding: ObjectModeEncoding,
callback?: (error?: Error | null) => void,
): boolean;
end(callback?: () => void): this;
end(chunk: Buffer | string, callback?: () => void): this;
end(chunk: Buffer | string, encoding: string, callback?: () => void): this;
end(chunk: any, callback?: () => void): this;
end(chunk: any, encoding: ObjectModeEncoding, callback?: () => void): this;
cork(): void;
uncork(): void;
setDefaultEncoding(encoding: string): this;
@@ -6933,6 +7417,7 @@ declare module "stream" {
* mirrors @types/node: extends Readable, the writable half declared
* again. The lowering treats both halves as first-class. */
class Duplex extends Readable {
constructor(opts: ObjectDuplexOptions);
constructor(opts?: DuplexOptions);
/* The underscore-method surface (see Readable). */
_write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void;
@@ -6947,9 +7432,19 @@ declare module "stream" {
encoding: string,
callback?: (error?: Error | null) => void,
): boolean;
/* An object-mode stream's values (the encoding argument reaches
* _write as passed). */
write(chunk: any, callback?: (error?: Error | null) => void): boolean;
write(
chunk: any,
encoding: ObjectModeEncoding,
callback?: (error?: Error | null) => void,
): boolean;
end(callback?: () => void): this;
end(chunk: Buffer | string, callback?: () => void): this;
end(chunk: Buffer | string, encoding: string, callback?: () => void): this;
end(chunk: any, callback?: () => void): this;
end(chunk: any, encoding: ObjectModeEncoding, callback?: () => void): this;
cork(): void;
uncork(): void;
setDefaultEncoding(encoding: string): this;
@@ -7021,16 +7516,18 @@ declare module "stream" {
}
class Transform extends Duplex {
constructor(opts: ObjectTransformOptions);
constructor(opts?: TransformOptions);
/* The underscore-method surface (see Readable). */
_transform(
chunk: any,
encoding: string,
callback: (error?: Error | null, data?: Buffer | string) => void,
callback: (error?: Error | null, data?: any) => void,
): void;
_flush(callback: (error?: Error | null, data?: Buffer | string) => void): void;
_flush(callback: (error?: Error | null, data?: any) => void): void;
}
class PassThrough extends Transform {
constructor(opts: ObjectTransformOptions);
constructor(opts?: TransformOptions);
}
Binary file not shown.
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/compiler",
"version": "0.4.1",
"version": "0.4.2",
"description": "The scriptc compiler — TypeScript/JavaScript frontend, typed IR, LLVM backend",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
@@ -40,7 +40,7 @@
},
"dependencies": {
"@scriptc/runtime": "workspace:*",
"tsc-ts": "0.0.5",
"tsc-ts": "0.0.7",
"typescript": "7.0.2",
"typescript5": "npm:typescript@5.9.3"
},
@@ -59,6 +59,9 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
tlsCa: features.tlsCa,
cipher: features.cipher,
ecdh: features.ecdh,
dh: features.dh,
pki: features.pki,
ed25519: features.ed25519,
webcrypto: features.webcrypto,
perfLoop: features.perfLoop,
};
@@ -60,6 +60,9 @@ export async function compilePackedLibrary(
tls: false,
cipher: false,
ecdh: false,
dh: false,
pki: false,
ed25519: false,
webcrypto: false,
perfLoop: false,
tlsCa: false,
@@ -109,6 +109,8 @@ function match(
ranges,
initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(),
proven: new Set(),
workers: false,
owned: new Set(),
});
}
@@ -132,6 +134,39 @@ test("a counted loop's affine accesses get one guard and exact operand ranges",
expect(version.ranges.get(shifted)).toEqual({ min: 1, max: Number.MAX_SAFE_INTEGER });
});
test("invariant terms without a range are tested by the guard", () => {
// for (let i = 0; i < n; i++) out[i] = src[k + i], with `k` a parameter
const shifted = bin("+", ref("k"), ref("i"));
const loop = counted([set(ref("out", f64s), ref("i"), get(ref("src", f64s), shifted))]);
const kernel = fn([loop]);
const version = match(kernel, loop)!;
expect([...version.guarded]).toEqual(["v1:k"]);
expect(version.affine.map((c) => c.terms.map((t) => t.key))).toEqual([[], ["v1:k"]]);
// In the fast copy the term has the range the guard tests.
expect(version.ranges.get(shifted.kind === "bin" ? shifted.left : shifted)).toEqual({
min: -(2 ** 32),
max: 2 ** 32,
});
const mod: IrModule = {
irVersion: 17,
sourceFile: loc.file,
entry: "__main",
functions: [
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
kernel,
],
};
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
const guard = body.slice(0, body.search(/^bytes\.version\.(fast|slow)\d+:$/m));
// -2^32 <= k <= 2^32, and k is the double of its frozen integer.
expect(guard).toMatch(/fcmp oge double %\S+, 0xC1F0000000000000/);
expect(guard).toMatch(/fcmp ole double %\S+, 0x41F0000000000000/);
expect(guard).toMatch(/freeze i64/);
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0];
expect(fast).not.toContain("bytes.index");
expect(body.slice(0, body.indexOf(fast))).toContain("bytes.index.invalid");
});
test("statically ranged indices compare one constant with the length", () => {
const masked = bin("&", bin("^", ref("i"), ref("k")), num(255));
const loop = counted([set(ref("out", f64s), num(0), get(ref("table", i32s), masked))]);
@@ -209,6 +244,120 @@ test("emitted loops version the body and tag typed-array memory", () => {
expect(elements).not.toBe(header);
});
test("loops to a typed array's length version the other typed arrays they index", () => {
const length = (receiver: IrExpr): IrExpr => ({
kind: "bytesIntrinsic",
method: "length",
receiver,
args: [],
type: F64,
loc,
});
const emit = (body: IrStmt[]): string => {
const mod: IrModule = {
irVersion: 17,
sourceFile: loc.file,
entry: "__main",
functions: [
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
fn(body),
],
};
return /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
};
// for (let i = 0; i < out.length; i++) out[i] = src[i] * 2
const scaled = set(ref("out", f64s), ref("i"), bin("*", get(ref("src", f64s), ref("i")), num(2)));
const body = emit([counted([scaled], length(ref("out", f64s)))]);
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0];
// The loop keeps its integer induction, and the fast copy reads `src`
// unchecked.
expect(body).toContain("integer induction i");
expect(fast).not.toContain("bytes.index");
expect(body.slice(0, body.indexOf(fast))).toContain("bytes.index.invalid");
// A body that rebinds `src` keeps one checked loop.
const rebound = emit([
counted(
[scaled, { kind: "assign", localId: "src", value: ref("out", f64s), loc }],
length(ref("out", f64s)),
),
]);
expect(rebound).not.toContain("bytes.version.fast");
expect(rebound).toContain("bytes.index.invalid");
});
test("worker programs test each receiver's shared storage once in the guard", () => {
// for (let i = 0; i < n; i++) out[i] = src[i] * 2, where either view may
// be shared.
const loop = counted([
set(ref("out", f64s), ref("i"), bin("*", get(ref("src", f64s), ref("i")), num(2))),
]);
const kernel = fn([loop]);
const mod: IrModule = {
irVersion: 17,
sourceFile: loc.file,
entry: "__main",
workers: true,
functions: [
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
kernel,
],
};
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
const guard = body.slice(0, body.search(/^bytes\.version\.(fast|slow)\d+:$/m));
expect(guard.match(/icmp eq ptr %\S+, null/g)).toHaveLength(2);
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0];
expect(fast).not.toContain("bytes.shared");
expect(fast).not.toMatch(/(load|store) atomic double/);
// The checked copy keeps the shared-memory path for each access.
const slow = body.slice(body.search(/^bytes\.version\.slow\d+:$/m), body.indexOf(fast));
expect(slow).toContain("load atomic double");
expect(slow).toContain("store atomic double");
});
test("index temps of a version's counter are integers in the fast copy", () => {
// for (let i = 0; i < out.length; i++) { const j = i * 4; out[i] = src[j] + src[j + 1] }
const length: IrExpr = {
kind: "bytesIntrinsic",
method: "length",
receiver: ref("out", f64s),
args: [],
type: F64,
loc,
};
const loop = counted(
[
{ kind: "varDecl", localId: "at", init: bin("*", ref("i"), num(4)), loc },
set(
ref("out", f64s),
ref("i"),
bin(
"+",
get(ref("src", f64s), ref("at")),
get(ref("src", f64s), bin("+", ref("at"), num(1))),
),
),
],
length,
);
const mod: IrModule = {
irVersion: 17,
sourceFile: loc.file,
entry: "__main",
functions: [
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
fn([loop], [local("at", F64)]),
],
};
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0];
expect(body).toContain("alloca i64 ; integer at");
expect(fast).toMatch(/mul nsw i64 %\S+, 4/);
expect(fast).toMatch(/add nsw i64 %\S+, 1/);
expect(fast).not.toContain("fmul");
expect(fast).not.toContain("fptosi");
expect(fast).not.toContain("bytes.index");
});
test("Float16Array accesses version in elements and share the element tag", () => {
const f16s: IrType = { kind: "bytes", elem: "f16" };
const withHalves = (body: IrStmt[]): IrFunction => {
@@ -6,7 +6,8 @@ import { byteNumberAccess } from "../../ir/byte-numbers.js";
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
import { exactInteger, widenInteger } from "./integer-values.js";
import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js";
import { emitBytesHeaderLength } from "./expr-bytes.js";
import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js";
import { f64Lit } from "./common.js";
type Node = IrExpr | IrStmt;
type VarRef = IrExpr & { kind: "varRef" };
@@ -41,10 +42,18 @@ interface AffineCheck {
export interface BytesLoopVersion {
/** Typed-array accesses that are in bounds whenever the guard holds. */
bounds: ReadonlySet<Node>;
/** Worker programs: typed-array accesses whose views the guard proves
* thread-owned (no shared storage). */
owned: ReadonlySet<Node>;
/** Root receiver bindings, read once before the loop. */
receivers: { ref: VarRef; global: boolean }[];
/** Receivers the guard requires to have no shared storage. */
ownedReceivers: number[];
fixed: FixedCheck[];
affine: AffineCheck[];
/** Keys of affine terms the guard tests at run time: exact integers
* within GUARDED_TERM. */
guarded: ReadonlySet<string>;
/** The counted loop whose iteration space the affine checks cover. */
counted: IntegerCountedForLoop | null;
/** The guard also requires every counter value within [-bound, bound]. */
@@ -66,6 +75,11 @@ export interface BytesLoopFacts {
initialized: ReadonlySet<string>;
/** Accesses already proved in bounds without a guard. */
proven: ReadonlySet<Node>;
/** Worker programs: an access of a view that may be shared tests the
* view's shared storage first. */
workers: boolean;
/** Accesses already proved thread-owned by an enclosing version. */
owned: ReadonlySet<Node>;
}
const MAX_COST = 400;
@@ -73,6 +87,9 @@ const MAX_FIXED_END = 2 ** 32;
const MAX_CHECKS = 8;
const MAX_OFFSET = 2 ** 31;
const SAFE = Number.MAX_SAFE_INTEGER;
/** The range the guard requires of an invariant index term without a
* proven one: every index of a typed array of at most 2^32 elements. */
const GUARDED_TERM: IntegerRange = { min: -(2 ** 32), max: 2 ** 32 };
const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]);
/** Loop versioning for typed-array bounds. A typed array's length and data
@@ -85,7 +102,14 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"])
* `hi < length`, such as `table[x & 0xff]` or a constant-trip counter;
* - an index `k * counter + invariant + c` of a counted loop needs its first
* and last values in bounds, such as `out[base + i]` for `i < n` or
* `view.getUint32(i * 4)`.
* `view.getUint32(i * 4)`. An invariant without a proven integer range,
* such as `base` for `const base = r * cols`, must also be an exact
* integer between -2^32 and 2^32, which the guard tests.
*
* In worker programs, every access of a view that may be shared tests its
* shared storage. A view's storage is also fixed at construction, so the
* same guard tests each invariant receiver once, and the fast copy reads
* and writes thread-owned views directly.
*
* Only small synchronous loops are versioned (nested loops included), and
* the emitter bounds how deeply versions nest. The checked loop runs
@@ -183,9 +207,8 @@ export function matchBytesLoopVersion(
}
return invariant(e.localId) ? e : null;
};
// A pure integer operand evaluated again before the loop.
const invariantKey = (e: IrExpr): string | null => {
if (!facts.ranges.get(e)) return null;
// A pure loop-invariant number, evaluated again before the loop.
const pureKey = (e: IrExpr): string | null => {
if (e.kind === "numLit") return Number.isSafeInteger(e.value) ? `n${e.value}` : null;
if (e.kind === "varRef")
return invariant(e.localId) ? `v${e.localId.length}:${e.localId}` : null;
@@ -195,12 +218,27 @@ export function matchBytesLoopVersion(
e.left.type.kind === "f64" &&
e.right.type.kind === "f64"
) {
const left = invariantKey(e.left),
right = invariantKey(e.right);
const left = pureKey(e.left),
right = pureKey(e.right);
return left !== null && right !== null ? `(${e.op} ${left} ${right})` : null;
}
return null;
};
// Invariant terms without a proven range, such as `base` in
// `w[base + j]` with `const base = r * cols` for a parameter `cols`. The
// guard tests each one: an exact integer (not -0) within GUARDED_TERM.
const guardedRanges = new Map<IrExpr, IntegerRange>();
const guardedNodes = new Map<IrExpr, string>();
// The key of a loop-invariant index term, with a proven range or guarded.
const invariantKey = (e: IrExpr): string | null => {
const key = pureKey(e);
if (key === null || facts.ranges.get(e)) return key;
if (e.kind === "numLit") return null;
guardedRanges.set(e, GUARDED_TERM);
guardedNodes.set(e, key);
return key;
};
const termRange = (e: IrExpr): IntegerRange => facts.ranges.get(e) ?? guardedRanges.get(e)!;
interface Affine {
/** The counter's coefficient; 0 when the value is loop-invariant. */
counter: number;
@@ -208,9 +246,10 @@ export function matchBytesLoopVersion(
offset: number;
}
// Every operand of an affine index is `counter * k + Σ terms + offset`.
// Invariant terms need proven ranges. Operands that involve the counter
// need none: the guard bounds the counter so that each of them is a safe
// integer, which makes the index's double arithmetic exact.
// Invariant terms need proven ranges or the guard's test (guarded terms).
// Operands that involve the counter need none: the guard bounds the
// counter so that each of them is a safe integer, which makes the index's
// double arithmetic exact.
const shapes = new Map<IrExpr, Affine | null>();
const affine = (e: IrExpr): Affine | null => {
if (shapes.has(e)) return shapes.get(e)!;
@@ -294,7 +333,7 @@ export function matchBytesLoopVersion(
};
const termMagnitude = (terms: Term[]): number =>
terms.reduce((sum, t) => {
const r = facts.ranges.get(t.expr)!;
const r = termRange(t.expr);
return sum + Math.max(Math.abs(r.min), Math.abs(r.max));
}, 0);
@@ -314,10 +353,19 @@ export function matchBytesLoopVersion(
}
return index;
};
// Worker programs: accesses through invariant receivers, proven or not,
// become thread-owned when the guard finds no shared storage.
const owned = new Map<number, Node[]>();
const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => {
if (facts.proven.has(node)) return;
const ref = root(array);
if (!ref) return;
if (facts.workers && !facts.owned.has(node)) {
const receiver = receiverOf(ref);
const nodes = owned.get(receiver);
if (nodes) nodes.push(node);
else owned.set(receiver, [node]);
}
if (facts.proven.has(node)) return;
const shape = counter !== null ? affine(index) : null;
if (shape && shape.counter !== 0) {
const operands = new Set<IrExpr>();
@@ -383,9 +431,15 @@ export function matchBytesLoopVersion(
});
const fixedKept = [...fixed.values()].slice(0, MAX_CHECKS);
const affineKept = affineGroups.slice(0, MAX_CHECKS - fixedKept.length);
if (fixedKept.length === 0 && affineKept.length === 0) return null;
const ownedKept = [...owned.entries()].slice(0, MAX_CHECKS);
if (fixedKept.length === 0 && affineKept.length === 0 && ownedKept.length === 0) return null;
const bounds = new Set<Node>();
const ownedNodes = new Set<Node>();
const used = new Set<number>();
for (const [receiver, nodes] of ownedKept) {
used.add(receiver);
for (const node of nodes) ownedNodes.add(node);
}
const ranges = new Map<IrExpr, IntegerRange>();
const counterLow = Math.max(counted?.range.min ?? 0, -bound),
counterHigh = Math.min(counted?.range.max ?? 0, bound);
@@ -397,20 +451,32 @@ export function matchBytesLoopVersion(
used.add(group.check.receiver);
for (const node of group.nodes) bounds.add(node);
for (const operand of group.operands) {
if (facts.ranges.get(operand)) continue;
const shape = shapes.get(operand)!;
const a = shape.counter * counterLow,
b = shape.counter * counterHigh;
let min = Math.min(a, b) + shape.offset,
max = Math.max(a, b) + shape.offset;
for (const term of shape.terms) {
const r = facts.ranges.get(term.expr)!;
const r = termRange(term.expr);
min += term.sign > 0 ? r.min : -r.max;
max += term.sign > 0 ? r.max : -r.min;
}
if (counterLow <= counterHigh) ranges.set(operand, { min, max });
if (counterLow > counterHigh) continue;
// The guard's bound on the counter can narrow a range the operand
// already has (a loop to a length proves only `i < 2^53`), so that
// `i * 4` in the fast copy is exact integer arithmetic.
const known = facts.ranges.get(operand);
if (!known) ranges.set(operand, { min, max });
else if (min > known.min || max < known.max)
ranges.set(operand, { min: Math.max(min, known.min), max: Math.min(max, known.max) });
}
}
// Guarded terms of the kept checks, and the range the guard proves for
// every node of each in the fast copy.
const guarded = new Set<string>();
for (const group of affineKept)
for (const term of group.check.terms) if (guardedRanges.has(term.expr)) guarded.add(term.key);
for (const [node, key] of guardedNodes) if (guarded.has(key)) ranges.set(node, GUARDED_TERM);
// Renumber the receivers the kept checks use.
const order = [...used].sort((a, b) => a - b);
const renumber = new Map(order.map((old, index) => [old, index]));
@@ -420,7 +486,9 @@ export function matchBytesLoopVersion(
(counted.range.min < -bound || counted.range.max > bound);
return {
bounds,
owned: ownedNodes,
receivers: order.map((index) => receivers[index]!),
ownedReceivers: ownedKept.map(([receiver]) => renumber.get(receiver)!),
fixed: fixedKept.map((g) => ({
receiver: renumber.get(g.check.receiver)!,
end: g.check.end,
@@ -432,6 +500,7 @@ export function matchBytesLoopVersion(
minOffset: g.check.minOffset,
maxEnd: g.check.maxEnd,
})),
guarded,
counted: affineKept.length > 0 ? counted : null,
counterBound: needsBound ? bound : null,
ranges,
@@ -439,7 +508,8 @@ export function matchBytesLoopVersion(
}
/** Branches to `fast` when every access the version proves is in bounds for
* every iteration, and to `slow` otherwise (including a counted loop whose
* every iteration and every view it proves thread-owned has no shared
* storage, and to `slow` otherwise (including a counted loop whose
* integer form is not established, or a module receiver not yet set). */
export function emitBytesLoopVersionGuard(
host: LlvmEmitterContext,
@@ -473,6 +543,11 @@ export function emitBytesLoopVersionGuard(
return wide;
});
const conditions: string[] = [];
for (const index of version.ownedReceivers) {
const owned = B.tmp();
B.line(`${owned} = icmp eq ptr ${emitBytesSharedStorage(host, receivers[index]!)}, null`);
conditions.push(owned);
}
for (const check of version.fixed) {
const ok = B.tmp();
B.line(`${ok} = icmp ugt i64 ${lengths[check.receiver]}, ${check.end}`);
@@ -523,6 +598,14 @@ export function emitBytesLoopVersionGuard(
let base = "0";
for (const term of check.terms) {
const value = host.emitExpr(term.expr);
if (version.guarded.has(term.key)) {
const tested = emitGuardedTerm(host, value.name);
conditions.push(tested.ok);
const sum = B.tmp();
B.line(`${sum} = ${term.sign > 0 ? "add" : "sub"} i64 ${base}, ${tested.integer}`);
base = sum;
continue;
}
const integer = exactInteger(host, value, term.expr);
if (!integer) {
// Unreachable: every term has a proven range. Stay checked.
@@ -570,6 +653,34 @@ export function emitBytesLoopVersionGuard(
else B.condBr(valid, fast, slow);
}
/** Tests that a guarded term is an exact integer within GUARDED_TERM (not
* -0; NaN fails the ordered comparisons) and gives its i64. The integer is
* frozen, so it is a fixed value even when the test fails. */
function emitGuardedTerm(host: LlvmEmitterContext, value: string): { ok: string; integer: string } {
const B = host.B;
const low = B.tmp(),
high = B.tmp(),
inRange = B.tmp(),
raw = B.tmp(),
integer = B.tmp(),
back = B.tmp(),
bits = B.tmp(),
backBits = B.tmp(),
exact = B.tmp(),
ok = B.tmp();
B.line(`${low} = fcmp oge double ${value}, ${f64Lit(GUARDED_TERM.min)}`);
B.line(`${high} = fcmp ole double ${value}, ${f64Lit(GUARDED_TERM.max)}`);
B.line(`${inRange} = and i1 ${low}, ${high}`);
B.line(`${raw} = fptosi double ${value} to i64`);
B.line(`${integer} = freeze i64 ${raw}`);
B.line(`${back} = sitofp i64 ${integer} to double`);
B.line(`${bits} = bitcast double ${value} to i64`);
B.line(`${backBits} = bitcast double ${back} to i64`);
B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`);
B.line(`${ok} = and i1 ${inRange}, ${exact}`);
return { ok, integer };
}
function scaleCounter(host: LlvmEmitterContext, value: string, scale: number): string {
if (scale === 1) return value;
const scaled = host.B.tmp();
@@ -1,13 +1,15 @@
import { expect, test } from "vitest";
import { DYN, F64, STRING, VOID, type IrModule } from "../../ir/ir.js";
import {
assignCompactLayouts,
buildClassGraph,
classFieldIndex,
classIdLoadLines,
classMembershipIntervals,
emitClassMembershipHelper,
type LlClassMeta,
} from "./classes.js";
import { emitLlvmModule } from "./emitter.js";
import { emitLlvmModule, lowRegionTarget } from "./emitter.js";
import {
mangleClassNew,
mangleClassReleaseDirect,
@@ -196,11 +198,9 @@ test("classes whose trace visits nothing start acyclic in a traced hierarchy", (
});
test("ProgramLifetime classes pack their fields after a 32-bit header", () => {
const node = { kind: "object" as const, className: "Node" };
const leaf = { kind: "object" as const, className: "Leaf" };
const BOOL = { kind: "bool" as const };
const nodeFields = [
{ name: "parent", type: node },
{ name: "parent", type: STRING },
{ name: "flag", type: BOOL },
{ name: "weight", type: F64 },
];
@@ -214,7 +214,7 @@ test("ProgramLifetime classes pack their fields after a 32-bit header", () => {
{
name: "Leaf",
base: "Node",
fields: [...nodeFields, { name: "next", type: leaf }, { name: "mark", type: BOOL }],
fields: [...nodeFields, { name: "next", type: STRING }, { name: "mark", type: BOOL }],
methods: [],
loc,
},
@@ -233,6 +233,105 @@ test("ProgramLifetime classes pack their fields after a 32-bit header", () => {
expect(mortal).toContain(`%${mangleClassStruct("Leaf")} = type { i64, ptr, i8, i32, ptr, i8 }`);
});
test("references between ProgramLifetime instances are 32-bit indexes into their region", () => {
const node = { kind: "object" as const, className: "Node" };
const leaf = { kind: "object" as const, className: "Leaf" };
const root = { localId: "root", name: "root", type: leaf };
const nodeFields = [{ name: "parent", type: node }];
const mod: IrModule = {
irVersion: 17,
sourceFile: loc.file,
entry: "main",
functions: [
{
name: "main",
params: [root],
returnType: VOID,
locals: [{ id: "root", name: "root", type: leaf, mutable: false }],
body: [
{
kind: "fieldSet",
obj: { kind: "varRef", localId: "root", type: leaf, loc },
className: "Leaf",
field: "parent",
value: {
kind: "fieldGet",
obj: { kind: "varRef", localId: "root", type: leaf, loc },
className: "Leaf",
field: "next",
type: leaf,
loc,
},
loc,
},
],
loc,
},
],
classes: [
{ name: "Node", fields: nodeFields, methods: [], loc },
{
name: "Leaf",
base: "Node",
fields: [...nodeFields, { name: "next", type: leaf }, { name: "label", type: STRING }],
methods: [],
loc,
},
],
contracts: { programLifetime: ["Node"] },
};
// Linux executables without sanitizers (lowRegionTarget).
const llvm = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false, lowRegion: true });
// parent and next take 4 bytes each; the string keeps its pointer.
expect(llvm).toContain(`%${mangleClassStruct("Node")} = type { i32, i32 }`);
expect(llvm).toContain(`%${mangleClassStruct("Leaf")} = type { i32, i32, ptr, i32 }`);
// Instances come from the region, reserved below 32 GiB before compiled
// code runs: a bump of the thread's chunk, and the runtime when it is used
// up.
const nw =
new RegExp(`^define internal ptr @${mangleClassNew("Leaf")}\\([^]*?^}`, "m").exec(llvm)?.[0] ??
"";
expect(nw).toContain("%plr.end = getelementptr i8, ptr %plr.next");
expect(nw).toContain("store ptr %plr.end, ptr %plr.c");
expect(nw).toMatch(/%plr.o = call ptr @scr_plr_alloc\(i64 /);
expect(llvm).toMatch(/call void @scr_init\(\)\n {2}call void @scr_plr_init\(\)/);
// A reference is the instance's address over 8 (0 is NULL): a read
// shifts the loaded word, a write shifts the pointer.
const main = llvm.split(/^define /m).find((fn) => fn.includes("; main\n")) ?? "";
expect(main).toMatch(/= load i32, ptr %\S+, !tbaa/);
expect(main).toMatch(/= shl nuw nsw i64 %\S+, 3\n\s+%\S+ = inttoptr i64 %\S+ to ptr/);
expect(main).toMatch(/= ptrtoint ptr %\S+ to i64\n\s+%\S+ = lshr i64 %\S+, 3/);
expect(main).toMatch(/store i32 %\S+, ptr %\S+/);
expect(main).not.toContain("select");
expect(lowRegionTarget("linux", false)).toBe(true);
expect(lowRegionTarget("linux", true)).toBe(false);
expect(lowRegionTarget("darwin", false)).toBe(false);
expect(lowRegionTarget("win32", false)).toBe(false);
// Other targets, 32-bit targets and mortal classes keep pointers.
const elsewhere = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false });
expect(elsewhere).toContain(`%${mangleClassStruct("Leaf")} = type { i32, ptr, ptr, ptr }`);
expect(elsewhere).not.toContain("scr_plr");
const llvm32 = emitLlvmModule(mod, { pointerBits: 32, lowRegion: true });
expect(llvm32).not.toContain("scr_plr");
const mortal = emitLlvmModule({ ...mod, contracts: {} }, { pointerBits: 64, lowRegion: true });
expect(mortal).not.toContain("scr_plr");
// Field accessors that read the storage as a pointer are refused.
const metas = buildClassGraph(mod, new Map());
assignCompactLayouts(
metas,
() => true,
(cls) => cls.fields.map(() => "ptr"),
(cls, i, isCompact) => {
const t = cls.fields[i]!.type;
return t.kind === "object" && isCompact(t.className);
},
);
const leafMeta = metas.get("Leaf")!;
expect(() => classFieldIndex(leafMeta, "next")).toThrow(/holds a 32-bit reference/);
expect(classFieldIndex(leafMeta, "next", "compressed").compressed).toBe(true);
expect(classFieldIndex(leafMeta, "label").compressed).toBe(false);
});
test("a recycled block is cleared from the end of a ProgramLifetime instance's 32-bit header", () => {
const BOOL = { kind: "bool" as const };
const mod: IrModule = {
+134 -10
View File
@@ -98,6 +98,10 @@ export class LlClassMeta {
compactElems: { offset: number; size: number; align: number; type: string; field: number }[] = [];
/** Compact classes: each field's GEP index, by its index in def.fields. */
compactSlots: number[] = [];
/** Compact classes: whether each field (by its index in def.fields) holds
* a compact class instance as a 32-bit index (COMPACT_REF) rather than a
* pointer: compactRefLoadLines decodes it, compactRefStoreLines encodes it. */
compressed: boolean[] = [];
constructor(readonly def: IrClassDef) {
this.root = this;
@@ -395,13 +399,24 @@ export function classEnvironmentIndex(meta: LlClassMeta): number {
/** A field's GEP index inside its class struct: rc at 0, the vtable word
* at 1 on hierarchy members, then the flattened field list. */
export function classFieldIndex(meta: LlClassMeta, field: string): { index: number; type: IrType } {
export function classFieldIndex(
meta: LlClassMeta,
field: string,
refs: "pointer" | "compressed" = "pointer",
): { index: number; type: IrType; compressed: boolean } {
const idx = meta.def.fields.findIndex((f) => f.name === field);
if (idx < 0)
throw new InternalCompilerError(
`llvm emitter bug: unknown field ${field} on class ${meta.def.name}`,
);
return { index: classFieldSlot(meta, idx), type: meta.def.fields[idx]!.type };
const compressed = meta.compressed[idx] === true;
// Callers that read or write a field's storage as a pointer must not see
// a compressed one; callers that decode and encode pass "compressed".
if (compressed && refs === "pointer")
throw new InternalCompilerError(
`llvm emitter bug: field ${field} of ${meta.def.name} holds a 32-bit reference`,
);
return { index: classFieldSlot(meta, idx), type: meta.def.fields[idx]!.type, compressed };
}
/** The GEP index of the field at `idx` in `meta.def.fields`. */
@@ -590,6 +605,75 @@ export function compactHeaderWord(meta: LlClassMeta): number {
return COMPACT_HEADER_TAG | (meta.hierarchy ? meta.pre + 1 : 0);
}
/** Lines that decode the 32-bit reference `idx` (an i32) of a compressed
* field into the pointer `out`. The program-lifetime region lies below
* 32 GiB (LlvmTargetOptions.lowRegion), so a reference is the instance's
* address over 8 and 0 is NULL: one shift, which x86-64 folds into the
* address of the next load. `t` names the temporaries. */
export function compactRefLoadLines(idx: string, out: string, t: string): string[] {
return [
` ${t}.z = zext i32 ${idx} to i64`,
` ${t}.o = shl nuw nsw i64 ${t}.z, 3`,
` ${out} = inttoptr i64 ${t}.o to ptr`,
];
}
/** Lines that encode the pointer `ptr` (NULL or a compact class instance)
* as the 32-bit reference `out` (an i32) a compressed field stores. */
export function compactRefStoreLines(ptr: string, out: string, t: string): string[] {
return [
` ${t}.pi = ptrtoint ptr ${ptr} to i64`,
` ${t}.s = lshr i64 ${t}.pi, 3`,
` ${out} = trunc i64 ${t}.s to i32`,
];
}
/** The storage type of a compressed field (LlClassMeta.compressed): a
* 32-bit index into the program-lifetime region. */
export const COMPACT_REF = "i32";
/** Lines that allocate `%o`, `size` zeroed bytes in the program-lifetime
* region: a bump of the calling thread's chunk (scr_rc.h ScrPlrChunk,
* thread-local in worker programs), and scr_plr_alloc when the chunk is
* exhausted or the target has no inline allocation. The lines end in an
* open block. */
function plrAllocLines(host: ClassHost, size: string): string[] {
const S = host.sizeType;
host.declare(`declare ptr @scr_plr_alloc(${S})`);
host.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
if (host.inlineAlloc !== true) return [` %o = call ptr @scr_plr_alloc(${S} ${size})`];
const chunk: string[] = [];
if (host.threadLocalAlloc === true) {
host.declare(`@scr_plr_chunk = external thread_local global { ptr, ptr }`);
host.declare(`declare nonnull ptr @llvm.threadlocal.address.p0(ptr nonnull)`);
chunk.push(` %plr.c = call ptr @llvm.threadlocal.address.p0(ptr @scr_plr_chunk)`);
} else {
host.declare(`@scr_plr_chunk = external dso_local global { ptr, ptr }`);
chunk.push(` %plr.c = getelementptr inbounds i8, ptr @scr_plr_chunk, i64 0`);
}
// A thread without a chunk has NULL for both, so nothing fits.
return [
...chunk,
` %plr.lp = getelementptr inbounds i8, ptr %plr.c, i64 8`,
` %plr.next = load ptr, ptr %plr.c`,
` %plr.lim = load ptr, ptr %plr.lp`,
` %plr.n7 = add ${S} ${size}, 7`,
` %plr.n = and ${S} %plr.n7, -8`,
` %plr.end = getelementptr i8, ptr %plr.next, ${S} %plr.n`,
` %plr.fits = icmp ule ptr %plr.end, %plr.lim`,
` %plr.fitsx = call i1 @llvm.expect.i1(i1 %plr.fits, i1 true)`,
` br i1 %plr.fitsx, label %plr.bump, label %plr.slow`,
`plr.bump:`,
` store ptr %plr.end, ptr %plr.c`,
` br label %plr.done`,
`plr.slow:`,
` %plr.o = call ptr @scr_plr_alloc(${S} ${size})`,
` br label %plr.done`,
`plr.done:`,
` %o = phi ptr [ %plr.next, %plr.bump ], [ %plr.o, %plr.slow ]`,
];
}
/** Byte size and alignment of a class field's LLVM storage type. */
function fieldStorage(type: string): { size: number; align: number } {
switch (type) {
@@ -628,7 +712,16 @@ export function assignCompactLayouts(
metaMap: ReadonlyMap<string, LlClassMeta>,
compact: (root: LlClassMeta) => boolean,
fieldTypes: (cls: IrClassDef) => string[],
compressedRef?: (
cls: IrClassDef,
field: number,
isCompact: (className: string) => boolean,
) => boolean,
): void {
const isCompact = (className: string): boolean => {
const target = metaMap.get(className);
return target !== undefined && target.def.runtime !== true && compact(target.root);
};
const byPre = [...metaMap.values()].sort((a, b) => a.pre - b.pre);
for (const meta of byPre) {
if (meta.def.runtime === true || !compact(meta.root)) continue;
@@ -642,7 +735,22 @@ export function assignCompactLayouts(
`llvm emitter bug: compact class ${meta.def.name} does not extend its base's layout`,
);
const elems = inherited.map((e) => ({ ...e }));
const types = fieldTypes(meta.def);
// A reference to a compact class instance is a 32-bit index into the
// program-lifetime region (compactRefLoadLines); inherited fields keep the
// base's choice.
const compressed = meta.def.fields.map((_, i) =>
i < baseCount
? base!.compressed[i]!
: compressedRef !== undefined && compressedRef(meta.def, i, isCompact),
);
const types = fieldTypes(meta.def).map((t, i) => {
if (!compressed[i]) return t;
if (t !== "ptr")
throw new InternalCompilerError(
`llvm emitter bug: compressed field ${meta.def.fields[i]!.name} of ${meta.def.name} is stored as ${t}`,
);
return COMPACT_REF;
});
const own = types
.map((type, field) => ({ type, field, ...fieldStorage(type) }))
.slice(baseCount)
@@ -679,6 +787,7 @@ export function assignCompactLayouts(
meta.compact = true;
meta.compactElems = elems;
meta.compactSlots = slots;
meta.compressed = compressed;
}
}
@@ -705,7 +814,10 @@ function undefFieldInits(
if (f.name === "%cause") return;
// A nullable-pointer field's zeroed slot already is its unit arm.
if (nullable?.get(meta.def.name, f.name)) return;
const { index } = classFieldIndex(meta, f.name);
// Only union and dyn fields get a store here; a compressed field is a
// class reference, which starts NULL.
const { index, compressed } = classFieldIndex(meta, f.name, "compressed");
if (compressed) return;
if (f.type.kind === "union" && host.inlineUnions.classField(meta.def.name, f.name)) {
// An inline field starts at the undefined arm's tag with zero bits.
const tag = undefinedArmTag(f.type, host.unionsById);
@@ -748,6 +860,9 @@ export function emitClassShapes(
nullable?: NullableRefFields,
int32?: Int32Slots,
): { typeDefs: string[]; defs: string[] } {
// Whether compact instances live in the program-lifetime region (any
// field holds a 32-bit reference to one; emitter.ts assignCompactLayouts).
const regionRefs = [...metaMap.values()].some((m) => m.compressed.some(Boolean));
const typeDefs: string[] = [];
const defs: string[] = [];
const emitted = (mod.classes ?? []).filter((c) => !c.runtime);
@@ -897,13 +1012,17 @@ export function emitClassShapes(
const fieldIndex = (i: number): number => classFieldSlot(meta, i);
// Teardown, trace and gcFree see each slot's storage type: a nullable
// class field holds the instance pointer (NULL for the unit arm), and
// every class retain/release/visit entry point is NULL-tolerant.
// every class retain/release/visit entry point is NULL-tolerant. A
// compressed field refers to a compact instance, which nothing releases
// or traces, so they skip it.
const indexedFields = [
...ownedFields(cls).map(({ field, i }) => ({
name: field.name,
type: storedFieldType(cls, field),
index: fieldIndex(i),
})),
...ownedFields(cls)
.filter(({ i }) => meta.compressed[i] !== true)
.map(({ field, i }) => ({
name: field.name,
type: storedFieldType(cls, field),
index: fieldIndex(i),
})),
];
const refFields = indexedFields.filter((f) => isRefCounted(f.type));
const tracedFields = traced
@@ -1118,6 +1237,11 @@ export function emitClassShapes(
if (regions !== null) {
nw.push(...regions.newAlloc(cls.name, sizeOf));
defs.push(...regions.classDefinitions(cls, sizeOf));
} else if (meta.compact && regionRefs) {
// Compressed fields refer to compact instances by their index in the
// program-lifetime region, so every compact instance lives there:
// zeroed, 8-byte aligned and never freed.
nw.push(...plrAllocLines(host, sizeOf));
} else
nw.push(
...emitObjectAlloc(
@@ -314,16 +314,19 @@ test("virtual calls admit every same-named method", () => {
expect(new AmbientReceiverReaders(functions).ignores(caller)).toBe(false);
});
test("synthetic array HOF helpers are constant-callback consumers", () => {
const mod = fixture();
const consumer = mod.functions[1]!;
const entry = mod.functions[2]!;
delete consumer.ownsPrototype;
consumer.name = "%arr.some.0";
const call = entry.body[0]!;
if (call.kind === "exprStmt" && call.expr.kind === "call") call.expr.callee = "%arr.some.0";
expect(facts(mod).get("%arr.some.0")?.get("test")).toBe("predicate");
});
test.each(["%arr.some.0", "%bytes.some.0"])(
"synthetic array and typed-array HOF helpers are constant-callback consumers (%s)",
(name) => {
const mod = fixture();
const consumer = mod.functions[1]!;
const entry = mod.functions[2]!;
delete consumer.ownsPrototype;
consumer.name = name;
const call = entry.body[0]!;
if (call.kind === "exprStmt" && call.expr.kind === "call") call.expr.callee = name;
expect(facts(mod).get(name)?.get("test")).toBe("predicate");
},
);
function factory(name: string, returns: IrExpr[]): IrFunction {
return {
@@ -29,12 +29,16 @@ export function findConstantCallbacks(
mod.functions
.filter(
(fn) =>
// Authored declarations, plus the frontend's synthetic array
// HOF loops (`%arr.<method>.<n>`): those are plain direct-call
// functions, and a literal callback gets a helper of its own.
// Authored declarations, plus the frontend's synthetic array and
// typed-array HOF loops (`%arr.<method>.<n>`, `%bytes.<method>.<n>`):
// those are plain direct-call functions, and a literal callback
// gets a helper of its own.
// Generator declarations are entered through their spawn wrapper,
// which moves the same arguments into the generator's frame.
(fn.ownsPrototype || fn.name.startsWith("%arr.") || fn.generator !== undefined) &&
(fn.ownsPrototype ||
fn.name.startsWith("%arr.") ||
fn.name.startsWith("%bytes.") ||
fn.generator !== undefined) &&
(!fn.async || fn.generator !== undefined) &&
!fn.captures &&
!fn.classCaptures &&
@@ -1,4 +1,10 @@
import type { IntegerCountedForLoop } from "../../ir/integer-loops.js";
import type {
IntegerCountedForLoop,
IntegerLoopFollower,
IntegerPairedForLoop,
} from "../../ir/integer-loops.js";
import { FOLLOWER_GUARD_LIMIT } from "../../ir/integer-loops.js";
import type { IrExpr } from "../../ir/ir.js";
import type { LlvmEmitterContext } from "./expr-context.js";
import { f64Lit } from "./common.js";
@@ -59,37 +65,89 @@ export function emitCountedLoopSafety(
`${safe} = fcmp ${loop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(loop.guardLimit)}`,
);
}
if (loop.startGuarded) {
// An exact integer of at most 2^53 - 1 in magnitude, and not -0
// (the bit patterns of the value and its round trip agree).
// NaN and infinities fail the ordered range tests.
const start = host.emitExpr(loop.start);
const low = B.tmp(),
high = B.tmp(),
raw = B.tmp(),
wide = B.tmp(),
back = B.tmp(),
bits = B.tmp(),
backBits = B.tmp(),
exact = B.tmp(),
inRange = B.tmp(),
ok = B.tmp();
B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`);
B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`);
B.line(`${inRange} = and i1 ${low}, ${high}`);
B.line(`${raw} = fptosi double ${start.name} to i64`);
B.line(`${wide} = freeze i64 ${raw}`);
B.line(`${back} = sitofp i64 ${wide} to double`);
B.line(`${bits} = bitcast double ${start.name} to i64`);
B.line(`${backBits} = bitcast double ${back} to i64`);
B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`);
B.line(`${ok} = and i1 ${inRange}, ${exact}`);
if (safe === "true") safe = ok;
else {
const both = B.tmp();
B.line(`${both} = and i1 ${safe}, ${ok}`);
safe = both;
}
if (loop.startGuarded) safe = andSafe(host, safe, emitExactStart(host, loop.start));
for (const follower of loop.followers) {
if (follower.startGuarded) safe = andSafe(host, safe, emitExactStart(host, follower.start));
if (follower.tripGuarded) safe = andSafe(host, safe, emitFollowerReach(host, loop, follower));
}
return safe;
}
/** `|start| + |step| * span / |driver step| <= 2^52` for a follower, where
* `span` is the distance from the driver's start to the limit. The body runs
* at most `span / |driver step| + 1` times, so every value the follower holds
* on a body entry is within the bound. NaN and infinities fail the ordered
* test. */
function emitFollowerReach(
host: LlvmEmitterContext,
loop: IntegerCountedForLoop,
follower: IntegerLoopFollower,
): string {
const B = host.B;
const start = host.emitExpr(loop.start),
limit = host.emitExpr(loop.limit),
from = host.emitExpr(follower.start);
const span = B.tmp(),
reach = B.tmp(),
magnitude = B.tmp(),
total = B.tmp(),
ok = B.tmp();
B.line(
loop.step > 0
? `${span} = fsub double ${limit.name}, ${start.name}`
: `${span} = fsub double ${start.name}, ${limit.name}`,
);
B.line(
`${reach} = fmul double ${span}, ${f64Lit(Math.abs(follower.step) / Math.abs(loop.step))}`,
);
host.declare(`declare double @llvm.fabs.f64(double)`);
B.line(`${magnitude} = call double @llvm.fabs.f64(double ${from.name})`);
B.line(`${total} = fadd double ${magnitude}, ${reach}`);
B.line(`${ok} = fcmp ole double ${total}, ${f64Lit(FOLLOWER_GUARD_LIMIT)}`);
return ok;
}
/** The run-time test of a guarded paired loop: each unproven start is an
* exact safe integer. */
export function emitPairedLoopSafety(host: LlvmEmitterContext, loop: IntegerPairedForLoop): string {
let safe = "true";
for (const counter of [loop.low, loop.high])
if (counter.startGuarded) safe = andSafe(host, safe, emitExactStart(host, counter.start));
return safe;
}
function andSafe(host: LlvmEmitterContext, safe: string, ok: string): string {
if (safe === "true") return ok;
const both = host.B.tmp();
host.B.line(`${both} = and i1 ${safe}, ${ok}`);
return both;
}
/** An exact integer of at most 2^53 - 1 in magnitude, and not -0 (the bit
* patterns of the value and its round trip agree). NaN and infinities fail
* the ordered range tests. */
function emitExactStart(host: LlvmEmitterContext, expr: IrExpr): string {
const B = host.B;
const start = host.emitExpr(expr);
const low = B.tmp(),
high = B.tmp(),
raw = B.tmp(),
wide = B.tmp(),
back = B.tmp(),
bits = B.tmp(),
backBits = B.tmp(),
exact = B.tmp(),
inRange = B.tmp(),
ok = B.tmp();
B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`);
B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`);
B.line(`${inRange} = and i1 ${low}, ${high}`);
B.line(`${raw} = fptosi double ${start.name} to i64`);
B.line(`${wide} = freeze i64 ${raw}`);
B.line(`${back} = sitofp i64 ${wide} to double`);
B.line(`${bits} = bitcast double ${start.name} to i64`);
B.line(`${backBits} = bitcast double ${back} to i64`);
B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`);
B.line(`${ok} = and i1 ${inRange}, ${exact}`);
return ok;
}
@@ -1,5 +1,6 @@
import { expect, test } from "vitest";
import { BOOL, F64, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js";
import type { IntegerCountedForLoop } from "../../ir/integer-loops.js";
import {
countedDenseShape,
matchDenseArrayLoop,
@@ -111,16 +112,24 @@ test("loops that may resize, rebind or move storage keep the ordinary copy only"
expect([...rebound!.arrays.keys()]).toEqual(["a"]);
});
const counted = (
fields: Pick<IntegerCountedForLoop, "start" | "limit" | "step" | "inclusive" | "range">,
): IntegerCountedForLoop => ({
localId: "i",
guarded: false,
limitGuarded: false,
startGuarded: false,
guardLimit: 0,
followers: [],
...fields,
});
test("counted shapes need a non-negative counter strictly below the length", () => {
const ascending = (limit: IrExpr, inclusive: boolean, min = 0) =>
countedDenseShape(loop([]), {
localId: "i",
start: num(min),
limit,
step: 1,
inclusive,
range: { min, max: 100 },
});
countedDenseShape(
loop([]),
counted({ start: num(min), limit, step: 1, inclusive, range: { min, max: 100 } }),
);
const minus = (offset: number): IrExpr => ({
kind: "bin",
op: "-",
@@ -129,19 +138,58 @@ test("counted shapes need a non-negative counter strictly below the length", ()
type: F64,
loc,
});
expect(ascending(method("a", "length"), false)?.length.localId).toBe("a");
expect(ascending(minus(1), true)?.length.localId).toBe("a");
expect(ascending(method("a", "length"), false)?.length?.localId).toBe("a");
expect(ascending(minus(1), true)?.length?.localId).toBe("a");
expect(ascending(method("a", "length"), true)).toBeNull();
expect(ascending(minus(1), false, -1)).toBeNull();
// Another invariant limit bounds the counter itself; the entry test
// compares it with every indexed array's hole-free prefix.
const limited = ascending(ref("n"), false);
expect(limited?.length).toBeNull();
expect(limited?.limit?.limit).toEqual(ref("n"));
expect(ascending(ref("n"), false, -1)).toBeNull();
const descending = (offset: number) =>
countedDenseShape(loop([]), {
localId: "i",
start: minus(offset),
limit: num(0),
step: -1,
inclusive: true,
range: { min: 0, max: 100 },
});
expect(descending(1)?.length.localId).toBe("a");
countedDenseShape(
loop([]),
counted({
start: minus(offset),
limit: num(0),
step: -1,
inclusive: true,
range: { min: 0, max: 100 },
}),
);
expect(descending(1)?.length?.localId).toBe("a");
expect(descending(0)).toBeNull();
});
test("a loop to another invariant limit covers each array whose length it keeps", () => {
const limited: DenseLoopShape = {
counter: "i",
length: null,
limit: counted({
start: num(0),
limit: ref("n"),
step: 1,
inclusive: false,
range: { min: 0, max: 100 },
}),
range: { min: 0, max: 100 },
};
const matchLimited = (body: IrStmt[], unaliased = new Set<string>()) =>
matchDenseArrayLoop(loop(body), limited, locals, unaliased, new Set(["a", "b"]));
const copy = write("b", read("a"));
const dense = matchLimited([copy]);
expect([...dense!.arrays.keys()].sort()).toEqual(["a", "b"]);
expect(dense!.length).toBeNull();
expect(dense!.limit?.limit).toEqual(ref("n"));
// An array the loop may resize is not covered; neither is one it may
// share storage with.
const resizing = [
{ kind: "exprStmt" as const, expr: read("a"), loc },
{ kind: "exprStmt" as const, expr: method("b", "getNumber", [ref("i")]), loc },
{ kind: "exprStmt" as const, expr: method("b", "push", [num(1)]), loc },
];
expect([...matchLimited(resizing, new Set(["b"]))!.arrays.keys()]).toEqual(["a"]);
expect(matchLimited(resizing)).toBeNull();
});
@@ -15,22 +15,33 @@
* test bounds `a.length` by 2^31 - 1, so the fast copy's counter is an int32.
* Arrays longer than that are sparse in practice (a dense one would need
* 16 GB of slots) and take the ordinary copy, as does every array with a
* hole, present undefined or sparse storage below `a.length`. */
* hole, present undefined or sparse storage below `a.length`.
*
* A counted loop with another invariant limit, such as `j < n` after
* `const n = a.length` or `j < 5`, is covered the same way: the test bounds
* the loop's exclusive limit by 2^31 - 1 and by each indexed array's
* `filled`, and the loop must provably never change any of their lengths. */
import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js";
import type { IntegerRange } from "../../ir/integer-ranges.js";
import { preservesArrayLength } from "../../ir/array-length-stability.js";
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
import type { IntegerCountedForLoop } from "../../ir/integer-loops.js";
import type { LlvmEmitterContext } from "./expr-context.js";
import { loadArrCount, SCR_ARR_FIELD } from "./common.js";
import { emitCountedLoopLimit } from "./counted-loops.js";
export const INT32_MAX = 2147483647;
export interface DenseArrayLoop {
/** The loop's counter binding. */
counter: string;
/** The array whose length bounds the counter. */
length: IrExpr & { kind: "varRef" };
/** The array whose length bounds the counter, or null for a counted loop
* bounded by `limit`. */
length: (IrExpr & { kind: "varRef" }) | null;
/** The ascending counted loop whose invariant limit bounds the counter,
* when `length` is null. */
limit: IntegerCountedForLoop | null;
/** Arrays whose accesses at the counter are proven to be inside the
* hole-free prefix in the fast copy, with a read of each binding. */
arrays: ReadonlyMap<string, IrExpr & { kind: "varRef" }>;
@@ -39,12 +50,15 @@ export interface DenseArrayLoop {
}
/** The fast copy duplicates the loop; keep the duplicated code small. */
const MAX_LOOP_NODES = 120;
const MAX_LOOP_NODES = 160;
/** How a matched integer loop bounds its counter below `length.length`. */
export interface DenseLoopShape {
counter: string;
length: IrExpr & { kind: "varRef" };
/** The array whose length bounds the counter, or null for `limit`. */
length: (IrExpr & { kind: "varRef" }) | null;
/** An ascending counted loop bounded by another invariant limit. */
limit?: IntegerCountedForLoop;
/** The counter's proven range on every pass of the ordinary loop; the
* fast copy keeps its minimum, which must be at least 0. */
range: IntegerRange;
@@ -73,22 +87,18 @@ function lengthOffset(e: IrExpr): { receiver: IrExpr & { kind: "varRef" }; offse
/** The array bounding a counted loop's counter below its length: an
* ascending loop compared below `a.length - c`, or a descending one starting
* at `a.length - c` (c >= 1). */
* at `a.length - c` (c >= 1). Another ascending loop is bounded by its own
* invariant limit. */
export function countedDenseShape(
loop: IrStmt & { kind: "for" },
counted: {
localId: string;
start: IrExpr;
limit: IrExpr;
step: number;
inclusive: boolean;
range: IntegerRange;
},
counted: IntegerCountedForLoop,
): DenseLoopShape | null {
if (counted.range.min < 0) return null;
if (counted.step > 0) {
const bound = lengthOffset(counted.limit);
if (!bound || bound.offset < (counted.inclusive ? 1 : 0)) return null;
if (!bound)
return { counter: counted.localId, length: null, limit: counted, range: counted.range };
if (bound.offset < (counted.inclusive ? 1 : 0)) return null;
return { counter: counted.localId, length: bound.receiver, range: counted.range };
}
// The start is evaluated once, before the loop; the counter only falls.
@@ -143,9 +153,11 @@ export function matchDenseArrayLoop(
return everyStmtChild(s, expr, stmt);
};
if (!loop.body.every(stmt) || !expr(loop.cond) || nested) return null;
const nodesOfLoop = [loop.cond, ...loop.body];
if (
length !== null &&
!preservesArrayLength(
[loop.cond, ...loop.body],
nodesOfLoop,
{ receiver: length.localId, inRange: counter },
locals,
unaliased,
@@ -161,7 +173,14 @@ export function matchDenseArrayLoop(
local.boxed ||
local.tdz ||
disturbed.has(id) ||
(id !== length.localId && !initialized.has(id))
(id !== length?.localId && !initialized.has(id))
)
continue;
// Without a bounding array, the entry test reads each array's `filled`
// once: the loop must not change any of their lengths.
if (
length === null &&
!preservesArrayLength(nodesOfLoop, { receiver: id, inRange: counter }, locals, unaliased)
)
continue;
arrays.set(id, ref);
@@ -170,13 +189,15 @@ export function matchDenseArrayLoop(
return {
counter,
length,
limit: length === null ? (shape.limit ?? null) : null,
arrays,
range: { min: shape.range.min, max: Math.min(shape.range.max, INT32_MAX - 1) },
};
}
/** The fast copy's entry test: the bounding length is at most 2^31 - 1 and
* at most every indexed array's hole-free prefix bound. */
/** The fast copy's entry test: the bounding length (or the counted loop's
* exclusive limit) is at most 2^31 - 1 and at most every indexed array's
* hole-free prefix bound. */
export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoop): string {
const B = host.B;
const size = host.sizeType;
@@ -186,7 +207,29 @@ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoo
host.markMemoryPointer(ptr, "array:header");
return ptr;
};
const receiver = host.emitStableReceiver(loop.length, []);
if (loop.length === null) {
// A limit at or below the start runs no pass, and passes the test.
const limit = emitCountedLoopLimit(host, loop.limit!);
const small = B.tmp();
B.line(`${small} = icmp sle i64 ${limit}, ${INT32_MAX}`);
let valid = small;
for (const ref of loop.arrays.values()) {
const array = host.emitStableReceiver(ref, []);
const filledPtr = header(array.name, SCR_ARR_FIELD.filled),
filled = B.tmp(),
wide = B.tmp(),
covered = B.tmp(),
both = B.tmp();
B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
B.line(`${wide} = zext i32 ${filled} to i64`);
B.line(`${covered} = icmp sle i64 ${limit}, ${wide}`);
B.line(`${both} = and i1 ${valid}, ${covered}`);
valid = both;
}
return valid;
}
const length = loop.length;
const receiver = host.emitStableReceiver(length, []);
const lenPtr = header(receiver.name, SCR_ARR_FIELD.len),
len = B.tmp(),
small = B.tmp();
@@ -194,7 +237,7 @@ export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoo
B.line(`${small} = icmp ule ${size} ${len}, ${INT32_MAX}`);
let valid = small;
for (const [id, ref] of loop.arrays) {
const array = id === loop.length.localId ? receiver : host.emitStableReceiver(ref, []);
const array = id === length.localId ? receiver : host.emitStableReceiver(ref, []);
const filledPtr = header(array.name, SCR_ARR_FIELD.filled),
filled = B.tmp(),
covered = B.tmp(),
+302
View File
@@ -108,6 +108,15 @@ const DYN_HANDLE_UNBOX_FN: Record<string, string> = {
dgramSocket: "scr_dgram_from_dyn",
};
/** ScrMessageKind (scr_message.h): the node kinds message readers test. */
const MESSAGE_KIND = {
UNDEFINED: 0,
NULL: 1,
BOOL: 2,
NUMBER: 3,
STRING: 4,
} as const;
export const DYN_KIND = {
NULL: 0,
BOOL: 1,
@@ -173,6 +182,8 @@ export class LlDyn {
private readonly dynFuncThunks = new Map<string, string>();
private readonly dynFuncBoxes = new Map<string, string>();
private readonly dynFuncAdapters = new Map<string, string>();
private readonly messageListeners = new Map<string, string | null>();
private readonly messageReaders = new Map<string, string>();
private readonly promiseDynAdapters = new Map<string, string>();
private readonly mapDynOps = new Map<string, string>();
private dynToStrFn: string | null = null;
@@ -4610,6 +4621,297 @@ export class LlDyn {
return name;
}
/** `sc_dml_<n>(ptr c, ptr message) -> i1`, the typed message entry of a
* closure registered as a 'message' listener (ScrDynMessageListener):
* builds the closure's parameter from the message's nodes and calls the
* closure. It answers false, without calling it, when the message does
* not have the parameter's exact layout, and the listener then runs
* through its thunk on the decoded message, as before. Null when the
* signature's parameter has no message reader (messageReadable). */
messageListenerHelper(t: IrType & { kind: "func" }): string | null {
const key = typeKey(t);
const existing = this.messageListeners.get(key);
if (existing !== undefined) return existing;
const param = t.params[0];
const supported =
t.params.length === 1 &&
t.rest !== true &&
param !== undefined &&
(t.ret.kind === "void" || t.ret.kind === "f64" || t.ret.kind === "bool") &&
this.messageReadable(param, new Set());
if (!supported) {
this.messageListeners.set(key, null);
return null;
}
const name = `sc_dml_${this.messageListeners.size}`;
this.messageListeners.set(key, name);
const B = new BlockBuilder();
B.entryAllocas.push(`%fail = alloca i1`);
B.line(`store i1 false, ptr %fail`);
const value = B.tmp();
B.line(
`${value} = call ${this.valTy(param)} @${this.messageReaderHelper(param)}(ptr %m, ${this.host.sizeType} 0, ptr %fail)`,
);
const failed = B.tmp();
B.line(`${failed} = load i1, ptr %fail`);
const lMiss = B.newLabel("dml.miss");
const lCall = B.newLabel("dml.call");
B.condBr(failed, lMiss, lCall);
B.startBlock(lMiss);
B.terminate(`ret i1 false`);
B.startBlock(lCall);
// The closure consumes its parameter; a pending exception stays for
// the emitter, as after the thunk.
const fnp = B.tmp();
const fn = B.tmp();
B.line(`${fnp} = getelementptr inbounds %ScrClosure, ptr %c, i64 0, i32 1`);
B.line(`${fn} = load ptr, ptr ${fnp}`);
const args = `ptr %c, ${this.valTy(param)} ${value}`;
if (t.ret.kind === "void") B.line(`call void ${fn}(${args})`);
else B.line(`${B.tmp()} = call ${this.valTy(t.ret)} ${fn}(${args})`);
B.terminate(`ret i1 true`);
this.defs.push(
`define internal zeroext i1 @${name}(ptr %c, ptr %m) ${FN_ATTRS} { ; message entry for ${llvmCommentText(key)}`,
B.render(),
`}`,
``,
);
return name;
}
/** Whether a message reader can build T: numbers, booleans and strings;
* records of them with only declared fields, none of union type; and
* boxed unions of those arms and null or undefined without a literal
* discriminator, with at most one number, boolean and string arm. */
private messageReadable(t: IrType, seen: Set<string>): boolean {
switch (t.kind) {
case "f64":
case "bool":
case "string":
return true;
case "record": {
const shape = this.host.recordsById.get(t.shapeId);
if (!shape || shape.tuple || shape.indexValue || shape.fields.length === 0) return false;
if (seen.has(t.shapeId) || this.isRecursiveShape(t)) return false;
seen.add(t.shapeId);
const readable = shape.fields.every(
(f) =>
!f.name.startsWith("%") &&
f.type.kind !== "union" &&
llFieldType(f.type, this.host.inlineUnions.record(f.type)) !== INLINE_UNION_FIELD &&
this.messageReadable(f.type, seen),
);
seen.delete(t.shapeId);
return readable;
}
case "union": {
const def = this.host.unionsById.get(t.unionId);
// A discriminated union's checked conversion also tests the arm's
// literal discriminator, which the readers do not.
if (!def || def.discriminant || this.host.nullableUnions.has(t.unionId)) return false;
const scalars = new Set<string>();
for (const arm of def.arms) {
if (arm.kind === "nullT" || arm.kind === "undefinedT") continue;
if (arm.kind === "f64" || arm.kind === "bool" || arm.kind === "string") {
if (scalars.has(arm.kind)) return false;
scalars.add(arm.kind);
continue;
}
if (arm.kind !== "record" || !this.messageReadable(arm, seen)) return false;
}
return true;
}
default:
return false;
}
}
/** `sc_dmr_<n>(ptr message, size node, ptr fail) -> T`: T (+1) built
* from a message node, for a type messageReadable accepts. Sets *fail
* and returns a dummy when the node does not have T's layout: the arm
* the checked conversion would choose, a record's declared keys in
* declaration order and no others. */
private messageReaderHelper(t: IrType): string {
const key = typeKey(t);
const existing = this.messageReaders.get(key);
if (existing) return existing;
const name = `sc_dmr_${this.messageReaders.size}`;
this.messageReaders.set(key, name);
const host = this.host;
const S = host.sizeType;
if (t.kind !== "record") host.declare(`declare i32 @scr_message_kind(ptr, ${S})`);
const retTy = this.valTy(t);
const dummy = retTy === "double" ? f64Lit(0) : retTy === "i1" ? "false" : "null";
const B = new BlockBuilder();
const lFail = B.newLabel("dmr.fail");
// scr_message_fields tests a record's kind itself.
const kind = B.tmp();
if (t.kind !== "record") B.line(`${kind} = call i32 @scr_message_kind(ptr %m, ${S} %n)`);
const requireKind = (want: number): void => {
const ok = B.tmp();
B.line(`${ok} = icmp eq i32 ${kind}, ${want}`);
const lOk = B.newLabel("dmr.kind");
B.condBr(ok, lOk, lFail);
B.startBlock(lOk);
};
switch (t.kind) {
case "f64":
case "bool": {
host.declare(`declare double @scr_message_number(ptr, ${S})`);
requireKind(t.kind === "f64" ? MESSAGE_KIND.NUMBER : MESSAGE_KIND.BOOL);
const n = B.tmp();
B.line(`${n} = call double @scr_message_number(ptr %m, ${S} %n)`);
if (t.kind === "f64") B.terminate(`ret double ${n}`);
else {
const b = B.tmp();
B.line(`${b} = fcmp une double ${n}, ${f64Lit(0)}`);
B.terminate(`ret i1 ${b}`);
}
break;
}
case "string": {
host.declare(`declare ptr @scr_message_string(ptr, ${S})`);
requireKind(MESSAGE_KIND.STRING);
const str = B.tmp();
B.line(`${str} = call ptr @scr_message_string(ptr %m, ${S} %n)`);
B.terminate(`ret ptr ${str}`);
break;
}
case "record": {
const shape = host.recordsById.get(t.shapeId)!;
const order = recordSourceFieldOrder(shape);
const fields = new Map(shape.fields.map((f, i) => [f.name, { field: f, index: i }]));
const base = host.nullableUnions.fieldBase(t.shapeId);
const struct = mangleRecordStruct(t.shapeId);
const names = this.recordNameTable(t.shapeId, shape);
host.declare(`declare zeroext i1 @scr_message_fields(ptr, ${S}, ptr, ptr, ${S}, ptr)`);
B.entryAllocas.push(`%kids = alloca [${order.length} x ${S}]`);
const matched = B.tmp();
B.line(
`${matched} = call zeroext i1 @scr_message_fields(ptr %m, ${S} %n, ptr ${names.names}, ptr ${names.lens}, ${S} ${order.length}, ptr %kids)`,
);
const lBuild = B.newLabel("dmr.build");
B.condBr(matched, lBuild, lFail);
B.startBlock(lBuild);
B.line(`%r0 = call ptr @${mangleRecordNew(t.shapeId)}()`);
order.forEach((fieldName, i) => {
const { field, index } = fields.get(fieldName)!;
const kidp = B.tmp();
const kid = B.tmp();
B.line(
`${kidp} = getelementptr inbounds [${order.length} x ${S}], ptr %kids, i64 0, i64 ${i}`,
);
B.line(`${kid} = load ${S}, ptr ${kidp}`);
const v = B.tmp();
B.line(
`${v} = call ${this.valTy(field.type)} @${this.messageReaderHelper(field.type)}(ptr %m, ${S} ${kid}, ptr %fail)`,
);
const failed = B.tmp();
B.line(`${failed} = load i1, ptr %fail`);
const lDrop = B.newLabel("dmr.drop");
const lStore = B.newLabel("dmr.store");
B.condBr(failed, lDrop, lStore);
B.startBlock(lDrop);
B.line(`call void ${releaseSym(host, t)}(ptr %r0)`);
B.terminate(`ret ptr null`);
B.startBlock(lStore);
const p = B.tmp();
B.line(
`${p} = getelementptr inbounds %${struct}, ptr %r0, i64 0, i32 ${index + base} ; .${llvmCommentText(fieldName)}`,
);
const fieldTy = llFieldType(field.type, host.inlineUnions.record(field.type));
if (fieldTy === "i8") {
const z = B.tmp();
B.line(`${z} = zext i1 ${v} to i8`);
B.line(`store i8 ${z}, ptr ${p}`);
} else {
B.line(`store ${fieldTy} ${v}, ptr ${p}`);
}
});
B.terminate(`ret ptr %r0`);
break;
}
case "union": {
const def = host.unionsById.get(t.unionId)!;
def.arms.forEach((arm, i) => {
const lNext = B.newLabel("dmr.next");
if (arm.kind === "nullT" || arm.kind === "undefinedT") {
const hit = B.tmp();
B.line(
`${hit} = icmp eq i32 ${kind}, ${arm.kind === "nullT" ? MESSAGE_KIND.NULL : MESSAGE_KIND.UNDEFINED}`,
);
const lHit = B.newLabel("dmr.unit");
B.condBr(hit, lHit, lNext);
B.startBlock(lHit);
B.terminate(`ret ptr ${host.unitInstanceRef(t.unionId, i)}`);
B.startBlock(lNext);
return;
}
if (arm.kind !== "record") {
const want =
arm.kind === "f64"
? MESSAGE_KIND.NUMBER
: arm.kind === "bool"
? MESSAGE_KIND.BOOL
: MESSAGE_KIND.STRING;
const hit = B.tmp();
B.line(`${hit} = icmp eq i32 ${kind}, ${want}`);
const lHit = B.newLabel("dmr.arm");
B.condBr(hit, lHit, lNext);
B.startBlock(lHit);
}
// A record arm that does not match leaves *fail clear for the
// next arm, like the checked conversion's next-arm match.
const v = B.tmp();
B.line(
`${v} = call ${this.valTy(arm)} @${this.messageReaderHelper(arm)}(ptr %m, ${S} %n, ptr %fail)`,
);
if (arm.kind === "record") {
const failed = B.tmp();
B.line(`${failed} = load i1, ptr %fail`);
const lHit = B.newLabel("dmr.arm");
const lMiss = B.newLabel("dmr.miss");
B.condBr(failed, lMiss, lHit);
B.startBlock(lMiss);
B.line(`store i1 false, ptr %fail`);
B.br(lNext);
B.startBlock(lHit);
}
const u = B.tmp();
if (arm.kind === "f64") {
host.declare(`declare ptr @scr_union_new_f64(i32, double)`);
B.line(`${u} = call ptr @scr_union_new_f64(i32 ${i}, double ${v})`);
} else if (arm.kind === "bool") {
host.declare(`declare ptr @scr_union_new_bool(i32, i1 zeroext)`);
B.line(`${u} = call ptr @scr_union_new_bool(i32 ${i}, i1 ${v})`);
} else {
const rc = vAdapters(host, arm);
host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
B.line(
`${u} = call ptr @scr_union_new_ref(i32 ${i}, ptr ${v}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, arm)})`,
);
}
B.terminate(`ret ptr ${u}`);
B.startBlock(lNext);
});
B.br(lFail);
break;
}
default:
throw new InternalCompilerError(`llvm emitter bug: message reader of ${key}`);
}
B.startBlock(lFail);
B.line(`store i1 true, ptr %fail`);
B.terminate(`ret ${retTy} ${dummy}`);
this.defs.push(
`define internal ${retTy === "i1" ? "zeroext i1" : retTy} @${name}(ptr %m, ${S} %n, ptr %fail) ${FN_ATTRS} { ; message reader for ${llvmCommentText(key)}`,
B.render(),
`}`,
``,
);
return name;
}
/** The adapter closure body for one TARGET signature: caps[0] is an
* untraced obj-box owning the dyn function value. */
dynFuncAdapterHelper(t: IrType & { kind: "func" }): string {
@@ -35,6 +35,7 @@ import {
mangleArgPack,
mangleAsyncAdoptCall,
mangleAsyncAwaitCall,
mangleAsyncDirectSpawn,
mangleAsyncSpawn,
mangleBorrowedFunction,
mangleClassStruct,
@@ -82,6 +83,8 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
}
emitter.currentConstantCallbacks =
emitter.debug === null ? (emitter.constantCallbacks.get(fn.name) ?? new Map()) : new Map();
emitter.knownCallbacks.clear();
emitter.callbackVersionsEnabled = emitter.debug === null && !fn.async && !fn.generator;
// Preserve concrete source bindings for debugger inspection. Suspended
// functions keep the established array lifetime across continuations.
emitter.streamingSplitsEnabled = emitter.debug === null && !fn.async && !fn.generator;
@@ -127,6 +130,8 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
emitter.stableCaptureIds =
fn.captures !== undefined ? emitter.lazyCaptures.stableCaptures(fn.name) : new Set();
emitter.integerLoopBindings.clear();
emitter.integerTemps.clear();
emitter.integerTempsEnabled = emitter.debug === null && !fn.async && !fn.generator;
emitter.countedLoopsEnabled = emitter.debug === null && !fn.async && !fn.generator;
emitter.speculativeLoopsEnabled = emitter.countedLoopsEnabled;
emitter.speculativeFunction = fn.name;
@@ -153,6 +158,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
: new Map();
emitter.integerViews.clear();
emitter.fieldPointerTags.clear();
emitter.compressedFieldPtrs.clear();
emitter.integerArrayBindings.clear();
emitter.loopArrayBorrows =
emitter.debug === null
@@ -226,6 +232,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
emitter.integerLoopRanges = rangeFacts.loopRanges;
emitter.int32Elements = slotFacts.elements ?? null;
emitter.bytesBounds = findBytesBounds(numericFn, emitter.integerRanges);
emitter.bytesOwned = new Set();
// Debug builds keep every number local a double for the debugger's
// variable view. Lazily boxed captured parameters keep their double,
// which the box is created from, and so do plain copies of a capture or
@@ -910,6 +917,20 @@ export function emitAsyncScaffolding(emitter: LlEmitter): string[] {
for (const line of emitter.emitAsyncAwaitCall(fn, argPackLines, true)) out.push(line);
}
const stackless = emitter.stacklessAsync.has(fn.name);
if (stackless && emitter.directAwaitCallees.has(fn.name)) {
// A coroutine's direct await: the same prefix, with the promise kept
// inside the returned fiber record.
emitter.declare(`declare ptr @scr_async_spawn_direct(ptr, ptr)`);
out.push(
`define internal ptr @${mangleAsyncDirectSpawn(fn.name)}(${spawnParams.join(", ")}) ${FN_ATTRS} { ; direct spawn ${fn.name}`,
`entry:`,
...argPackLines,
` %f = call ptr @scr_async_spawn_direct(ptr @${mangleTrampoline(fn.name)}, ptr %ap)`,
` ret ptr %f`,
`}`,
``,
);
}
const spawn = stackless ? "scr_async_spawn_stackless" : "scr_async_spawn";
emitter.declare(`declare ptr @scr_fiber_promise(ptr)`);
emitter.declare(`declare ptr @${spawn}(ptr, ptr)`);
@@ -556,6 +556,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
``,
`@scr_error_vts = external ${tl}global [${RUNTIME_ERROR_CLASSES.size} x %ScrVt]`,
`declare void @scr_init()`,
...(emitter.usesCompactRefs ? [`declare void @scr_plr_init()`] : []),
`declare void @scr_lib_init(i32, ptr)`,
...(emitter.mod.workers ? [`declare void @scr_runtime_workers_v8()`] : []),
...(emitter.stackChecks?.size ? [`declare void @scr_stack_guard_init()`] : []),
@@ -848,6 +849,9 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
...(emitter.runtimeAbiMarker ? [` call void @${RUNTIME_ABI_MARKER}()`] : []),
...(emitter.mod.workers ? [` call void @scr_runtime_workers_v8()`] : []),
` call void @scr_init()`,
// Compressed fields hold addresses in the program-lifetime region, which
// is reserved below 32 GiB before compiled code runs.
...(emitter.usesCompactRefs ? [` call void @scr_plr_init()`] : []),
// Each context (the main thread, every worker) installs its stack
// guard before running compiled code that checks it.
...(emitter.stackChecks?.size ? [` call void @scr_stack_guard_init()`] : []),
@@ -42,14 +42,21 @@ import {
matchIntegerArrayForLoop,
matchIntegerBytesForLoop,
matchIntegerCountedForLoop,
matchIntegerPairedForLoop,
} from "../../ir/integer-loops.js";
import { emitByteWindowGuard, matchByteWindow } from "./byte-windows.js";
import { countedDenseShape, emitDenseLoopGuard, matchDenseArrayLoop } from "./dense-array-loops.js";
import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js";
import {
emitCountedLoopLimit,
emitCountedLoopSafety,
emitPairedLoopSafety,
} from "./counted-loops.js";
import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js";
import { mangleLocal } from "../mangle.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlValue } from "./expr-context.js";
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
import { widenInteger } from "./integer-values.js";
import { SCALAR_UNION_RESULT } from "./int32-abi.js";
import { emitLiteralSwitch } from "./switch-dispatch.js";
import {
@@ -70,6 +77,9 @@ import {
} from "./string-construction.js";
import { emitStringInputs } from "./string-lifetimes.js";
/** A callback loop version copies the loop; keep the copies small. */
const MAX_CALLBACK_VERSION_COST = 400;
export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
const B = emitter.B;
emitter.frames.push([]);
@@ -267,6 +277,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
else emitter.toFrame(v);
B.line(`store ${emitter.llType(b.type)} ${v.name}, ptr ${b.slot}`);
emitter.storeIntegerView(s.localId, v, s.init);
storeIntegerTemp(emitter, s.localId, b, v);
if (isRefCounted(b.type)) {
emitter.scopes[emitter.scopes.length - 1]!.push({ slot: b.slot, type: b.type });
emitter.noteLastUseMove(s, b);
@@ -517,6 +528,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
s.index,
s.value,
emitter.bytesBounds.has(s),
emitter.bytesOwned.has(s),
);
break;
}
@@ -558,7 +570,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
emitter.emitPublishedFieldGuard(obj.name, null, s.field);
const { ptr, type, inline } =
s.kind === "fieldSet"
? emitter.classFieldPtr(obj.name, s.className, s.field)
? emitter.classFieldPtr(obj.name, s.className, s.field, "compressed")
: emitter.recordFieldPtr(obj.name, s.shapeId, s.field);
// A result-slot entry fills zeroed caller storage, each field once.
if (
@@ -787,6 +799,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
break;
}
case "while": {
const callbacks = callbackVersionBindings(emitter, s);
if (callbacks) {
emitCallbackVersionedLoop(emitter, s, callbacks);
break;
}
const workerBudget = emitter.workerLoopBudget(s);
const le = B.newLabel("loop.e");
emitter.emitLoopCopies(s, true, le, (exit) => {
@@ -795,6 +812,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
const lp = workerBudget !== null ? B.newLabel("loop.p") : lc;
B.br(lc);
B.startBlock(lc);
enterLoopHeader(emitter, lc);
B.condBr(emitter.emitCondition(s.cond), lb, exit);
B.startBlock(lb);
emitter.jumpTargets.push({
@@ -819,6 +837,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
break;
}
case "doWhile": {
const callbacks = callbackVersionBindings(emitter, s);
if (callbacks) {
emitCallbackVersionedLoop(emitter, s, callbacks);
break;
}
const workerBudget = emitter.workerLoopBudget(s);
// Body first (runs at least once); continue jumps to the CONDITION.
const le = B.newLabel("loop.e");
@@ -827,6 +850,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
const lc = B.newLabel("loop.c");
B.br(lb);
B.startBlock(lb);
enterLoopHeader(emitter, lb);
emitter.jumpTargets.push({
kind: "loop",
brkLabel: exit,
@@ -847,6 +871,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
break;
}
case "for": {
const callbacks = callbackVersionBindings(emitter, s);
if (callbacks) {
emitCallbackVersionedLoop(emitter, s, callbacks);
break;
}
// The init's scope wraps the whole loop (break/continue must NOT
// release it — scopeDepth captured after the push, C parity).
const integerArrayLoop =
@@ -865,8 +894,15 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
emitter.unaliasedArrays,
)
: null;
// Two counters closing in on each other (`lo++, hi--`) take the same
// integer storage as a counted loop's one.
const pairedLoop =
!integerLoop && !countedLoop && emitter.countedLoopsEnabled
? matchIntegerPairedForLoop(s, emitter.numericLocals, emitter.integerRanges)
: null;
// Byte windows and dense loops reason about one counter.
const window =
countedLoop && emitter.byteWindowsEnabled
countedLoop && countedLoop.followers.length === 0 && emitter.byteWindowsEnabled
? matchByteWindow(
s,
countedLoop,
@@ -884,7 +920,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
length: integerArrayLoop.limitReceiver as IrExpr & { kind: "varRef" },
range: { min: 0, max: 4294967294 },
}
: countedLoop && !countedLoop.guarded
: countedLoop && !countedLoop.guarded && countedLoop.followers.length === 0
? countedDenseShape(s, countedLoop)
: null;
const dense =
@@ -945,18 +981,32 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
B.startBlock(done);
break;
}
const version =
const versionable =
emitter.bytesLoopVersionsEnabled &&
emitter.bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH &&
!emitter.bytesLoopVersioning.has(s)
? emitter.matchBytesLoopVersion(s, countedLoop)
: null;
!emitter.bytesLoopVersioning.has(s);
// A loop to a typed array's or an array's length keeps its integer
// induction. Its counted form only gives the version guard the
// counter's iteration space, so the guard also covers accesses of
// other typed arrays at the counter, such as `x[i]` and `y[i]` in
// `for (let i = 0; i < out.length; i++) out[i] = x[i] * y[i]`.
const versionCounted =
versionable && !countedLoop && integerLoop && emitter.countedLoopsEnabled
? matchIntegerCountedForLoop(
s,
emitter.numericLocals,
emitter.integerRanges,
emitter.unaliasedArrays,
)
: countedLoop;
const version = versionable ? emitter.matchBytesLoopVersion(s, versionCounted) : null;
if (version) {
const fast = B.newLabel("bytes.version.fast"),
slow = B.newLabel("bytes.version.slow"),
done = B.newLabel("bytes.version.done");
emitBytesLoopVersionGuard(emitter, version, fast, slow);
const originalBounds = emitter.bytesBounds,
originalOwned = emitter.bytesOwned,
originalRanges = emitter.integerRanges;
emitter.bytesLoopVersioning.add(s);
try {
@@ -965,6 +1015,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
B.br(done);
B.startBlock(fast);
emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]);
if (version.owned.size > 0)
emitter.bytesOwned = new Set([...originalOwned, ...version.owned]);
if (version.ranges.size > 0)
emitter.integerRanges = new Map([...originalRanges, ...version.ranges]);
if (version.counted?.guarded) emitter.provenCountedGuards.add(s);
@@ -972,6 +1024,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
B.br(done);
} finally {
emitter.bytesBounds = originalBounds;
emitter.bytesOwned = originalOwned;
emitter.integerRanges = originalRanges;
emitter.provenCountedGuards.delete(s);
emitter.bytesLoopVersioning.delete(s);
@@ -980,8 +1033,10 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
break;
}
let countedJoin: string | null = null;
if (countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) {
const safe = emitCountedLoopSafety(emitter, countedLoop);
if ((countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) || pairedLoop?.guarded) {
const safe = countedLoop
? emitCountedLoopSafety(emitter, countedLoop)
: emitPairedLoopSafety(emitter, pairedLoop!);
const fast = B.newLabel("counted.fast");
const slow = B.newLabel("counted.slow");
countedJoin = B.newLabel("counted.done");
@@ -996,6 +1051,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
emitter.scopes.push([]);
let integerSlot: string | null = null;
let countedLimit: string | null = null;
const pairedSlots: string[] = [];
const followerSlots: string[] = [];
if (integerLoop) {
integerSlot = B.slot();
B.entryAllocas.push(
@@ -1039,7 +1096,42 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
? emitter.denseLoop.range
: countedLoop.range,
});
for (const follower of countedLoop.followers) {
const slot = B.slot();
B.entryAllocas.push(
`${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(follower.localId)!.name}`,
);
const value = emitter.emitExpr(follower.start);
const followerStart = B.tmp();
B.line(`${followerStart} = fptosi double ${value.name} to i64`);
B.line(`store i64 ${followerStart}, ptr ${slot}`);
followerSlots.push(slot);
emitter.integerLoopBindings.set(follower.localId, {
slot,
type: "i64",
signed: true,
range: follower.range,
});
}
countedLimit = emitCountedLoopLimit(emitter, countedLoop);
} else if (pairedLoop) {
for (const counter of [pairedLoop.low, pairedLoop.high]) {
const slot = B.slot();
B.entryAllocas.push(
`${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(counter.localId)!.name}`,
);
const value = emitter.emitExpr(counter.start);
const start = B.tmp();
B.line(`${start} = fptosi double ${value.name} to i64`);
B.line(`store i64 ${start}, ptr ${slot}`);
pairedSlots.push(slot);
emitter.integerLoopBindings.set(counter.localId, {
slot,
type: "i64",
signed: true,
range: pairedLoop.range,
});
}
} else if (s.init) {
// A multi-declarator head shares the loop's scope. Emitting its
// IR block as an ordinary block would release captured/ref locals
@@ -1083,6 +1175,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
s.update || capturedLets.length > 0 || workerBudget !== null ? B.newLabel("loop.u") : lc;
B.br(lc);
B.startBlock(lc);
enterLoopHeader(emitter, lc);
if (integerLoop && integerSlot) {
const receiver = emitter.emitStableReceiver(integerLoop.limitReceiver, []);
const lenPtr = B.tmp();
@@ -1121,6 +1214,14 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
`${inBounds} = icmp ${countedLoop.step > 0 ? "slt" : "sgt"} i64 ${index}, ${countedLimit}`,
);
B.condBr(inBounds, lb, exit);
} else if (pairedLoop) {
const low = B.tmp(),
high = B.tmp(),
open = B.tmp();
B.line(`${low} = load i64, ptr ${pairedSlots[0]}`);
B.line(`${high} = load i64, ptr ${pairedSlots[1]}`);
B.line(`${open} = icmp ${pairedLoop.inclusive ? "sle" : "slt"} i64 ${low}, ${high}`);
B.condBr(open, lb, exit);
} else if (s.cond) B.condBr(emitter.emitCondition(s.cond), lb, exit);
else B.br(lb);
B.startBlock(lb);
@@ -1152,6 +1253,21 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
B.line(`${old} = load i64, ptr ${integerSlot}`);
B.line(`${next} = add nsw i64 ${old}, ${countedLoop.step}`);
B.line(`store i64 ${next}, ptr ${integerSlot}`);
countedLoop.followers.forEach((follower, i) => {
const was = B.tmp();
const now = B.tmp();
B.line(`${was} = load i64, ptr ${followerSlots[i]}`);
B.line(`${now} = add nsw i64 ${was}, ${follower.step}`);
B.line(`store i64 ${now}, ptr ${followerSlots[i]}`);
});
} else if (pairedLoop) {
[pairedLoop.low, pairedLoop.high].forEach((counter, i) => {
const old = B.tmp();
const next = B.tmp();
B.line(`${old} = load i64, ptr ${pairedSlots[i]}`);
B.line(`${next} = add nsw i64 ${old}, ${counter.step}`);
B.line(`store i64 ${next}, ptr ${pairedSlots[i]}`);
});
} else if (s.update) emitter.emitStmt(s.update);
B.br(lc);
}
@@ -1159,7 +1275,15 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
B.startBlock(le);
emitter.releaseScope(emitter.scopes.pop()!);
if (integerLoop) emitter.integerLoopBindings.delete(integerLoop.localId);
if (countedLoop) emitter.integerLoopBindings.delete(countedLoop.localId);
if (countedLoop) {
emitter.integerLoopBindings.delete(countedLoop.localId);
for (const follower of countedLoop.followers)
emitter.integerLoopBindings.delete(follower.localId);
}
if (pairedLoop) {
emitter.integerLoopBindings.delete(pairedLoop.low.localId);
emitter.integerLoopBindings.delete(pairedLoop.high.localId);
}
if (integerArrayLoop) emitter.integerArrayBindings.delete(integerArrayLoop.localId);
if (countedJoin) {
B.br(countedJoin);
@@ -1526,6 +1650,48 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
* Abrupt completions inside tryBody/catchBody ride the finallyStack:
* returns inline copies at the return site, while break/continue use the
* shared region walk before branching to their resolved target. */
/** A constant number local whose initializer has an exact integer form
* (such as `const j = i * 4` in a typed-array loop version) keeps that
* integer in an i64 slot beside the double, so reads such as `px[j + 1]`
* index with integer arithmetic instead of converting the double back.
* Every other declaration of the local drops the integer: the same
* declaration is emitted once per loop copy, and a copy must not read the
* integer another copy stored. */
function storeIntegerTemp(
emitter: LlEmitter,
localId: string,
b: ReturnType<LlEmitter["binding"]>,
v: LlValue,
): void {
const local = b.local;
if (
!emitter.integerTempsEnabled ||
b.kind !== "local" ||
!local ||
local.type.kind !== "f64" ||
local.mutable ||
local.tdz === true ||
emitter.captureIds.has(localId) ||
!v.integer
) {
emitter.integerTemps.delete(localId);
return;
}
const B = emitter.B;
let slot = emitter.integerTemps.get(localId)?.slot;
if (slot === undefined) {
slot = B.slot();
B.entryAllocas.push(`${slot} = alloca i64 ; integer ${local.name}`);
}
B.line(`store i64 ${widenInteger(emitter, v.integer)}, ptr ${slot}`);
emitter.integerTemps.set(localId, {
slot,
type: "i64",
signed: true,
range: v.integer.range,
});
}
export function emitTryCatch(emitter: LlEmitter, s: IrStmt & { kind: "tryCatch" }): void {
const B = emitter.B;
const hasCatch = s.catchBody !== null;
@@ -1771,6 +1937,105 @@ export function emitSwitch(emitter: LlEmitter, s: IrStmt & { kind: "switch" }):
B.startBlock(end);
}
/** The loop copies a callback loop version needs: the bindings the loop
* calls that the constant-callback census resolves to one function, each
* with that function's direct entry. Only small loops of optimized
* synchronous functions, and only bindings declared before the loop,
* qualify: the guard reads each binding before the loop runs. Versions do
* not nest: a loop inside either copy of a version (including the
* versioned loop itself, emitted again for each copy) keeps its calls as
* they are, so a loop nest is emitted at most twice. */
function callbackVersionBindings(
emitter: LlEmitter,
s: IrStmt & { kind: "for" | "while" | "doWhile" },
): { ref: IrExpr & { kind: "varRef" }; direct: string }[] | null {
if (
!emitter.callbackVersionsEnabled ||
emitter.currentConstantCallbacks.size === 0 ||
emitter.callbackVersioning.size > 0
)
return null;
const found = new Map<string, { ref: IrExpr & { kind: "varRef" }; direct: string }>();
// Bindings the loop declares or writes (a callback declared in its body).
const bound = new Set<string>();
let cost = 0;
const expr = (e: IrExpr): boolean => {
if (++cost > MAX_CALLBACK_VERSION_COST) return false;
if (e.kind === "assignExpr") bound.add(e.localId);
if (e.kind === "callValue" && e.callee.kind === "varRef" && !found.has(e.callee.localId)) {
const local = emitter.currentLocals.get(e.callee.localId);
const target =
local && local.tdz !== true ? emitter.knownCallbackTarget(e.callee) : undefined;
if (target) found.set(e.callee.localId, { ref: e.callee, direct: target.direct });
}
return everyExprChild(e, expr, stmt);
};
const stmt = (x: IrStmt): boolean => {
if (++cost > MAX_CALLBACK_VERSION_COST) return false;
if (x.kind === "varDecl" || x.kind === "assign" || x.kind === "forOf") bound.add(x.localId);
else if (x.kind === "tryCatch" && x.catchLocalId !== null) bound.add(x.catchLocalId);
return everyStmtChild(x, expr, stmt);
};
if (!stmt(s)) return null;
const bindings = [...found.values()].filter(({ ref }) => !bound.has(ref.localId));
return bindings.length > 0 ? bindings : null;
}
/** A callback loop version. The guard tests, once before the loop, that
* each binding's closure holds its known function: a closure's code
* pointer is written only when the closure is built, and the census
* resolves only bindings the function never rebinds. The copy where the
* guard holds calls those functions directly, with no indirect fallback
* that could rewrite the closure or what it captures, so LLVM can keep a
* captured scalar the callback updates in a register. The other copy is the
* ordinary loop. */
function emitCallbackVersionedLoop(
emitter: LlEmitter,
s: IrStmt & { kind: "for" | "while" | "doWhile" },
bindings: { ref: IrExpr & { kind: "varRef" }; direct: string }[],
): void {
const B = emitter.B;
let ok = "true";
for (const { ref, direct } of bindings) {
// A borrowed read: the guard only compares the code pointer.
const closure = emitter.emitReadReceiver(ref).name;
const fnp = B.tmp(),
fn = B.tmp(),
same = B.tmp();
B.line(`${fnp} = getelementptr inbounds %ScrClosure, ptr ${closure}, i64 0, i32 1`);
B.line(`${fn} = load ptr, ptr ${fnp}`);
B.line(`${same} = icmp eq ptr ${fn}, ${direct}`);
if (ok === "true") ok = same;
else {
const both = B.tmp();
B.line(`${both} = and i1 ${ok}, ${same}`);
ok = both;
}
}
const fast = B.newLabel("callback.version.fast"),
slow = B.newLabel("callback.version.slow"),
done = B.newLabel("callback.version.done");
B.condBr(ok, fast, slow);
emitter.callbackVersioning.add(s);
try {
B.startBlock(slow);
emitter.emitStmt(s);
B.br(done);
B.startBlock(fast);
for (const { ref } of bindings) emitter.knownCallbacks.add(ref.localId);
emitter.emitStmt(s);
B.br(done);
} finally {
for (const { ref } of bindings) emitter.knownCallbacks.delete(ref.localId);
emitter.callbackVersioning.delete(s);
}
B.startBlock(done);
}
/** Entries in a row whose fast copy bailed before a speculative loop site
* stops trying it. */
const SPECULATION_BAILS = 8;
/** Emit a loop's condition, body and update through `emitCopy`, twice
* when speculative-integers.ts plans a fast integer copy. Both copies
* share the loop's setup and exit; the ordinary copy comes first so the
@@ -1825,23 +2090,66 @@ export function emitLoopCopies(
ok = both;
}
}
// Each entry whose fast copy bails counts at the loop site, and a fast
// copy that completes a speculated write and leaves the loop clears the
// count. After SPECULATION_BAILS bailing entries in a row, the site runs
// the ordinary copy only. The count is a hint: both copies give the same
// results, and a race between workers loses at most an update.
const bails = `@scr.spec.bails.${emitter.speculativeLoopSites++}`;
emitter.declare(`${bails} = internal global i8 0`);
const count = B.tmp(),
trying = B.tmp();
B.line(`${count} = load atomic i8, ptr ${bails} unordered, align 1`);
B.line(`${trying} = icmp ult i8 ${count}, ${SPECULATION_BAILS}`);
if (ok === "true") ok = trying;
else {
const both = B.tmp();
B.line(`${both} = and i1 ${ok}, ${trying}`);
ok = both;
}
const fast = B.newLabel("spec.fast"),
slow = B.newLabel("spec.slow");
B.condBr(ok, fast, slow);
const resume = new Map<IrStmt, { label: string; placed: boolean }>();
for (const s of plan.writes) resume.set(s, { label: B.newLabel("spec.resume"), placed: false });
// A failed check finishes its iteration in a third copy, entered only
// there: it re-executes the statement and the rest of the body and the
// update, then enters the ordinary copy through its header. Jumping
// into the middle of the ordinary copy instead would give that loop a
// second entry, and LLVM does not optimize an irreducible loop.
const points = [...resume.values()];
let header: string | null = null;
B.startBlock(slow);
emitter.speculationHeader = (label) => {
header = label;
};
try {
emitCopy(exit);
} finally {
emitter.speculationHeader = null;
}
if (header === null)
throw new InternalCompilerError("llvm emitter bug: speculative loop copy without a header");
B.startBlock(B.newLabel("spec.tail"));
emitter.speculationResume = resume;
emitter.speculationHeader = () => {
B.br(header!);
B.startBlock(B.newLabel("spec.unused"));
};
try {
emitCopy(exit);
} finally {
emitter.speculationResume = null;
emitter.speculationHeader = null;
}
B.startBlock(fast);
if (![...resume.values()].every((r) => r.placed)) {
if (!points.every((r) => r.placed)) {
B.br(slow);
return;
}
const ran = B.slot();
B.entryAllocas.push(`${ran} = alloca i1 ; speculated write completed`);
B.line(`store i1 false, ptr ${ran}`);
for (const id of plan.locals.keys()) {
const shadow = B.slot();
B.entryAllocas.push(
@@ -1878,22 +2186,43 @@ export function emitLoopCopies(
writes: plan.writes,
resume: new Map([...resume].map(([s, r]) => [s, r.label])),
deferred,
bails,
ran,
};
try {
if (deferred.size === 0) emitCopy(exit);
else {
const leave = B.newLabel("spec.leave");
emitCopy(leave);
B.startBlock(leave);
emitter.writeBackDeferred(deferred);
B.br(exit);
}
const leave = B.newLabel("spec.leave");
emitCopy(leave);
B.startBlock(leave);
emitter.writeBackDeferred(deferred);
// An entry that never ran a speculated write (a loop of no iterations)
// says nothing about the site.
const wrote = B.tmp(),
left = B.tmp(),
counted = B.tmp(),
both = B.tmp(),
clear = B.newLabel("spec.clear");
B.line(`${wrote} = load i1, ptr ${ran}`);
B.line(`${left} = load atomic i8, ptr ${bails} unordered, align 1`);
B.line(`${counted} = icmp ne i8 ${left}, 0`);
B.line(`${both} = and i1 ${wrote}, ${counted}`);
B.condBr(both, clear, exit);
B.startBlock(clear);
B.line(`store atomic i8 0, ptr ${bails} unordered, align 1`);
B.br(exit);
} finally {
emitter.speculation = null;
for (const id of plan.locals.keys()) emitter.integerLoopBindings.delete(id);
}
}
/** At the top of a loop copy's header: lets a speculative loop find its
* ordinary copy's header, and keeps its third copy out of the loop. */
function enterLoopHeader(emitter: LlEmitter, header: string): void {
const dispatch = emitter.speculationHeader;
emitter.speculationHeader = null;
dispatch?.(header);
}
/** A proven integer loop (speculative-integers.ts) for this loop, or null.
* A shadowed local's double is stale until the loop is left, which only
* the loop's exit, a return and an exception do: the loop is planned only
@@ -2020,7 +2349,8 @@ export function emitSpeculativeWrite(
if (emitter.frames.pop()!.length > 0)
throw new InternalCompilerError("llvm emitter bug: speculative write owns a temporary");
B.line(`store i64 ${result}, ptr ${binding.slot}`);
const deferred = emitter.speculation!.deferred;
const { deferred, bails: site, ran } = emitter.speculation!;
if (ran !== undefined) B.line(`store i1 true, ptr ${ran}`);
if (!deferred.has(target.localId)) {
const number = B.tmp();
B.line(`${number} = sitofp i64 ${result} to double`);
@@ -2031,6 +2361,14 @@ export function emitSpeculativeWrite(
B.br(done);
B.startBlock(failed);
emitter.writeBackDeferred(deferred);
if (site !== undefined) {
const count = B.tmp(),
next = B.tmp();
emitter.declare("declare i8 @llvm.uadd.sat.i8(i8, i8)");
B.line(`${count} = load atomic i8, ptr ${site} unordered, align 1`);
B.line(`${next} = call i8 @llvm.uadd.sat.i8(i8 ${count}, i8 1)`);
B.line(`store atomic i8 ${next}, ptr ${site} unordered, align 1`);
}
B.br(resume!);
B.startBlock(done);
}
+457 -32
View File
@@ -266,6 +266,8 @@ import {
classFieldTypes,
classMembershipIntervals,
classFieldIndex,
compactRefLoadLines,
compactRefStoreLines,
classPreLines,
classStructSym,
programLifetimeClass,
@@ -287,6 +289,7 @@ import {
import {
arrNewCall,
boxAccess,
elemAccess,
boxNewCall,
boxReleaseSym,
computeTraced,
@@ -423,6 +426,20 @@ export interface LlvmTargetOptions {
/** Translated WebAssembly memories check bounds explicitly instead of
* relying on guard pages (wasm-modules.ts wasmBoundsChecks). */
wasmBoundsChecks?: boolean;
/** The executable can reserve its program-lifetime region below 32 GiB
* (lowRegionTarget): fields that refer to compact class instances then
* store the instance's address over 8 in 32 bits (classes.ts
* compactRefLoadLines). Elsewhere they keep 8-byte pointers. */
lowRegion?: boolean;
}
/** LlvmTargetOptions.lowRegion for an executable: Linux targets without
* sanitizers, whose address space below 32 GiB is free (ASan keeps its
* shadow memory there; other systems map their own images and libraries
* there). `platform` is a target platform (`linux`, `darwin`, `win32`,
* `wasi`). */
export function lowRegionTarget(platform: string, sanitize: boolean): boolean {
return platform === "linux" && !sanitize;
}
export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string {
@@ -571,6 +588,33 @@ function mentionsLocal(e: IrExpr, localId: string): boolean {
return found;
}
/** The IR nodes in `body` that name local `id`: its declaration, reads,
* writes and closure captures. */
function countLocalMentions(body: readonly IrStmt[], id: string): number {
let count = 0;
const visit = (node: unknown): void => {
if (node === null || typeof node !== "object") return;
if (Array.isArray(node)) {
for (const item of node) visit(item);
return;
}
const record = node as Record<string, unknown>;
if (record["localId"] === id) count++;
const captures = record["captures"];
if (Array.isArray(captures))
for (const c of captures)
if (
c === id ||
(typeof c === "object" && c !== null && (c as { localId?: unknown }).localId === id)
)
count++;
for (const [key, value] of Object.entries(record))
if (key !== "type" && key !== "loc" && key !== "captures") visit(value);
};
visit(body);
return count;
}
export class LlEmitter {
localArrayReads = new Map<string, LocalArrayRead>();
loopArrayBorrows: ReadonlySet<IrStmt> = new Set();
@@ -593,6 +637,13 @@ export class LlEmitter {
integerArrayBindings = new Set<string>();
readonly fieldAliasTags = new Map<string, number>();
readonly fieldPointerTags = new Map<string, number>();
/** The field pointers (classFieldPtr) of this function whose fields hold
* 32-bit references to compact class instances (LlClassMeta.compressed):
* loadField/loadRef decode them and storeField/storeRef encode them. */
readonly compressedFieldPtrs = new Set<string>();
/** Whether a class has compressed fields: main reserves the region before
* compiled code runs (scr_plr_init). */
usesCompactRefs = false;
/** Native class fields with different property names cannot occupy the
* same slot of one object. Inherited views of the same property share a
@@ -780,6 +831,12 @@ export class LlEmitter {
readonly callLifetimes: CallLifetimes;
readonly constantCallbacks: Map<string, Map<string, string>>;
currentConstantCallbacks: ReadonlyMap<string, string> = new Map();
/** Callback bindings whose closures an enclosing callback loop version
* proved to hold their known function: calls take the direct entry with
* no fallback (emitCallbackVersionedLoop). */
knownCallbacks = new Set<string>();
readonly callbackVersioning = new Set<IrStmt>();
callbackVersionsEnabled = false;
readonly stackCallbacks: StackCallbacks;
readonly receiverReaders: AmbientReceiverReaders;
readonly stackCaptures: StackCaptures;
@@ -861,9 +918,15 @@ export class LlEmitter {
/** Fiberless async functions a stackless coroutine body awaits directly;
* each gets an inline await entry (emitCoroInlineAwait). */
readonly inlineAwaitCallees = new Set<string>();
/** Stackless async functions some coroutine body awaits directly: each
* gets a direct spawn entry (emitCoroDirectAwait). */
readonly directAwaitCallees = new Set<string>();
/** The async functions whose bodies never suspend and that a direct
* await may enter in place (selectStacklessAsync). */
private inlineAwaitable = new Set<string>();
/** Locals whose array literal emitStmts left for the `await Promise.all`
* that reads them (deferredPromiseAllLocal). */
private readonly deferredPromiseAllEntries = new Map<string, ExprOf<"arrayLit">>();
/** The promise a `return` emitted past its finally regions already
* produced for its adopting await (borrowed from the return's parked
* slot); that await takes it instead of evaluating its operand. */
@@ -1024,6 +1087,17 @@ export class LlEmitter {
string,
{ slot: string; type: "i32" | "i64"; signed?: boolean; range?: { min: number; max: number } }
>();
/** Constant number locals whose latest emitted declaration stored an
* exact integer beside the double, such as `const j = i * 4` in a
* typed-array loop version: an i64 slot and the integer's range. */
integerTemps = new Map<
string,
{ slot: string; type: "i64"; signed: true; range: IntegerRange }
>();
/** Integer temps are kept only in optimized synchronous functions: an
* async or generator body resumes with its locals in its frame, which
* an entry alloca is not part of. */
integerTempsEnabled = false;
countedLoopsEnabled = false;
/** Speculative integer loops (speculative-integers.ts) run in this function. */
speculativeLoopsEnabled = false;
@@ -1034,9 +1108,20 @@ export class LlEmitter {
resume: Map<IrStmt, string>;
/** Globals whose double is written back only when the copy is left. */
deferred: Set<string>;
/** The loop site's count of bailing entries in a row (a global), and
* whether this entry completed a speculated write (a slot). A proven
* integer loop has neither. */
bails?: string;
ran?: string;
} | null = null;
/** Speculative loop sites numbered so far in the module. */
speculativeLoopSites = 0;
/** The ordinary copy being emitted: resume labels still to place. */
speculationResume: Map<IrStmt, { label: string; placed: boolean }> | null = null;
/** Run once at the top of the next loop copy's header
* (`enterLoopHeader`): records the ordinary copy's header, or sends the
* third copy's header there. */
speculationHeader: ((header: string) => void) | null = null;
/** The function being emitted, for speculative globals. */
speculativeFunction = "";
/** Every function that writes each number global (computed on demand). */
@@ -1068,6 +1153,9 @@ export class LlEmitter {
* guard already passed. */
readonly provenCountedGuards = new Set<IrStmt>();
bytesBounds: ReadonlySet<IrExpr | IrStmt> = new Set();
/** Worker programs: typed-array accesses whose views an enclosing version
* guard proved thread-owned. */
bytesOwned: ReadonlySet<IrExpr | IrStmt> = new Set();
integerViews = new Map<string, string>();
/** Module globals the current function owns outright (initializer
* bindings): their function-local slot, which stands in for the global. */
@@ -1507,7 +1595,32 @@ export class LlEmitter {
this.classMeta,
(root) => programLifetimeClass(this.shapeHost, root) && !root.threadSlot,
(cls) => classFieldTypes(this.shapeHost, cls, this.int32Slots),
// Executables whose region lies below 32 GiB hold compact class
// instances there (scr_plr_alloc), so fields referring to one store
// a 32-bit reference: the field's type, the class of a
// `C | undefined` or `C | null` pointer, or the reference arm of a
// nullable pointer field, is a compact class. `C | null | undefined`
// keeps the pointer: its null is a sentinel outside the region.
mod.lib === undefined && options.lowRegion === true
? (cls, i, isCompact) => {
const f = cls.fields[i]!;
if (this.inlineUnions.classField(cls.name, f.name)) return false;
const t = f.type;
if (t.kind === "object") return isCompact(t.className);
if (t.kind !== "union") return false;
const single = this.nullableUnions.single(t.unionId);
if (single !== null)
return (
single.nullTag < 0 &&
single.arm.kind === "object" &&
isCompact(single.arm.className)
);
const nullable = this.nullableFields.get(cls.name, f.name);
return nullable !== null && isCompact(nullable.arm.className);
}
: undefined,
);
this.usesCompactRefs = [...this.classMeta.values()].some((m) => m.compressed.some(Boolean));
for (const [name, rec] of RUNTIME_ERROR_CLASSES) {
const meta = this.classMeta.get(name);
if (!meta) break; // hand-written IR without the builtin defs: no stamps
@@ -1800,14 +1913,14 @@ export class LlEmitter {
/** Whether a fiberless body running on its caller's stack needs the
* frame scr_async_inline_enter/exit keep: it isolates the Error.stack
* frame chain, which only stack-observing programs push, and the
* AsyncLocalStorage context, which only programs that enter stores
* change (RuntimeFeatures asyncContext), as a fresh fiber would. Other
* programs run the body with a plain call. The frame also counts the
* bodies running on a fiber's stack for the runtime's trap on a fiberless
* body that suspends. */
* frame chain, which only stack-observing programs push, as a fresh fiber
* would. Other programs run the body with a plain call. The body runs in
* its caller's AsyncLocalStorage context either way, as Node runs an async
* body up to its first await. The frame also counts the bodies running on
* a fiber's stack for the runtime's trap on a fiberless body that
* suspends. */
get fiberlessFrames(): boolean {
return this.stackTraces || this.schedulesInContext;
return this.stackTraces;
}
/** The spawn wrapper of a fiberless async function: runs the body on the
@@ -1882,6 +1995,69 @@ export class LlEmitter {
return fn.captures === undefined;
}
/** Whether a stackless coroutine body's `await f(...)` may await f's
* fiber record instead of its promise (emitCoroDirectAwait): f is a
* stackless async function whose direct call an await-call entry could
* take. The await consumes the promise, so nothing else can observe it.
* Worker programs keep the promise for their stop checkpoint. */
directAwaitEligible(fnName: string): boolean {
if (this.wasi || this.mod.workers === true || this.currentCoro?.kind !== "async") return false;
const fn = this.fnByName.get(fnName);
if (fn === undefined || fn.async !== true || fn.generator !== undefined) return false;
if (!this.stacklessAsync.has(fnName) || fn.captures !== undefined) return false;
if (fn.returnType.kind === "dyn") return false;
return fn.asyncCacheGlobal === undefined && fn.asyncCycleCacheGlobal === undefined;
}
/** A stackless coroutine body's `await f(...)` of a stackless f, without
* f's promise object: f's prefix runs as the spawn of a fiber record that
* keeps the promise inside. A body still running wakes this coroutine
* when it finishes, where its promise's settlement would wake its one
* waiter; a finished one takes the settled await's turn. The result (or
* the rejection, re-thrown) comes out of the record, which goes. */
emitCoroDirectAwait(call: ExprOf<"call">, type: IrType): LlValue {
const coro = this.currentCoro;
if (coro === null)
throw new InternalCompilerError("llvm emitter bug: direct await outside a coroutine");
const B = this.B;
const record = emitCallExpr(this, call, "direct").name;
this.declare(`declare zeroext i1 @scr_coro_direct_prepare(ptr, ptr)`);
const suspend = B.tmp();
B.line(
`${suspend} = call zeroext i1 @scr_coro_direct_prepare(ptr ${coro.self}, ptr ${record})`,
);
const park = B.newLabel("await.suspend");
const ready = B.newLabel("await.ready");
B.condBr(suspend, park, ready);
B.startBlock(park);
this.emitCoroSuspendPrepared();
B.br(ready);
B.startBlock(ready);
if (type.kind === "void") {
this.declare(`declare void @scr_coro_direct_void(ptr)`);
B.line(`call void @scr_coro_direct_void(ptr ${record})`);
this.emitPendingCheck();
return { name: "", type };
}
const t = B.tmp();
if (type.kind === "f64" || type.kind === "date") {
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") {
this.declare(`declare zeroext i1 @scr_coro_direct_bool(ptr)`);
B.line(`${t} = call zeroext i1 @scr_coro_direct_bool(ptr ${record})`);
} else if (type.kind === "string") {
this.declare(`declare ptr @scr_coro_direct_str(ptr)`);
B.line(`${t} = call ptr @scr_coro_direct_str(ptr ${record})`);
} else {
this.declare(`declare ptr @scr_coro_direct_ref(ptr)`);
B.line(`${t} = call ptr @scr_coro_direct_ref(ptr ${record})`);
}
const out = this.own({ name: this.adoptHeap(t, type), type });
this.emitPendingCheck();
return out;
}
/** The inline await entry of a fiberless async function (`sc_ai_*`): its
* body on the caller's stack (between scr_async_inline_enter/exit when
* fiberlessFrames), as in its spawn wrapper, returning the result and
@@ -1945,6 +2121,185 @@ export class LlEmitter {
this.emitPendingCheck();
return value;
}
/** The entries of `Promise.all(arg)` that a stackless coroutine body's
* await may run in place (emitCoroInlinePromiseAll): `arg` is a non-empty
* array literal, or the local whose declaration emitStmts deferred to this
* await (deferredPromiseAllEntries), whose every entry is a call to a
* never-suspending async function, and the result is an array. Null
* otherwise. Consumes a deferred local's entries. */
inlinePromiseAllEntries(all: ExprOf<"intrinsic">): ExprOf<"arrayLit"> | null {
if (all.name !== "promise.all" || all.type.kind !== "promise") return null;
if (all.type.inner.kind !== "array") return null;
const arg = all.args[0];
if (arg?.kind === "varRef") {
const deferred = this.deferredPromiseAllEntries.get(arg.localId);
if (deferred === undefined) return null;
this.deferredPromiseAllEntries.delete(arg.localId);
return deferred;
}
return arg?.kind === "arrayLit" && this.promiseAllInlineable(arg) ? arg : null;
}
/** Whether every entry of `entries` is a call an await could run in place
* (inlineAwaitEligible) that yields its promise's inner type. */
private promiseAllInlineable(entries: ExprOf<"arrayLit">): boolean {
if (entries.elems.length === 0 || (entries.spreads?.length ?? 0) > 0) return false;
if (entries.type.kind !== "array" || entries.type.elem.kind !== "promise") return false;
const inner = entries.type.elem.inner;
return entries.elems.every((el) => {
if (el.kind !== "call" || !this.inlineAwaitEligible(el.callee)) return false;
const callee = this.fnByName.get(el.callee)!;
return typeEquals(callee.returnType, inner) && !this.callLifetimes.borrowed.has(el.callee);
});
}
/** Whether `stmts[i]` declares a local holding an array literal that only
* `stmts[i + 1]` reads, as the operand of `await Promise.all(local)` that
* runs first in that statement and may run its entries in place. Nothing
* runs between the literal and the await, so the await evaluates the
* literal itself (inlinePromiseAllEntries). Debug builds keep the local. */
deferredPromiseAllLocal(stmts: readonly IrStmt[], i: number): ExprOf<"arrayLit"> | null {
const decl = stmts[i];
const next = stmts[i + 1];
if (this.debug !== null || decl?.kind !== "varDecl" || decl.init?.kind !== "arrayLit")
return null;
if (this.currentCoro?.kind !== "async" || next === undefined) return null;
const operand =
next.kind === "varDecl"
? next.init
: next.kind === "exprStmt"
? next.expr
: next.kind === "assign"
? next.value
: null;
if (operand?.kind !== "awaitExpr" || operand.adopt === true) return null;
const all = operand.value;
if (all.kind !== "intrinsic" || all.name !== "promise.all" || all.type.kind !== "promise")
return null;
if (all.type.inner.kind !== "array") return null;
const arg = all.args[0];
if (arg?.kind !== "varRef" || arg.localId !== decl.localId) return null;
if (!this.promiseAllInlineable(decl.init)) return null;
if (this.currentLocals.get(decl.localId)?.boxed) return null;
// The declaration and this read are the local's only mentions.
return countLocalMentions(this.currentFunctionBody, decl.localId) === 2 ? decl.init : null;
}
/** A stackless coroutine body's `await Promise.all([f(...), ...])` of
* never-suspending calls, with Promise.all's turns and without its
* promises: the entries run in place in order and their values fill the
* result array. Promise.all queues one reaction job per entry, back to
* back, and the last of them settles the result, whose await then takes a
* turn: here one turn stands for the entries' jobs and a second for the
* await. A throwing entry becomes the rejection the await re-throws from
* its turn; the remaining entries still run, and later throws are handled
* rejections, as Promise.all's subscriptions make them. */
emitCoroInlinePromiseAll(entries: ExprOf<"arrayLit">, type: IrType): LlValue {
if (type.kind !== "array")
throw new InternalCompilerError("llvm emitter bug: inline Promise.all result");
const B = this.B;
const elem = type.elem;
const acc = elemAccess(elem);
const values = B.tmp();
// Up to three values fit in the array's own block (scr_arr_new_*_small).
const small = entries.elems.length <= 3;
B.line(
`${values} = ${arrNewCall(this.shapeHost, elem, small ? "0" : String(entries.elems.length), small)}`,
);
const out = this.own({ name: values, type });
this.declare(`declare ptr @scr_promise_new()`);
this.declare(`declare void @scr_promise_reject_pending(ptr)`);
this.declare(`declare void @scr_promise_mark_handled(ptr)`);
const promiseType = entries.type.kind === "array" ? entries.type.elem : type;
// Each entry's rejection, owned by the frame: unobserved until the
// entries have all run, as Promise.all subscribes only then, so an
// entry argument's synchronous throw leaves them unhandled.
const rejections: string[] = [];
for (const entry of entries.elems) {
if (entry.kind !== "call")
throw new InternalCompilerError("llvm emitter bug: inline Promise.all entry");
const slot = B.tmp();
B.entryAllocas.push(`${slot} = alloca ptr`);
B.line(`store ptr null, ptr ${slot}`);
this.ownSlot(slot, promiseType);
rejections.push(slot);
const value = emitCallExpr(this, entry, "inline");
const thrown = B.tmp();
for (const line of this.pendingTestLines(thrown)) B.line(line.trim());
const clean = B.newLabel("pa.clean");
const rejected = B.newLabel("pa.rejected");
const next = B.newLabel("pa.next");
B.condBr(thrown, rejected, clean);
B.startBlock(clean);
if (acc === "ref") this.toHeap(value);
this.arrPush(values, acc, value.name);
B.br(next);
B.startBlock(rejected);
const promise = B.tmp();
B.line(`${promise} = call ptr @scr_promise_new()`);
B.line(`call void @scr_promise_reject_pending(ptr ${promise})`);
B.line(`store ptr ${promise}, ptr ${slot}`);
B.br(next);
B.startBlock(next);
}
// Promise.all's subscriptions handle every rejection, and the first
// (in entry order) rejects the result.
let first = "null";
for (let k = rejections.length - 1; k >= 0; k--) {
const slot = rejections[k]!;
const rejection = B.tmp();
const present = B.tmp();
B.line(`${rejection} = load ptr, ptr ${slot}`);
B.line(`${present} = icmp ne ptr ${rejection}, null`);
const mark = B.newLabel("pa.handled");
const after = B.newLabel("pa.unrejected");
B.condBr(present, mark, after);
B.startBlock(mark);
B.line(`call void @scr_promise_mark_handled(ptr ${rejection})`);
B.br(after);
B.startBlock(after);
const chosen = B.tmp();
B.line(`${chosen} = select i1 ${present}, ptr ${rejection}, ptr ${first}`);
first = chosen;
}
// The entries' reaction jobs.
this.emitCoroAwait(null);
const failed = B.tmp();
B.line(`${failed} = icmp ne ptr ${first}, null`);
const fulfilled = B.newLabel("pa.fulfilled");
const rejected = B.newLabel("pa.reject");
const join = B.newLabel("pa.join");
B.condBr(failed, rejected, fulfilled);
B.startBlock(fulfilled);
this.emitCoroAwait(null);
B.br(join);
B.startBlock(rejected);
this.declare(`declare void @scr_coro_await_void(ptr)`);
this.emitCoroAwait(first);
B.line(`call void @scr_coro_await_void(ptr ${first})`);
B.br(join);
B.startBlock(join);
// The rejections are released here, only where an entry threw, rather
// than by the frame, which releases each slot whether or not it holds a
// promise.
this.declare(`declare void @scr_promise_release(ptr)`);
for (const slot of rejections) {
const rejection = B.tmp();
const present = B.tmp();
B.line(`${rejection} = load ptr, ptr ${slot}`);
B.line(`${present} = icmp ne ptr ${rejection}, null`);
const release = B.newLabel("pa.release");
const after = B.newLabel("pa.released");
B.condBr(present, release, after);
B.startBlock(release);
B.line(`call void @scr_promise_release(ptr ${rejection})`);
B.br(after);
B.startBlock(after);
}
this.disownSlots(rejections.map((ptr) => ({ ptr })));
this.emitPendingCheck();
return out;
}
emitAsyncAwaitCall(fn: IrFunction, argPackLines: string[] | null, adopt = false): string[] {
return emitAsyncAwaitCall(this, fn, argPackLines, adopt);
}
@@ -3217,15 +3572,47 @@ export class LlEmitter {
objName: string,
className: string,
field: string,
): { ptr: string; type: IrType; inline: boolean } {
refs: "pointer" | "compressed" = "pointer",
): { ptr: string; type: IrType; inline: boolean; compressed: boolean } {
const meta = this.classMetaOf(className);
const { index, type } = classFieldIndex(meta, field);
const { index, type, compressed } = classFieldIndex(meta, field, refs);
const p = this.B.tmp();
this.B.line(
`${p} = getelementptr inbounds %${classStructSym(className)}, ptr ${objName}, i64 0, i32 ${index}`,
);
this.markFieldPointer(p, field);
return { ptr: p, type, inline: this.inlineUnions.classField(className, field) };
if (compressed) this.compressedFieldPtrs.add(p);
return { ptr: p, type, inline: this.inlineUnions.classField(className, field), compressed };
}
/** Loads the reference a field pointer addresses: a pointer, or the
* decoded 32-bit reference of a compressed field (compressedFieldPtrs).
* `tagged`: the load carries the field's alias tags. */
loadRef(ptr: string, tagged = true): string {
const B = this.B;
const v = B.tmp();
const tags = tagged ? this.fieldAliasAttachment(ptr) : "";
if (!this.compressedFieldPtrs.has(ptr)) {
B.line(`${v} = load ptr, ptr ${ptr}${tags}`);
return v;
}
const idx = B.tmp();
B.line(`${idx} = load i32, ptr ${ptr}${tags}`);
for (const line of compactRefLoadLines(idx, v, `${v}.r`)) B.line(line.trimStart());
return v;
}
/** Stores `value` (a pointer) through a field pointer: as is, or encoded
* as the 32-bit reference of a compressed field. */
storeRef(ptr: string, value: string): void {
const B = this.B;
if (!this.compressedFieldPtrs.has(ptr)) {
B.line(`store ptr ${value}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
return;
}
const idx = B.tmp();
for (const line of compactRefStoreLines(value, idx, `${idx}.r`)) B.line(line.trimStart());
B.line(`store i32 ${idx}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
}
/** The dynamic class's preorder number (borrowed object; the static class
@@ -3357,6 +3744,7 @@ export class LlEmitter {
const { tag, bits } = loadInlineUnion(B, ptr);
return stackUnionBox(B, this.sizeType, tag, bits);
}
if (this.compressedFieldPtrs.has(ptr)) return this.loadRef(ptr);
const fieldTy = llFieldType(t, false);
const raw = B.tmp();
B.line(`${raw} = load ${fieldTy}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
@@ -3383,8 +3771,7 @@ export class LlEmitter {
replaceField(ptr: string, type: IrType, inline: boolean, v: LlValue): void {
if (!inline && isRefCounted(type)) {
this.toHeap(v);
const old = this.B.tmp();
this.B.line(`${old} = load ptr, ptr ${ptr}`);
const old = this.loadRef(ptr, false);
this.storeField(ptr, type, v.name, false);
this.releaseHeap(old, type);
return;
@@ -3408,6 +3795,10 @@ export class LlEmitter {
storeInlineUnion(B, ptr, tag, bits);
return;
}
if (this.compressedFieldPtrs.has(ptr)) {
this.storeRef(ptr, value);
return;
}
const fieldTy = llFieldType(t, false);
if (fieldTy !== "i8") {
B.line(`store ${fieldTy} ${value}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
@@ -3476,6 +3867,8 @@ export class LlEmitter {
ranges: this.integerRanges,
initialized,
proven: this.bytesBounds,
workers: this.mod.workers === true,
owned: this.bytesOwned,
});
}
@@ -3525,6 +3918,25 @@ export class LlEmitter {
return b;
}
/** The function a call through `callee` is known to reach (a literal
* closure, or a binding the constant-callback census resolves) and the
* entry it calls directly. Async and generator functions stay indirect. */
knownCallbackTarget(callee: IrExpr): { known: IrFunction; direct: string } | undefined {
const knownName =
callee.kind === "closure"
? callee.fnName
: callee.kind === "varRef"
? this.currentConstantCallbacks.get(callee.localId)
: undefined;
const known = knownName ? this.fnByName.get(knownName) : undefined;
if (!known || known.async || known.generator) return undefined;
if (known.captures === undefined) {
this.fnValues.add(known.name);
return { known, direct: `@${mangleWrapper(known.name)}` };
}
return { known, direct: `@${this.callTarget(known.name)}` };
}
/** The payload slot of a capture box (ScrBox.slot, field 5). */
boxSlot(box: string): string {
const slot = this.B.tmp();
@@ -3538,12 +3950,18 @@ export class LlEmitter {
const r = B.tmp();
// Scalar payloads are read in place (scr_box_get_f64/bool are a
// memcpy and a nonzero test of the same slot), so LLVM can keep a
// captured number in a register across a loop of calls.
// captured number in a register across a loop of calls. They have an
// alias category of their own: every other access to a box's payload
// is untagged or a runtime call.
if (acc === "f64") {
B.line(`${r} = load double, ptr ${this.boxSlot(box)}`);
B.line(
`${r} = load double, ptr ${this.boxSlot(box)}${this.memoryAliasAttachment("box:scalar")}`,
);
} else if (acc === "bool") {
const raw = B.tmp();
B.line(`${raw} = load i64, ptr ${this.boxSlot(box)}`);
B.line(
`${raw} = load i64, ptr ${this.boxSlot(box)}${this.memoryAliasAttachment("box:scalar")}`,
);
B.line(`${r} = icmp ne i64 ${raw}, 0`);
} else {
this.declare(`declare ptr @scr_box_get_ref(ptr)`);
@@ -3557,11 +3975,15 @@ export class LlEmitter {
const B = this.B;
const acc = boxAccess(t);
if (acc === "f64") {
B.line(`store double ${value}, ptr ${this.boxSlot(box)}`);
B.line(
`store double ${value}, ptr ${this.boxSlot(box)}${this.memoryAliasAttachment("box:scalar")}`,
);
} else if (acc === "bool") {
const raw = B.tmp();
B.line(`${raw} = zext i1 ${value} to i64`);
B.line(`store i64 ${raw}, ptr ${this.boxSlot(box)}`);
B.line(
`store i64 ${raw}, ptr ${this.boxSlot(box)}${this.memoryAliasAttachment("box:scalar")}`,
);
} else {
this.declare(`declare void @scr_box_set_ref(ptr, ptr)`);
B.line(`call void @scr_box_set_ref(ptr ${box}, ptr ${value})`);
@@ -4499,6 +4921,11 @@ export class LlEmitter {
if (this.B.isTerminated()) return;
if (cursor) cursor.index = i;
this.stmtCursor = cursor;
const deferred = this.deferredPromiseAllLocal(stmts, i);
if (deferred !== null) {
this.deferredPromiseAllEntries.set((stmts[i] as { localId: string }).localId, deferred);
continue;
}
this.emitStmt(stmts[i]!);
}
} finally {
@@ -5182,8 +5609,9 @@ export class LlEmitter {
: [
{
name: f.name,
index: classFieldIndex(meta, f.name).index,
index: classFieldIndex(meta, f.name, "compressed").index,
type: this.nullableFields.storageType(className, f.name, f.type),
compressed: classFieldIndex(meta, f.name, "compressed").compressed,
},
],
),
@@ -5390,11 +5818,9 @@ export class LlEmitter {
emitUnionProjection(e: IrExpr): LlValue {
const nullable = this.nullableFieldGet(e);
if (!nullable || e.kind !== "fieldGet") return this.emitReadReceiver(e);
const B = this.B;
const receiver = this.emitReadReceiver(e.obj);
const { ptr } = this.classFieldPtr(receiver.name, e.className, e.field);
const p = B.tmp();
B.line(`${p} = load ptr, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
const { ptr } = this.classFieldPtr(receiver.name, e.className, e.field, "compressed");
const p = this.loadRef(ptr);
return { name: this.stackNullableBox(p, nullable), type: e.type };
}
@@ -5407,9 +5833,8 @@ export class LlEmitter {
if (!nullable || e.kind !== "fieldGet")
throw new InternalCompilerError("llvm emitter bug: owned nullable stack of non-field");
const receiver = this.emitReadReceiver(e.obj);
const { ptr } = this.classFieldPtr(receiver.name, e.className, e.field);
const p = this.B.tmp();
this.B.line(`${p} = load ptr, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
const { ptr } = this.classFieldPtr(receiver.name, e.className, e.field, "compressed");
const p = this.loadRef(ptr);
const owner = this.own({ name: this.retainFrame(p, nullable.arm), type: nullable.arm });
return { box: this.stackNullableBox(owner.name, nullable), owner };
}
@@ -5486,7 +5911,6 @@ export class LlEmitter {
nullable: NullableRefField,
value: IrExpr,
): void {
const B = this.B;
let next: string;
if (
value.kind === "unionWrap" &&
@@ -5509,10 +5933,9 @@ export class LlEmitter {
} else {
next = this.unionToNullable(this.emitUnionProjection(value).name, nullable);
}
const { ptr } = this.classFieldPtr(obj, className, field);
const old = B.tmp();
B.line(`${old} = load ptr, ptr ${ptr}`);
B.line(`store ptr ${next}, ptr ${ptr}${this.fieldAliasAttachment(ptr)}`);
const { ptr } = this.classFieldPtr(obj, className, field, "compressed");
const old = this.loadRef(ptr, false);
this.storeRef(ptr, next);
this.releaseHeap(old, nullable.arm);
}
@@ -5835,8 +6258,9 @@ export class LlEmitter {
expr?: IrExpr,
inBounds = false,
invalidNaN = false,
owned = false,
): LlValue {
return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN);
return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN, owned);
}
emitToUint32(value: string, expr?: IrExpr, uint32?: string): string {
@@ -5899,8 +6323,9 @@ export class LlEmitter {
indexExpr?: IrExpr,
expr?: IrExpr,
inBounds = false,
owned = false,
): void {
return emitBytesSet(this, elem, receiver, index, value, indexExpr, expr, inBounds);
return emitBytesSet(this, elem, receiver, index, value, indexExpr, expr, inBounds, owned);
}
emitBytesIntrinsic(e: IrExpr & { kind: "bytesIntrinsic" }): LlValue {
@@ -901,6 +901,12 @@ export function emitAsyncExpr(
// the callee's adopting entry. Past finally regions, the return has
// already produced the promise (adoptedOperand).
const adopted = e.adopt === true ? host.takeAdoptedOperand() : null;
// A stackless coroutine body runs the entries of `Promise.all` of
// never-suspending calls in place and takes Promise.all's turns.
if (adopted === null && e.value.kind === "intrinsic" && host.currentCoro?.kind === "async") {
const entries = host.inlinePromiseAllEntries(e.value);
if (entries !== null) return host.emitCoroInlinePromiseAll(entries, e.type);
}
// A stackless coroutine body runs a never-suspending callee in place
// and takes the await's turn itself.
if (adopted === null && e.value.kind === "call" && host.inlineAwaitEligible(e.value.callee)) {
@@ -912,6 +918,22 @@ export function emitAsyncExpr(
return host.emitCoroInlineAwait(e.value, e.adopt === true);
}
}
// A stackless coroutine body awaits a stackless callee's fiber record
// instead of its promise.
if (
adopted === null &&
e.adopt !== true &&
e.value.kind === "call" &&
host.directAwaitEligible(e.value.callee)
) {
const callee = host.fnByName.get(e.value.callee)!;
if (
typeEquals(callee.returnType, e.type) &&
!host.callLifetimes.borrowed.has(e.value.callee)
) {
return host.emitCoroDirectAwait(e.value, e.type);
}
}
if (adopted === null && e.value.kind === "call" && host.awaitCallEligible(e.value.callee)) {
const callee = host.fnByName.get(e.value.callee)!;
if (
+128 -40
View File
@@ -176,22 +176,30 @@ export function emitBytesLength(
return { name: out, type: F64 };
}
/** Worker programs: a view of a SharedArrayBuffer (`shared` is fixed before
* script code can see the view) is read and written by the runtime under
* its shared-memory lock; every other buffer belongs to this thread and
* takes the ordinary inline path. The test follows the index check: a
/** A view's shared backing (null for a buffer this thread owns). It is
* set at construction, before script code can see the view. */
export function emitBytesSharedStorage(host: LlvmEmitterContext, receiver: string): string {
const B = host.B;
const slot = B.tmp(),
storage = B.tmp();
B.line(`${slot} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 9`);
B.line(`${storage} = load ptr, ptr ${slot}${bytesHeaderAlias(host)}`);
return storage;
}
/** Worker programs: an element of a view of a SharedArrayBuffer (`shared` is
* fixed before script code can see the view) is read and written with one
* relaxed atomic access of its width, as the runtime's shared accessors do;
* every other buffer belongs to this thread and takes the ordinary path. The test follows the index check: a
* view's length never changes, so both paths use the one checked integer
* index. Branches to the returned label for a thread-owned buffer and
* continues in the shared block. */
function emitSharedBytesSplit(host: LlvmEmitterContext, receiver: string): string {
const B = host.B;
host.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
const slot = B.tmp(),
storage = B.tmp(),
const storage = emitBytesSharedStorage(host, receiver),
isShared = B.tmp(),
expected = B.tmp();
B.line(`${slot} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 9`);
B.line(`${storage} = load ptr, ptr ${slot}${bytesHeaderAlias(host)}`);
B.line(`${isShared} = icmp ne ptr ${storage}, null`);
B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${isShared}, i1 false)`);
const shared = B.newLabel("bytes.shared"),
@@ -209,15 +217,17 @@ export function emitBytesGet(
expr?: IrExpr,
inBounds = false,
invalidNaN = false,
owned = false,
): LlValue {
const B = host.B;
// Any invalid index traps (the array runtime's discipline).
if (!invalidNaN) return emitBytesLoad(host, elem, receiver, index, expr, inBounds, undefined);
if (!invalidNaN)
return emitBytesLoad(host, elem, receiver, index, expr, inBounds, undefined, owned);
// The numeric view of an ordinary read: an invalid index never touches
// storage and reads as NaN — ToNumber of Node's undefined.
const invalid = B.newLabel("bytes.read.invalid");
const done = B.newLabel("bytes.read.done");
const loaded = emitBytesLoad(host, elem, receiver, index, expr, inBounds, invalid);
const loaded = emitBytesLoad(host, elem, receiver, index, expr, inBounds, invalid, owned);
const ok = B.newLabel("bytes.read.ok");
B.br(ok);
B.startBlock(ok);
@@ -260,8 +270,10 @@ function emitBytesLoad(
expr: IrExpr | undefined,
inBounds: boolean,
invalid: string | undefined,
owned: boolean,
): LlValue {
if (host.mod.workers) {
// `owned`: an enclosing version guard proved the view thread-owned.
if (host.mod.workers && !owned) {
const idx = emitBytesIndex(host, receiver, index, expr, invalid, inBounds);
return emitSharedOrLocalLoad(host, elem, receiver, idx);
}
@@ -276,8 +288,9 @@ function emitBytesLoad(
* shared. An integer element joins as its 32-bit value and converts to a
* number after the join, as the local load does, so compares and
* ToInt32/ToUint32 of the result stay integer operations. The shared read
* is an ordinary call: another thread's write must stay visible to a loop
* that reads the same element again. */
* is a monotonic atomic load, which LLVM neither hoists nor merges: another
* thread's write stays visible to a loop that reads the same element again.
* Float16 elements read through the runtime. */
function emitSharedOrLocalLoad(
host: LlvmEmitterContext,
elem: IrBytesElem,
@@ -286,12 +299,18 @@ function emitSharedOrLocalLoad(
): LlValue {
const B = host.B;
const floating = elem === "f16" || elem === "f32" || elem === "f64";
const fn = floating ? "scr_bytes_shared_get_f64" : "scr_bytes_shared_get_int";
const ret = floating ? "double" : "i32";
host.declare(`declare ${ret} @${fn}(ptr, ${host.sizeType})`);
const local = emitSharedBytesSplit(host, receiver);
const shared = B.tmp();
B.line(`${shared} = call ${ret} @${fn}(ptr ${receiver}, ${host.sizeType} ${idx}) cold`);
let shared: string;
if (elem === "f16") {
host.declare(`declare double @scr_bytes_shared_get_f64(ptr, ${host.sizeType})`);
shared = B.tmp();
B.line(
`${shared} = call double @scr_bytes_shared_get_f64(ptr ${receiver}, ${host.sizeType} ${idx}) cold`,
);
} else {
const loaded = emitSharedLoadAt(host, elem, host.emitBytesData(receiver), idx);
shared = loaded.uint32 ?? loaded.name;
}
const sharedDone = B.newLabel("bytes.shared.done");
B.br(sharedDone);
B.startBlock(local);
@@ -316,6 +335,43 @@ function emitSharedOrLocalLoad(
return { name: out, type: F64, uint32: u32 };
}
/** The element at a checked index of a shared view, given its data pointer:
* a relaxed atomic load of the element's width, aligned as shared views
* are. Not for Float16 elements. */
function emitSharedLoadAt(
host: LlvmEmitterContext,
elem: IrBytesElem,
data: string,
idx: string,
): LlValue {
const B = host.B;
const size = BYTES_ELEMENT_SIZE[elem];
const p = B.tmp();
if (elem === "f32" || elem === "f64") {
const ty = elem === "f32" ? "float" : "double";
const raw = B.tmp();
B.line(`${p} = getelementptr inbounds ${ty}, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`${raw} = load atomic ${ty}, ptr ${p} monotonic, align ${size}`);
if (elem === "f64") return { name: raw, type: F64 };
const out = B.tmp();
B.line(`${out} = fpext float ${raw} to double`);
return { name: out, type: F64 };
}
const bits = size * 8;
const raw = B.tmp();
B.line(`${p} = getelementptr inbounds i${bits}, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`${raw} = load atomic i${bits}, ptr ${p} monotonic, align ${size}`);
const signed = elem === "i8" || elem === "i16" || elem === "i32";
let wide = raw;
if (bits < 32) {
wide = B.tmp();
B.line(`${wide} = ${signed ? "sext" : "zext"} i${bits} ${raw} to i32`);
}
const out = B.tmp();
B.line(`${out} = ${signed ? "sitofp" : "uitofp"} i32 ${wide} to double`);
return { name: out, type: F64, uint32: wide };
}
/** The element at a checked index of a thread-owned view, given its data
* pointer. */
function emitBytesLoadAt(
@@ -471,13 +527,14 @@ export function emitBytesSet(
indexExpr?: IrExpr,
expr?: IrExpr,
inBounds = false,
owned = false,
): void {
const B = host.B;
// Invalid integer-indexed writes are ignored after evaluating the RHS.
const done = B.newLabel("bytes.store.done");
const data = host.emitBytesData(receiver);
const idx = emitBytesIndex(host, receiver, index, indexExpr, done, inBounds);
if (host.mod.workers) {
if (host.mod.workers && !owned && elem === "f16") {
host.declare(`declare void @scr_bytes_shared_set(ptr, ${host.sizeType}, double)`);
const local = emitSharedBytesSplit(host, receiver);
B.line(
@@ -488,42 +545,72 @@ export function emitBytesSet(
}
let stored: string | null;
if (elem === "u8c") {
host.declare(`declare double @scr_bytes_to_u8_clamp(double)`);
const clamped = B.tmp();
// ToUint8Clamp, as scr_bytes_to_u8_clamp computes it: NaN and values
// at most 0 store 0, values at least 255 store 255, and values between
// round to the nearest integer, ties to even. Adding and subtracting
// 2^52 rounds a value in (0, 255) that way in the default rounding mode,
// with no call or branch, so loops of clamped stores can vectorize.
const positive = B.tmp(),
high = B.tmp(),
shifted = B.tmp(),
rounded = B.tmp(),
upper = B.tmp(),
clamped = B.tmp();
stored = B.tmp();
B.line(`${clamped} = call double @scr_bytes_to_u8_clamp(double ${value.name})`);
B.line(`${positive} = fcmp ogt double ${value.name}, 0.0`);
B.line(`${high} = fcmp oge double ${value.name}, 255.0`);
B.line(`${shifted} = fadd double ${value.name}, 0x4330000000000000`);
B.line(`${rounded} = fsub double ${shifted}, 0x4330000000000000`);
B.line(`${upper} = select i1 ${high}, double 255.0, double ${rounded}`);
B.line(`${clamped} = select i1 ${positive}, double ${upper}, double 0.0`);
B.line(`${stored} = fptoui double ${clamped} to i32`);
} else
stored =
elem === "f16" || elem === "f32" || elem === "f64"
? null
: host.emitToUint32(value.name, expr, value.uint32);
const p = B.tmp();
if (elem === "f16") {
// One round-to-nearest-even step from the double, in the runtime.
host.declare(`declare zeroext i16 @scr_f16_from_f64(double)`);
const narrowed = B.tmp();
const p = B.tmp();
B.line(`${narrowed} = call zeroext i16 @scr_f16_from_f64(double ${value.name})`);
B.line(`${p} = getelementptr inbounds i16, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`store i16 ${narrowed}, ptr ${p}, align 1${bytesElementAlias(host)}`);
} else if (BYTES_ELEMENT_SIZE[elem] < 4) {
const bits = BYTES_ELEMENT_SIZE[elem] * 8;
const byte = B.tmp();
B.line(`${byte} = trunc i32 ${stored!} to i${bits}`);
B.line(`${p} = getelementptr inbounds i${bits}, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`store i${bits} ${byte}, ptr ${p}, align 1${bytesElementAlias(host)}`);
} else if (elem === "f32") {
const narrowed = B.tmp();
B.line(`${narrowed} = fptrunc double ${value.name} to float`);
B.line(`${p} = getelementptr inbounds float, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`store float ${narrowed}, ptr ${p}, align 1${bytesElementAlias(host)}`);
} else if (elem === "f64") {
B.line(`${p} = getelementptr inbounds double, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`store double ${value.name}, ptr ${p}, align 1${bytesElementAlias(host)}`);
} else {
B.line(`${p} = getelementptr inbounds i32, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`store i32 ${stored!}, ptr ${p}, align 1${bytesElementAlias(host)}`);
B.br(done);
B.startBlock(done);
return;
}
// The element's bits as an LLVM value of the element's width.
let ty: string;
let operand: string;
if (BYTES_ELEMENT_SIZE[elem] < 4) {
ty = `i${BYTES_ELEMENT_SIZE[elem] * 8}`;
operand = B.tmp();
B.line(`${operand} = trunc i32 ${stored!} to ${ty}`);
} else if (elem === "f32") {
ty = "float";
operand = B.tmp();
B.line(`${operand} = fptrunc double ${value.name} to float`);
} else if (elem === "f64") {
ty = "double";
operand = value.name;
} else {
ty = "i32";
operand = stored!;
}
if (host.mod.workers && !owned) {
// A shared view's element takes a relaxed atomic store of its width.
const local = emitSharedBytesSplit(host, receiver);
const p = B.tmp();
B.line(`${p} = getelementptr inbounds ${ty}, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`store atomic ${ty} ${operand}, ptr ${p} monotonic, align ${BYTES_ELEMENT_SIZE[elem]}`);
B.br(done);
B.startBlock(local);
}
const p = B.tmp();
B.line(`${p} = getelementptr inbounds ${ty}, ptr ${data}, ${host.sizeType} ${idx}`);
B.line(`store ${ty} ${operand}, ptr ${p}, align 1${bytesElementAlias(host)}`);
B.br(done);
B.startBlock(done);
}
@@ -661,6 +748,7 @@ export function emitBytesIntrinsic(
e.args[0],
host.bytesBounds.has(e),
e.invalidNaN === true,
host.bytesOwned.has(e),
);
case "slice":
return call(
@@ -29,8 +29,8 @@ import {
mangleClassStruct,
mangleFnClosure,
mangleFunction,
mangleWrapper,
mangleVtStruct,
mangleAsyncDirectSpawn,
} from "../mangle.js";
import {
classEnvironmentIndex,
@@ -74,7 +74,7 @@ export function emitCallExpr(
| "instanceOf"
| "virtualCall"
>,
awaitEntry: false | "await" | "adopt" | "inline" = false,
awaitEntry: false | "await" | "adopt" | "inline" | "direct" = false,
/** A projection consumes a scalar union result (Int32Abi.union): answer
* a private stack box instead of a heap box. */
stackResult = false,
@@ -184,6 +184,14 @@ export function emitCallExpr(
: `${host.llType(callee.params[i]!.type)} ${a.name}`,
)
.join(", ");
if (awaitEntry === "direct") {
// A stackless coroutine's await of a stackless callee: the spawn
// returns the callee's fiber record (emitCoroDirectAwait).
host.directAwaitCallees.add(e.callee);
const record = B.tmp();
B.line(`${record} = call ptr @${mangleAsyncDirectSpawn(e.callee)}(${argList})`);
return { name: record, type: callee.returnType };
}
if (awaitEntry === "inline") {
// A stackless coroutine's await of a fiberless callee: the entry
// runs the body in place and leaves a throw pending; the enclosing
@@ -830,21 +838,9 @@ export function emitCallExpr(
host.declare("declare void @scr_dyn_this_push_dyn(ptr)");
B.line(`call void @scr_dyn_this_push_dyn(ptr ${receiver?.name ?? "null"})`);
};
const knownName =
e.callee.kind === "closure"
? e.callee.fnName
: e.callee.kind === "varRef"
? host.currentConstantCallbacks.get(e.callee.localId)
: undefined;
const candidate = knownName ? host.fnByName.get(knownName) : undefined;
const known = candidate && !candidate.async && !candidate.generator ? candidate : undefined;
let direct: string | undefined;
if (known) {
if (known.captures === undefined) {
host.fnValues.add(known.name);
direct = `@${mangleWrapper(known.name)}`;
} else direct = `@${host.callTarget(known.name)}`;
}
const target = host.knownCallbackTarget(e.callee);
const known = target?.known;
const direct = target?.direct;
// A known closure body that borrows reference parameters is entered
// directly with lent arguments, as a direct call to a borrowing body
// is: an argument is read without a reference when neither a later
@@ -930,7 +926,13 @@ export function emitCallExpr(
let pending = host.indirectMayThrow;
let pendingGuard: string | undefined;
let t: string;
if (direct && e.callee.kind === "closure") {
// A literal closure, or a binding an enclosing callback loop version
// proved to hold its known function, enters it directly.
if (
direct &&
(e.callee.kind === "closure" ||
(e.callee.kind === "varRef" && host.knownCallbacks.has(e.callee.localId)))
) {
t = invoke(lending ?? direct, !host.receiverReaders.ignores(known!));
if (quiet) pending = false;
} else {
@@ -510,9 +510,13 @@ export function emitOptionalChainReceiver(
// pointer itself, through a private stack box when the union's own
// representation is boxed (emitUnionProjection's form).
const owner = host.emitReadReceiver(e.receiver.obj);
const { ptr } = host.classFieldPtr(owner.name, e.receiver.className, e.receiver.field);
const p = B.tmp();
B.line(`${p} = load ptr, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
const { ptr } = host.classFieldPtr(
owner.name,
e.receiver.className,
e.receiver.field,
"compressed",
);
const p = host.loadRef(ptr);
r = {
name: host.nullableUnions.get(def.id) ? p : host.stackNullableBox(p, nullableField),
type: e.receiver.type,
@@ -16,6 +16,7 @@ import { streamTypedRefEligible } from "../../ir/analysis.js";
import {
DYN,
type IrExpr,
type IrType,
isDynTypedRefType,
isRefCounted,
isUnitType,
@@ -221,6 +222,19 @@ export function emitDynamicExpr(
host.own({ name: prepared, type: { kind: "dyn" } });
host.emitPendingCheck();
const args = e.args.map((a) => emitBorrowedInput(host, a));
// A closure registered as a 'message' listener gets a typed message
// entry: ports deliver messages of its parameter's layout without
// decoding them to checked-dynamic values first.
const listener = messageListenerArg(e);
if (listener !== null) {
const entry = host.dyn.messageListenerHelper(listener);
if (entry !== null) {
host.declare(`declare void @scr_dyn_func_set_message(ptr, ptr, ptr)`);
B.line(
`call void @scr_dyn_func_set_message(ptr ${args[1]!.name}, ptr @${host.dyn.dynFuncThunkHelper(listener)}, ptr @${entry})`,
);
}
}
let argsPtr = "null";
if (args.length > 0) {
const arr = B.slot();
@@ -344,7 +358,7 @@ export function emitDynamicExpr(
const payload = host.unionPeek(u.name, def.id);
const { ptr, type, inline } =
arm.kind === "object"
? host.classFieldPtr(payload, arm.className, e.field)
? host.classFieldPtr(payload, arm.className, e.field, "compressed")
: host.recordFieldPtr(payload, arm.shapeId, e.field);
const nullable =
arm.kind === "object" ? host.nullableFields.get(arm.className, e.field) : null;
@@ -1323,3 +1337,24 @@ export const INHERITED_OBJECT_MEMBERS: ReadonlySet<string> = new Set([
"hasOwnProperty",
"propertyIsEnumerable",
]);
/** EventEmitter methods that register their second argument as a listener. */
const LISTENER_METHODS: ReadonlySet<string> = new Set([
"on",
"once",
"addListener",
"prependListener",
"prependOnceListener",
]);
/** The closure type of `recv.on("message", listener)` and the other
* listener registrations with a typed closure for the 'message' event, or
* null. */
function messageListenerArg(e: ExprOf<"dynInvoke">): (IrType & { kind: "func" }) | null {
if (!LISTENER_METHODS.has(e.method) || e.args.length !== 2) return null;
const [event, listener] = e.args;
if (event?.kind !== "dynFrom" || event.value.kind !== "strLit" || event.value.value !== "message")
return null;
if (listener?.kind !== "dynFrom" || listener.value.type.kind !== "func") return null;
return listener.value.type;
}
@@ -59,7 +59,8 @@ export function emitLiteralExpr(
throw new InternalCompilerError(`llvm emitter bug: bare unitLit '${e.unit}'`);
case "varRef": {
host.materializeSplitLocal(e.localId);
const integerSlot = host.integerLoopBindings.get(e.localId);
const integerSlot =
host.integerLoopBindings.get(e.localId) ?? host.integerTemps.get(e.localId);
if (integerSlot !== undefined) {
const integer = B.tmp();
const number = B.tmp();
@@ -1064,13 +1065,17 @@ export function emitRecordExpr(
switch (e.kind) {
case "fieldGet": {
const obj = host.emitReadReceiver(e.obj);
const { ptr, type, inline } = host.classFieldPtr(obj.name, e.className, e.field);
const { ptr, type, inline } = host.classFieldPtr(
obj.name,
e.className,
e.field,
"compressed",
);
if (host.int32Slots.isField(e.className, e.field)) return host.loadInt32Field(ptr, e.type);
if (inline) return host.own({ name: host.inlineFieldUnion(ptr, type), type: e.type });
const nullable = host.nullableFieldGet(e);
if (nullable) {
const p = B.tmp();
B.line(`${p} = load ptr, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
const p = host.loadRef(ptr);
return host.own({ name: host.nullableToOwnedUnion(p, nullable), type: e.type });
}
const v = host.loadField(ptr, type, false);
@@ -20,7 +20,14 @@ import {
} from "../../ir/ir.js";
import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js";
import { BlockBuilder } from "./blocks.js";
import { type LlClassMeta, classFieldIndex, classPreLines, classStructSym } from "./classes.js";
import {
type LlClassMeta,
classFieldIndex,
classPreLines,
classStructSym,
compactRefLoadLines,
compactRefStoreLines,
} from "./classes.js";
import { SCR_ARR_FIELD, llvmCommentText } from "./common.js";
import {
FN_ATTRS,
@@ -280,7 +287,7 @@ export function streamTypedRefCommitAdapter(
const pending = `f${index}_pending`;
const store = `f${index}_store`;
const after = `f${index}_after`;
const { index: fieldIndex } = classFieldIndex(meta, field.name);
const { index: fieldIndex, compressed } = classFieldIndex(meta, field.name, "compressed");
const int32 = host.int32Slots.isField(t.className, field.name);
const inline = host.inlineUnions.classField(t.className, field.name);
const fieldTy = int32 ? "i32" : llFieldType(field.type, inline);
@@ -335,7 +342,12 @@ export function streamTypedRefCommitAdapter(
` br i1 %${pending}, label %done, label %${store}`,
`${store}:`,
` %f${index}_ptr = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 ${fieldIndex}`,
` %f${index}_old = load ${fieldTy}, ptr %f${index}_ptr`,
...(compressed
? [
` %f${index}_oldidx = load i32, ptr %f${index}_ptr`,
...compactRefLoadLines(`%f${index}_oldidx`, `%f${index}_old`, `%f${index}_od`),
]
: [` %f${index}_old = load ${fieldTy}, ptr %f${index}_ptr`]),
);
if (int32) {
// int32-slots.ts keeps a field int32 only when no dynamic store in
@@ -375,7 +387,12 @@ export function streamTypedRefCommitAdapter(
` %f${index}_pay = load ptr, ptr %f${index}_payp`,
` %f${index}_sel = select i1 %f${index}_isref, ptr %f${index}_pay, ptr null`,
` %f${index}_new = call ptr ${rc.retain}(ptr %f${index}_sel)`,
` store ptr %f${index}_new, ptr %f${index}_ptr`,
...(compressed
? [
...compactRefStoreLines(`%f${index}_new`, `%f${index}_newidx`, `%f${index}_ne`),
` store i32 %f${index}_newidx, ptr %f${index}_ptr`,
]
: [` store ptr %f${index}_new, ptr %f${index}_ptr`]),
` call void @scr_union_release(ptr %${next})`,
` call void ${rc.release}(ptr %f${index}_old)`,
` br label %${after}`,
@@ -398,6 +415,11 @@ export function streamTypedRefCommitAdapter(
` %f${index}_stored = zext i1 %${next} to i8`,
` store i8 %f${index}_stored, ptr %f${index}_ptr`,
);
} else if (compressed) {
lines.push(
...compactRefStoreLines(`%${next}`, `%f${index}_nextidx`, `%f${index}_nx`),
` store i32 %f${index}_nextidx, ptr %f${index}_ptr`,
);
} else {
lines.push(` store ${fieldTy} %${next}, ptr %f${index}_ptr`);
}
@@ -832,7 +854,7 @@ export function streamTypedRefMaterializeAdapter(
B.condBr(present, yes, afterField);
B.startBlock(yes);
}
const { index } = classFieldIndex(meta, field.name);
const { index, compressed } = classFieldIndex(meta, field.name, "compressed");
const fieldPtr = B.tmp();
let fieldValue = B.tmp();
B.line(
@@ -844,7 +866,12 @@ export function streamTypedRefMaterializeAdapter(
B.line(`${fieldValue} = sitofp i32 ${raw} to double`);
} else if (host.inlineUnions.classField(t.className, field.name))
fieldValue = loadStructField(B, host.sizeType, fieldPtr, field.type, true);
else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`);
else if (compressed) {
const idx = B.tmp();
B.line(`${idx} = load i32, ptr ${fieldPtr}`);
for (const line of compactRefLoadLines(idx, fieldValue, `${fieldValue}.r`))
B.line(line.trimStart());
} else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`);
if (llFieldType(field.type, false) === "i8") {
const boolValue = B.tmp();
B.line(`${boolValue} = trunc i8 ${fieldValue} to i1`);
@@ -15,23 +15,55 @@ export function streamDataAdapter(
sym = `sc_ee_dad_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
const p = cbT.params[0];
if (
cbT.params.length > 1 ||
(p && p.kind !== "bytes" && p.kind !== "string" && p.kind !== "dyn" && p.kind !== "union")
) {
if (cbT.params.length > 1) {
throw new InternalCompilerError(
"llvm emitter bug: stream data listener param shape (frontend must fence)",
);
}
// A `Buffer | string` union wraps the filled slot; any other union (or
// other type) converts from the chunk's checked-dynamic value.
const bytesOrString =
p?.kind === "union" &&
(() => {
const def = host.unionsById.get(p.unionId);
return (
def !== undefined &&
!host.nullableUnions.has(p.unionId) &&
def.arms.length === 2 &&
def.arms.some((a) => a.kind === "bytes" && a.elem === "u8") &&
def.arms.some((a) => a.kind === "string")
);
})();
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
host.declare(`declare void @scr_closure_release(ptr)`);
// The emit ABI's three slots (scr_stream_emit_data): the bytes chunk,
// the string chunk, and an object-mode entry's value.
const d: string[] = [
`define internal void @${sym}(ptr %cb, ptr %a0, ptr %a1) ${FN_ATTRS} { ; stream 'data' adapter ${typeKey(cbT)}`,
`define internal void @${sym}(ptr %cb, ptr %a0, ptr %a1, ptr %a2) ${FN_ATTRS} { ; stream 'data' adapter ${typeKey(cbT)}`,
`entry:`,
` %capsp = getelementptr inbounds %ScrClosure, ptr %cb, i64 1`,
` %bx = load ptr, ptr %capsp`,
` %orig = call ptr @scr_box_get_ref(ptr %bx) ; the listener, +1`,
];
const finishTyped = (arg: string, argTy: string): void => {
d.push(
` %fnp = getelementptr inbounds %ScrClosure, ptr %orig, i64 0, i32 1`,
` %fn = load ptr, ptr %fnp`,
);
const retTy = host.llType(cbT.ret);
const argList = `ptr %orig, ${argTy} ${arg}`;
if (retTy === "void") {
d.push(` call void %fn(${argList})`);
} else {
d.push(` %ret = call ${retTy} %fn(${argList})`);
if (isRefCounted(cbT.ret)) {
d.push(
` call void ${releaseSym(host.shapeHost, cbT.ret)}(ptr %ret) ; discarded listener result`,
);
}
}
d.push(` call void @scr_closure_release(ptr %orig)`, ` ret void`);
};
const finish = (arg: string | null): void => {
d.push(
` %fnp = getelementptr inbounds %ScrClosure, ptr %orig, i64 0, i32 1`,
@@ -57,8 +89,8 @@ export function streamDataAdapter(
} else if (p.kind === "bytes" || p.kind === "string") {
const msg =
p.kind === "bytes"
? "a 'data' listener declaring a Buffer chunk received a string (the stream has an encoding set)"
: "a 'data' listener declaring a string chunk received a Buffer (call setEncoding, or declare the chunk as a Buffer)";
? "a 'data' listener declaring a Buffer chunk received a string or an object-mode value (the stream has an encoding set, or is in object mode)"
: "a 'data' listener declaring a string chunk received a Buffer or an object-mode value (call setEncoding, or declare the chunk's type)";
const slot = p.kind === "bytes" ? "%a0" : "%a1";
host.declare(`declare void @scr_throw_error_msg(i32, ptr, ${host.sizeType})`);
d.push(
@@ -73,7 +105,7 @@ export function streamDataAdapter(
);
finish("%r0");
d.push(`}`, ``);
} else if (p.kind === "union") {
} else if (p.kind === "union" && bytesOrString) {
// A `Buffer | string` chunk: wrap whichever slot the stream filled at
// its arm of the (boxed) union.
const def = host.unionsById.get(p.unionId);
@@ -94,6 +126,9 @@ export function streamDataAdapter(
host.declare(`declare void @scr_bytes_release_v(ptr)`);
host.declare(`declare ptr @scr_str_retain_v(ptr)`);
host.declare(`declare void @scr_str_release_v(ptr)`);
host.declare(`declare void @scr_throw_error_msg(i32, ptr, ${host.sizeType})`);
const unionMsg =
"a 'data' listener declaring a Buffer | string chunk received an object-mode value that is neither (declare the chunk's type)";
d.push(
` %uslot = alloca ptr`,
` %isb = icmp ne ptr %a0, null`,
@@ -104,6 +139,15 @@ export function streamDataAdapter(
` store ptr %ub, ptr %uslot`,
` br label %go`,
`str:`,
// an object-mode value that is neither: the catchable error the
// one-type adapters throw
` %nos = icmp eq ptr %a1, null`,
` br i1 %nos, label %bad, label %strok`,
`bad:`,
` call void @scr_throw_error_msg(i32 1, ptr ${host.cstr(unionMsg)}, ${host.sizeType} ${Buffer.byteLength(unionMsg, "utf8")})`,
` call void @scr_closure_release(ptr %orig)`,
` ret void`,
`strok:`,
` %rs = call ptr @scr_str_retain_v(ptr %a1)`,
` %us = call ptr @scr_union_new_ref(i32 ${strTag}, ptr %rs, ptr @scr_str_retain_v, ptr @scr_str_release_v, ptr null)`,
` store ptr %us, ptr %uslot`,
@@ -114,11 +158,20 @@ export function streamDataAdapter(
finish("%uv");
d.push(`}`, ``);
} else {
// dyn: box by runtime tag — the JS lane's adapter parameter.
// dyn: the object-mode value, else box by runtime tag — the JS lane's
// adapter parameter. Any other type converts from that value.
host.declare(`declare ptr @scr_dyn_new_buffer(ptr)`);
host.declare(`declare ptr @scr_dyn_new_str(ptr)`);
host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
d.push(
` %dslot = alloca ptr`,
` %isv = icmp ne ptr %a2, null`,
` br i1 %isv, label %val, label %chunk`,
`val:`,
` %dvv = call ptr @scr_dyn_retain_v(ptr %a2)`,
` store ptr %dvv, ptr %dslot`,
` br label %go`,
`chunk:`,
` %isb = icmp ne ptr %a0, null`,
` br i1 %isb, label %buf, label %str`,
`buf:`,
@@ -132,7 +185,25 @@ export function streamDataAdapter(
`go:`,
` %dv = load ptr, ptr %dslot`,
);
finish("%dv");
if (p.kind === "dyn") {
finish("%dv");
} else {
const helper = host.dyn.dynCheckHelper(p, false);
const ty = host.llType(p);
host.declare(`declare void @scr_dyn_release(ptr)`);
host.declare(`declare zeroext i1 @scr_exc_pending()`);
d.push(
` %tv = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %dv, ptr null)`,
` call void @scr_dyn_release(ptr %dv)`,
` %pend = call zeroext i1 @scr_exc_pending()`,
` br i1 %pend, label %fail, label %conv`,
`fail:`,
` call void @scr_closure_release(ptr %orig)`,
` ret void`,
`conv:`,
);
finishTyped("%tv", ty);
}
d.push(`}`, ``);
}
host.resolveThunkDefs.push(...d);
@@ -164,12 +235,25 @@ export function streamDoneFnFor(
const dataT = kind === "t" || kind === "l" ? doneT.params[1] : undefined;
let bytesTag = -1;
let strTag = -1;
// data of any type other than a union of Buffer, string, undefined and
// null (an object-mode Transform's value) completes through the
// value entry points with the data boxed.
let valueData = false;
if (dataT !== undefined) {
if (dataT.kind !== "union")
throw new InternalCompilerError("llvm emitter bug: stream done data param not a union");
const def = host.unionsById.get(dataT.unionId);
bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes") : -1;
strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
const def = dataT.kind === "union" ? host.unionsById.get(dataT.unionId) : undefined;
valueData =
def === undefined ||
!def.arms.every(
(a) =>
(a.kind === "bytes" && a.elem === "u8") ||
a.kind === "string" ||
a.kind === "undefinedT" ||
a.kind === "nullT",
);
if (!valueData) {
bytesTag = def!.arms.findIndex((a) => a.kind === "bytes");
strTag = def!.arms.findIndex((a) => a.kind === "string");
}
}
const entry =
kind === "w"
@@ -179,10 +263,16 @@ export function streamDoneFnFor(
: kind === "d"
? "scr_stream_destroy_done"
: kind === "t"
? "scr_stream_transform_done"
: "scr_stream_flush_done";
const twoSlot = kind === "t" || kind === "l";
host.declare(`declare void @${entry}(ptr, ptr${twoSlot ? ", ptr, ptr" : ""})`);
? valueData
? "scr_stream_transform_done_value"
: "scr_stream_transform_done"
: valueData
? "scr_stream_flush_done_value"
: "scr_stream_flush_done";
const twoSlot = (kind === "t" || kind === "l") && !valueData;
host.declare(
`declare void @${entry}(ptr, ptr${twoSlot ? ", ptr, ptr" : valueData ? ", ptr" : ""})`,
);
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
host.declare(`declare void @scr_stream_release_v(ptr)`);
host.declare(`declare ptr @scr_error_retain_v(ptr)`);
@@ -190,7 +280,7 @@ export function streamDoneFnFor(
const params = [
"ptr %self",
...(errT !== undefined ? ["ptr %e"] : []),
...(dataT !== undefined ? ["ptr %d"] : []),
...(dataT !== undefined ? [`${host.llType(dataT)} %d`] : []),
];
const d: string[] = [
`define internal void @${sym}(${params.join(", ")}) ${FN_ATTRS} { ; stream '${kind}' done ${typeKey(doneT)}`,
@@ -234,7 +324,15 @@ export function streamDoneFnFor(
unwrap("%d", strTag, "@scr_str_retain_v", "%sslot");
}
d.push(` %ev = load ptr, ptr %eslot`);
if (twoSlot) {
if (valueData) {
const conv = host.dyn.toDynHelper(dataT!);
host.declare(`declare void @scr_dyn_release(ptr)`);
d.push(
` %dv = call ptr @${conv}(${host.llType(dataT!)} %d)`,
` call void @${entry}(ptr %s, ptr %ev, ptr %dv) ; moves err; borrows s and the value`,
` call void @scr_dyn_release(ptr %dv)`,
);
} else if (twoSlot) {
d.push(
` %bv = load ptr, ptr %bslot`,
` %sv = load ptr, ptr %sslot`,
@@ -245,7 +343,8 @@ export function streamDoneFnFor(
}
d.push(` call void @scr_stream_release_v(ptr %s)`);
if (errT !== undefined) d.push(` call void ${releaseSym(host.shapeHost, errT)}(ptr %e)`);
if (dataT !== undefined) d.push(` call void ${releaseSym(host.shapeHost, dataT)}(ptr %d)`);
if (dataT !== undefined && isRefCounted(dataT))
d.push(` call void ${releaseSym(host.shapeHost, dataT)}(ptr %d)`);
d.push(` ret void`, `}`, ``);
host.resolveThunkDefs.push(...d);
return sym;
@@ -611,18 +710,14 @@ export function streamCbThunkFor(
: pos === 1;
if (p.kind === "dyn") {
if (isChunkPos) {
host.declare(`declare ptr @scr_dyn_new_buffer(ptr)`);
d.push(` %dc${i} = call ptr @scr_dyn_new_buffer(ptr %chunk)`);
// the chunk's value: a Buffer, or an object-mode side's value
host.declare(`declare ptr @scr_stream_chunk_value(ptr, ptr)`);
d.push(` %dc${i} = call ptr @scr_stream_chunk_value(ptr %s, ptr %chunk)`);
passed.push(`ptr %dc${i}`);
} else if (isEncPos) {
host.declare(`declare ptr @scr_str_new(ptr, ${host.sizeType})`);
host.declare(`declare ptr @scr_dyn_new_str(ptr)`);
host.declare(`declare void @scr_str_release(ptr)`);
d.push(
` %es${i} = call ptr @scr_str_new(ptr ${host.cstr("buffer")}, ${host.sizeType} 6)`,
` %ed${i} = call ptr @scr_dyn_new_str(ptr %es${i})`,
` call void @scr_str_release(ptr %es${i})`,
);
// 'buffer', or the encoding argument an object-mode write passed
host.declare(`declare ptr @scr_stream_chunk_encoding_value(ptr)`);
d.push(` %ed${i} = call ptr @scr_stream_chunk_encoding_value(ptr %s)`);
passed.push(`ptr %ed${i}`);
} else if (isErrPos) {
// finished/pipeline succeed with UNDEFINED (Node calls the eos
@@ -673,15 +768,71 @@ export function streamCbThunkFor(
}
continue;
}
if (isChunkPos) {
host.declare(`declare ptr @scr_bytes_retain_v(ptr)`);
d.push(` %rc${i} = call ptr @scr_bytes_retain_v(ptr %chunk)`);
if (isChunkPos && p.kind === "bytes") {
// A Buffer, which an object-mode side's value must be (else a
// TypeError, thrown out of the write).
host.declare(`declare ptr @scr_stream_chunk_bytes(ptr, ptr)`);
d.push(
` %rc${i} = call ptr @scr_stream_chunk_bytes(ptr %s, ptr %chunk)`,
` %rcbad${i} = icmp eq ptr %rc${i}, null`,
` br i1 %rcbad${i}, label %rcfail${i}, label %rcok${i}`,
`rcfail${i}:`,
...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []),
` ret void`,
`rcok${i}:`,
);
if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`);
passed.push(`ptr %rc${i}`);
} else if (isEncPos) {
// Node's encoding for decoded (Buffer) chunks is 'buffer'.
host.declare(`declare ptr @scr_str_new(ptr, ${host.sizeType})`);
d.push(` %en${i} = call ptr @scr_str_new(ptr ${host.cstr("buffer")}, ${host.sizeType} 6)`);
} else if (isChunkPos) {
// Any other declared type converts from the chunk's value (an
// object-mode side's value, or the Buffer), or throws out of the
// write.
const helper = host.dyn.dynCheckHelper(p, false);
const ty = host.llType(p);
host.declare(`declare ptr @scr_stream_chunk_value(ptr, ptr)`);
host.declare(`declare void @scr_dyn_release(ptr)`);
host.declare(`declare zeroext i1 @scr_exc_pending()`);
if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`);
d.push(
` %cv${i} = call ptr @scr_stream_chunk_value(ptr %s, ptr %chunk)`,
` %ct${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %cv${i}, ptr null)`,
` call void @scr_dyn_release(ptr %cv${i})`,
` %ctbad${i} = call zeroext i1 @scr_exc_pending()`,
` br i1 %ctbad${i}, label %ctfail${i}, label %ctok${i}`,
`ctfail${i}:`,
...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []),
` ret void`,
`ctok${i}:`,
);
passed.push(`${ty} %ct${i}`);
} else if (isEncPos && p.kind === "string") {
// Node's encoding for decoded (Buffer) chunks is 'buffer'; an
// object-mode side passes the write's encoding argument ("" for
// undefined and null).
host.declare(`declare ptr @scr_stream_chunk_encoding(ptr)`);
d.push(` %en${i} = call ptr @scr_stream_chunk_encoding(ptr %s)`);
passed.push(`ptr %en${i}`);
} else if (isEncPos) {
// Another declared type (string | null | undefined) converts from
// the encoding argument's value.
const helper = host.dyn.dynCheckHelper(p, false);
const ty = host.llType(p);
host.declare(`declare ptr @scr_stream_chunk_encoding_value(ptr)`);
host.declare(`declare void @scr_dyn_release(ptr)`);
host.declare(`declare zeroext i1 @scr_exc_pending()`);
if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`);
d.push(
` %ev${i} = call ptr @scr_stream_chunk_encoding_value(ptr %s)`,
` %et${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %ev${i}, ptr null)`,
` call void @scr_dyn_release(ptr %ev${i})`,
` %etbad${i} = call zeroext i1 @scr_exc_pending()`,
` br i1 %etbad${i}, label %etfail${i}, label %etok${i}`,
`etfail${i}:`,
...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []),
` ret void`,
`etok${i}:`,
);
passed.push(`${ty} %et${i}`);
} else if (isErrPos) {
// destroy's error argument: `Error | null` — wrap type-directedly.
// The finished/pipeline callback ("e") may declare `Error | null |
@@ -969,6 +969,64 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
return host.emitGenericLibCall(e);
}
/** Math.pow(2, n): for an integer n from -1074 to 1023 the result is the
* power of two whose bits are its exponent field (or, below -1022, one
* mantissa bit of a subnormal), the value pow returns. Other exponents call
* scr_math_pow. */
function emitPowerOfTwo(host: LlvmEmitterContext, n: string): string {
const B = host.B;
const check = B.newLabel("pow2.check");
const exact = B.newLabel("pow2.exact");
const call = B.newLabel("pow2.call");
const join = B.newLabel("pow2.join");
const low = B.tmp();
const high = B.tmp();
const inRange = B.tmp();
B.line(`${low} = fcmp oge double ${n}, ${f64Lit(-1074)}`);
B.line(`${high} = fcmp ole double ${n}, ${f64Lit(1023)}`);
B.line(`${inRange} = and i1 ${low}, ${high}`);
B.condBr(inRange, check, call);
B.startBlock(check);
const int = B.tmp();
const back = B.tmp();
const integral = B.tmp();
B.line(`${int} = fptosi double ${n} to i32`);
B.line(`${back} = sitofp i32 ${int} to double`);
B.line(`${integral} = fcmp oeq double ${back}, ${n}`);
B.condBr(integral, exact, call);
B.startBlock(exact);
const normal = B.tmp();
const biased = B.tmp();
const biasedWide = B.tmp();
const normalBits = B.tmp();
const shift = B.tmp();
const shiftWide = B.tmp();
const subnormalBits = B.tmp();
const bits = B.tmp();
const power = B.tmp();
B.line(`${normal} = icmp sge i32 ${int}, -1022`);
B.line(`${biased} = add i32 ${int}, 1023`);
B.line(`${biasedWide} = zext i32 ${biased} to i64`);
B.line(`${normalBits} = shl i64 ${biasedWide}, 52`);
B.line(`${shift} = add i32 ${int}, 1074`);
B.line(`${shiftWide} = zext i32 ${shift} to i64`);
B.line(`${subnormalBits} = shl i64 1, ${shiftWide}`);
B.line(`${bits} = select i1 ${normal}, i64 ${normalBits}, i64 ${subnormalBits}`);
B.line(`${power} = bitcast i64 ${bits} to double`);
const exactEnd = B.currentLabel();
B.br(join);
B.startBlock(call);
host.declare(`declare double @scr_math_pow(double, double)`);
const called = B.tmp();
B.line(`${called} = call double @scr_math_pow(double ${f64Lit(2)}, double ${n})`);
const callEnd = B.currentLabel();
B.br(join);
B.startBlock(join);
const out = B.tmp();
B.line(`${out} = phi double [ ${power}, %${exactEnd} ], [ ${called}, %${callEnd} ]`);
return out;
}
export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
const B = host.B;
if (e.fn === "math.min" || e.fn === "math.max") {
@@ -986,6 +1044,10 @@ export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr):
const v = host.emitExpr(e.args[0]!);
return { name: emitTruncF64(host, v.name), type: e.type };
}
if (e.fn === "math.pow" && e.args[0]!.kind === "numLit" && e.args[0]!.value === 2) {
const v = host.emitExpr(e.args[1]!);
return { name: emitPowerOfTwo(host, v.name), type: e.type };
}
if (e.fn === "math.floor" || e.fn === "math.ceil") {
const intr = e.fn === "math.floor" ? "floor" : "ceil";
const v = host.emitExpr(e.args[0]!);
@@ -924,32 +924,41 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
}
if (e.fn === "emitter.emitData") {
// A user emit('data', chunk) on a stream-rooted receiver: fill the
// matching payload slot of the two-slot 'data' ABI, NULL the other.
// matching payload slot of the three-slot 'data' ABI (bytes, string,
// object-mode value), NULL the others; a checked-dynamic chunk fills
// them by tag (scr_stream_emit_value).
const args = e.args.map((a) => host.emitExpr(a));
const chunkT = e.args[2]!.type;
const both =
chunkT.kind === "string"
? [`ptr null`, `ptr ${args[2]!.name}`]
: [`ptr ${args[2]!.name}`, `ptr null`];
host.declare(`declare zeroext i1 @scr_emitter_emit(ptr, ptr, ...)`);
const t = B.tmp();
B.line(
`${t} = call zeroext i1 (ptr, ptr, ...) @scr_emitter_emit(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ${both.join(", ")})`,
);
if (chunkT.kind === "dyn") {
host.declare(`declare zeroext i1 @scr_stream_emit_value(ptr, ptr, ptr)`);
B.line(
`${t} = call zeroext i1 @scr_stream_emit_value(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name})`,
);
} else {
const slots =
chunkT.kind === "string"
? [`ptr null`, `ptr ${args[2]!.name}`, `ptr null`]
: [`ptr ${args[2]!.name}`, `ptr null`, `ptr null`];
host.declare(`declare zeroext i1 @scr_emitter_emit(ptr, ptr, ...)`);
B.line(
`${t} = call zeroext i1 (ptr, ptr, ...) @scr_emitter_emit(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ${slots.join(", ")})`,
);
}
host.emitPendingCheck();
return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type };
}
if (e.fn === "emitter.onData" || e.fn === "emitter.onDataDyn") {
// The stream-'data' registration: same runtime entries as
// emitter.on/onDyn, but the DATA adapter (the two-slot payload ABI
// — scr_stream_emit_data) behind the arity-2 fixed shim.
// emitter.on/onDyn, but the DATA adapter (the three-slot payload ABI
// — scr_stream_emit_data) behind the arity-3 fixed shim.
const isDyn = e.fn === "emitter.onDataDyn";
const cbT = e.args[isDyn ? 3 : 2]!.type;
if (cbT.kind !== "func")
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} listener not a func`);
const args = e.args.map((a) => host.emitExpr(a));
const adapterFn = host.streamDataAdapter(cbT);
host.declare(`declare void @scr_ee_inv_fixed2(ptr, ptr)`);
host.declare(`declare void @scr_ee_inv_fixed3(ptr, ptr)`);
const t = B.tmp();
if (isDyn) {
host.toHeap(args[3]!);
@@ -959,7 +968,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
);
B.line(
`${t} = call ptr @scr_emitter_on_dyn(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ` +
`ptr ${args[2]!.name}, ptr ${wrapped}, ptr @scr_ee_inv_fixed2, i1 ${args[4]!.name}, i1 ${args[5]!.name})`,
`ptr ${args[2]!.name}, ptr ${wrapped}, ptr @scr_ee_inv_fixed3, i1 ${args[4]!.name}, i1 ${args[5]!.name})`,
);
} else {
host.declare(`declare ptr @scr_closure_retain_v(ptr)`);
@@ -972,7 +981,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
);
B.line(
`${t} = call ptr @scr_emitter_on_via(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ` +
`ptr ${args[2]!.name}, ptr ${wrapped}, ptr @scr_ee_inv_fixed2, i1 ${args[3]!.name}, i1 ${args[4]!.name})`,
`ptr ${args[2]!.name}, ptr ${wrapped}, ptr @scr_ee_inv_fixed3, i1 ${args[3]!.name}, i1 ${args[4]!.name})`,
);
}
const out = host.own({ name: t, type: e.type });
@@ -448,6 +448,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"buffer.poolFromArray": "scr_buffer_from_arr",
"buffer.poolFromDyn": "scr_buffer_from_dyn_pooled",
"buffer.poolFromDynRef": "scr_buffer_from_dyn_ref_pooled",
"buffer.poolFromDynOffset": "scr_buffer_from_dyn_offset_pooled",
"buffer.poolSize": "scr_buffer_pool_size",
"buffer.setPoolSize": "scr_buffer_set_pool_size",
"buffer.byteLenDyn": "scr_buffer_byte_length_dyn",
@@ -577,6 +578,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"crypto.cipherNew": "scr_crypto_cipher_new",
"crypto.ecdhNew": "scr_crypto_ecdh_new",
"crypto.ecdhConvertKey": "scr_crypto_ecdh_convert_key",
"crypto.dhNew": "scr_crypto_dh_new",
"crypto.dhGroupNew": "scr_crypto_dh_group_new",
"crypto.hmacNewStr": "scr_crypto_hmac_new_str",
"crypto.hmacNewBytes": "scr_crypto_hmac_new_bytes",
"crypto.hashUpdateStr": "scr_crypto_hash_update_str",
@@ -614,11 +617,20 @@ export const LIB_FN_SYMS: Record<string, string> = {
"crypto.generateKeySync": "scr_crypto_generate_key_sync",
"crypto.keyObjectInput": "scr_crypto_key_object_input",
"crypto.isKeyObject": "scr_crypto_is_key_object",
"webcrypto.generateKey": "scr_webcrypto_generate_key",
"webcrypto.importKey": "scr_webcrypto_import_key",
"webcrypto.exportKey": "scr_webcrypto_export_key",
"webcrypto.encrypt": "scr_webcrypto_encrypt",
"webcrypto.decrypt": "scr_webcrypto_decrypt",
"webcrypto.wrapKey": "scr_webcrypto_wrap_key",
"webcrypto.unwrapKey": "scr_webcrypto_unwrap_key",
"webcrypto.deriveBits": "scr_webcrypto_derive_bits",
"webcrypto.deriveKey": "scr_webcrypto_derive_key",
"webcrypto.sign": "scr_webcrypto_sign",
"webcrypto.verify": "scr_webcrypto_verify",
"webcrypto.digest": "scr_webcrypto_digest",
"webcrypto.isCryptoKey": "scr_webcrypto_is_crypto_key",
"webcrypto.object": "scr_webcrypto_object",
"crypto.randomUUIDv7": "scr_crypto_random_uuid_v7",
"crypto.pbkdf2": "scr_crypto_pbkdf2",
"crypto.hkdf": "scr_crypto_hkdf",
@@ -754,6 +766,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"procStream.write": "scr_proc_stream_write",
"stream.stdioWritable": "scr_stream_stdio_writable",
"stream.unboxDyn": "scr_stream_unbox_dyn",
"stream.handleStream": "scr_stream_handle",
"stream.pipeDyn": "scr_stream_pipe_dyn",
"stream.unpipeDyn": "scr_stream_unpipe_dyn",
"process.kill": "scr_process_kill_named",
@@ -999,12 +1012,15 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.nativeSetIs": "scr_dyn_native_set_is",
"dyn.nativeMapIs": "scr_dyn_native_map_is",
"dyn.nativeUrlIs": "scr_dyn_native_url_is",
"dyn.nativeDateIs": "scr_dyn_native_date_is",
"dyn.nativeDateIs": "scr_dyn_date_instance_is",
"dyn.nativeDateHandleIs": "scr_dyn_native_date_is",
"dyn.globalConstructor": "scr_dyn_global_constructor",
"dyn.builtinFunction": "scr_dyn_builtin_function",
"date.nativeNew": "scr_dyn_native_date_new",
"date.nativeNewFields": "scr_dyn_native_date_new_fields",
"date.nativeNewValue": "scr_dyn_native_date_new_value",
"date.argumentsTime": "scr_dyn_date_arguments_time",
"date.classOperation": "scr_date_class_operation_install",
"date.checkedValue": "scr_dyn_native_date_value",
"dyn.nativeRegexIs": "scr_dyn_native_regex_is",
"dyn.toString": "scr_dyn_to_string_argument",
@@ -1029,6 +1045,14 @@ export const LIB_FN_SYMS: Record<string, string> = {
"crypto.x509ValidFromStr": "scr_crypto_x509_valid_from_str",
"crypto.x509ValidTo": "scr_crypto_x509_valid_to",
"crypto.x509ValidToStr": "scr_crypto_x509_valid_to_str",
"crypto.createPrivateKey": "scr_crypto_create_private_key",
"crypto.createPublicKey": "scr_crypto_create_public_key",
"crypto.generateKeyPairSync": "scr_crypto_generate_key_pair_sync",
"crypto.generateKeyPairCb": "scr_crypto_generate_key_pair_async",
"crypto.signOneshot": "scr_crypto_sign_oneshot",
"crypto.verifyOneshot": "scr_crypto_verify_oneshot",
"crypto.createSign": "scr_crypto_create_sign",
"crypto.createVerify": "scr_crypto_create_verify",
"fs.writeFileModeSync": "scr_fs_write_file_mode",
"fs.chownSync": "scr_fs_chown",
"fs.lchownSync": "scr_fs_lchown",
@@ -1039,6 +1063,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
"atomics.op": "scr_atomics_op",
"atomics.notify": "scr_atomics_notify",
"atomics.wait": "scr_atomics_wait",
"atomics.opDyn": "scr_atomics_op_dyn",
"atomics.waitDyn": "scr_atomics_wait_dyn",
"atomics.notifyDyn": "scr_atomics_notify_dyn",
"worker.new": "scr_worker_new",
"worker.isMainThread": "scr_context_is_main",
"worker.threadId": "scr_context_thread_number",
@@ -1100,10 +1127,13 @@ export const LIB_FN_SYMS: Record<string, string> = {
"readable.pushStr": "scr_stream_push_str",
"readable.pushStrEnc": "scr_stream_push_str_enc",
"readable.pushEncoding": "scr_stream_set_push_encoding",
"stream.objectMode": "scr_stream_object_mode",
"stream.chunkDone": "scr_stream_chunk_done",
"readable.pushNull": "scr_stream_push_null",
"readable.pushDyn": "scr_stream_push_dyn",
"readable.unshift": "scr_stream_unshift",
"readable.unshiftStr": "scr_stream_unshift_str",
"readable.unshiftDyn": "scr_stream_unshift_dyn",
"readable.pause": "scr_stream_pause",
"readable.resume": "scr_stream_resume",
"readable.isPaused": "scr_stream_is_paused",
@@ -1480,6 +1510,10 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.arrayPrototype": "scr_dyn_array_prototype",
"dyn.arrayConstructor": "scr_dyn_array_constructor",
"dyn.objectPrototype": "scr_dyn_object_prototype",
"dyn.errorPrototype": "scr_dyn_error_prototype",
"dyn.inheritsErrorPrototype": "scr_dyn_inherits_error_prototype",
"dyn.errorCaptureStackTrace": "scr_dyn_error_capture_stack_trace",
"dyn.errorCaptureStackTraceFunction": "scr_dyn_error_capture_stack_trace_function",
"dyn.functionApply": "scr_dyn_function_apply",
"dyn.builtinMethod": "scr_dyn_builtin_method",
"dyn.classPrototype": "scr_dyn_class_prototype",
@@ -1567,6 +1601,7 @@ export function mockTimerSym(
}
export const USES_TIMERS_LIB_FNS = new Set<string>([
"crypto.generateKeyPairCb",
"process.emitWarning",
// Constructor-function streams defer their events on the stream ticks.
"dyn.builtinStreamClass",
@@ -1575,6 +1610,13 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
// ecdh.setPublicKey reports DEP0031 on the next tick.
"crypto.ecdhNew",
// WebCrypto results settle through the deferred crypto queue.
"webcrypto.generateKey",
"webcrypto.encrypt",
"webcrypto.decrypt",
"webcrypto.wrapKey",
"webcrypto.unwrapKey",
"webcrypto.deriveBits",
"webcrypto.deriveKey",
"webcrypto.sign",
"webcrypto.verify",
"webcrypto.digest",
@@ -1645,6 +1687,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"readable.pushDyn",
"readable.unshift",
"readable.unshiftStr",
"readable.unshiftDyn",
"readable.read",
"readable.readDyn",
"readable.setEncoding",
@@ -1676,6 +1719,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"console.write",
"writable.writeU",
"writable.writeDyn",
"writable.writeEnc",
"writable.end",
"writable.uncork",
"stream.destroy",
@@ -2,6 +2,7 @@
import { InternalCompilerError } from "../../errors.js";
import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js";
import { f64Lit } from "./common.js";
import { pipelinePumpedRuns } from "../../ir/builtin-signatures.js";
const STREAM_CANONICAL_CBS: Record<string, ("r" | "w" | "f" | "d" | "t" | "l")[]> = {
readable: ["r", "d"],
@@ -418,10 +419,34 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
host.emitPendingCheck();
return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type };
}
if (e.fn === "writable.writeEnc") {
// (recv, flags, chunk[, encoding][, cb]) — flags: 1 encoding, 2
// callback (MOVES). Without an encoding the argument is undefined, or
// null when the callback took its place.
const flagsArg = e.args[1]!;
if (flagsArg.kind !== "numLit")
throw new InternalCompilerError("llvm emitter bug: writable.writeEnc flags not a literal");
const flags = flagsArg.value;
const args = e.args.map((a) => host.emitExpr(a));
let at = 3;
const enc = flags & 1 ? args[at++]!.name : writeEncNull(host, flags & 2 ? "null" : "undef");
let cb = "null";
if (flags & 2) {
host.toHeap(args[at]!);
cb = args[at]!.name;
}
host.declare(`declare zeroext i1 @scr_stream_write_enc(ptr, ptr, ptr, ptr)`);
const t = B.tmp();
B.line(
`${t} = call zeroext i1 @scr_stream_write_enc(ptr ${args[0]!.name}, ptr ${args[2]!.name}, ptr ${enc}, ptr ${cb})`,
);
host.emitPendingCheck();
return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type };
}
if (e.fn === "writable.end") {
// (recv, flags[, chunk][, cb]) — flags: 1 bytes chunk, 2 string
// chunk, 8 dyn chunk (write first, Node's end(chunk) decomposition),
// 4 callback.
// (recv, flags[, chunk][, encoding][, cb]) — flags: 1 bytes chunk, 2
// string chunk, 8 dyn chunk (write first, Node's end(chunk)
// decomposition), 16 an encoding after the dyn chunk, 4 callback.
const flagsArg = e.args[1]!;
if (flagsArg.kind !== "numLit")
throw new InternalCompilerError("llvm emitter bug: writable.end flags not a literal");
@@ -431,19 +456,27 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
let chunkB = "null";
let chunkS = "null";
let chunkD: string | null = null;
let encD = "null";
if (flags & 1) chunkB = args[at++]!.name;
else if (flags & 2) chunkS = args[at++]!.name;
else if (flags & 8) chunkD = args[at++]!.name;
if (flags & 16) encD = args[at++]!.name;
else if (chunkD !== null && flags & 4) encD = writeEncNull(host, "null");
let cbName = "null";
if (flags & 4) {
host.toHeap(args[at]!);
cbName = args[at]!.name;
}
if (chunkD !== null) {
host.declare(`declare zeroext i1 @scr_stream_write_dyn(ptr, ptr, ptr)`);
// end(chunk, cb) writes with a null encoding argument (Node's end
// shifts the callback out of it)
host.declare(`declare zeroext i1 @scr_stream_write_enc(ptr, ptr, ptr, ptr)`);
B.line(
`${B.tmp()} = call zeroext i1 @scr_stream_write_dyn(ptr ${args[0]!.name}, ptr ${chunkD}, ptr null)`,
`${B.tmp()} = call zeroext i1 @scr_stream_write_enc(ptr ${args[0]!.name}, ptr ${chunkD}, ptr ${encD}, ptr null)`,
);
// a write that throws (an unknown encoding) leaves the stream as it
// was: Node's end() throws before it ends the stream
host.emitPendingCheck();
}
host.declare(`declare ptr @scr_stream_end(ptr, ptr, ptr, ptr)`);
const t = B.tmp();
@@ -662,14 +695,14 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
e.fn === "stream.pipelineStages" ||
e.fn === "stream.pipelineStagesDyn"
) {
// (n, at, k, flags, mask, s1..sn, stage1..stagek, (next, read,
// return)?, box?, (signal, abort)?[, cb]): the streams and the stage
// closures ride stack arrays; absent groups pass null.
const [nArg, atArg, kArg, flagsArg, maskArg] = e.args;
// (n, k, positions, flags, mask, s1..sn, stage1..stagek, helpers...,
// box?, (signal, abort)?[, cb]): the streams, the stage closures and
// the helpers ride stack arrays; absent groups pass null.
const [nArg, kArg, positionsArg, flagsArg, maskArg] = e.args;
if (
nArg?.kind !== "numLit" ||
atArg?.kind !== "numLit" ||
kArg?.kind !== "numLit" ||
positionsArg?.kind !== "numLit" ||
flagsArg?.kind !== "numLit" ||
maskArg?.kind !== "numLit"
)
@@ -677,6 +710,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
const n = nArg.value;
const k = kArg.value;
const flags = flagsArg.value;
const helperCount = 3 * pipelinePumpedRuns(n + k, positionsArg.value);
const args = e.args.map((a) => host.emitExpr(a));
const packed = (from: number, count: number): string => {
if (count === 0) return "null";
@@ -691,20 +725,17 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
};
const streams = packed(5, n);
const stages = packed(5 + n, k);
let at = 5 + n + k;
const helpers = packed(5 + n + k, helperCount);
let at = 5 + n + k + helperCount;
const take = (present: boolean): string => (present ? args[at++]!.name : "null");
const hasHelpers = (flags & 8) !== 0;
const next = take(hasHelpers);
const read = take(hasHelpers);
const ret = take(hasHelpers);
const box = take((flags & 16) !== 0);
const box = take(((flags >> 1) & 3) === 2);
const signal = take((flags & 32) !== 0);
const abort = take((flags & 32) !== 0);
const head =
`double ${f64Lit(n)}, ptr ${streams}, double ${f64Lit(atArg.value)}, double ${f64Lit(k)}, ` +
`ptr ${stages}, double ${f64Lit(flags)}, double ${f64Lit(maskArg.value)}, ` +
`ptr ${next}, ptr ${read}, ptr ${ret}, ptr ${box}, ptr ${signal}, ptr ${abort}`;
const params = "double, ptr, double, double, ptr, double, double, ptr, ptr, ptr, ptr, ptr, ptr";
`double ${f64Lit(n)}, ptr ${streams}, double ${f64Lit(k)}, ptr ${stages}, ` +
`double ${f64Lit(positionsArg.value)}, double ${f64Lit(flags)}, double ${f64Lit(maskArg.value)}, ` +
`ptr ${helpers}, ptr ${box}, ptr ${signal}, ptr ${abort}`;
const params = "double, ptr, double, ptr, double, double, double, ptr, ptr, ptr, ptr";
const t = B.tmp();
if (e.fn === "sp.pipelineStages") {
host.declare(`declare ptr @scr_sp_pipeline_stages(${params})`);
@@ -772,3 +803,13 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
// adapters; the handful of union-wrapped reads use the envGet shapes.
return host.emitGenericLibCall(e);
}
/** The encoding argument a write passes when the call has none: undefined
* (a null pointer), or the null value when a callback took its place. */
function writeEncNull(host: LlvmEmitterContext, which: "null" | "undef"): string {
if (which === "undef") return "null";
host.declare(`declare ptr @scr_dyn_new_null()`);
const t = host.B.tmp();
host.B.line(`${t} = call ptr @scr_dyn_new_null()`);
return t;
}
@@ -121,6 +121,13 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu
case "tls.sockOnSecureConnect":
host.toHeap(args[1]!);
return finishRuntimeCall(host, e, "scr_tls_sock_on_secure_connect", a);
case "tls.sockOnSecure":
host.toHeap(args[1]!);
return finishRuntimeCall(host, e, "scr_tls_sock_on_secure", a);
case "tls.wrapSocket":
return finishRuntimeCall(host, e, "scr_tls_wrap_socket", [a[0]!, a[1]!, ptr()]);
case "tls.wrapSocketCtx":
return finishRuntimeCall(host, e, "scr_tls_wrap_socket", a);
case "tls.sockOnSession": {
const cb = args[1]!,
type = callbackType(cb);
@@ -925,3 +925,18 @@ test.each([
if (consumer !== "local") expect(ir).toContain("local.array.dense");
}
});
test("a dense read tests the hole-free prefix before the capacity", () => {
const module = immediateFixture();
for (const bits of [32, 64] as const) {
const ir = workBody(module, bits);
// Field 6 is `filled` and field 2 `cap`: the first branch takes the value
// path below the prefix, and only the range block reads the capacity.
const firstFilled = ir.search(/getelementptr inbounds %ScrArr, ptr %[^,]+, i32 0, i32 6\b/);
const firstCap = ir.search(/getelementptr inbounds %ScrArr, ptr %[^,]+, i32 0, i32 2\b/);
expect(firstFilled).toBeGreaterThan(0);
expect(firstCap).toBeGreaterThan(firstFilled);
expect(ir.slice(firstFilled, firstCap)).toMatch(/br i1 [^,]+, label %local\.array\.value/);
expect(ir.lastIndexOf("local.array.range", firstCap)).toBeGreaterThan(firstFilled);
}
});
@@ -12,7 +12,7 @@ import { everyStmtList } from "../../ir/traverse.js";
import { analyzeCallLifetimes, type CallLifetimes } from "./call-lifetimes.js";
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import { ReferenceEffects } from "./reference-effects.js";
import { SCR_ARR_FIELD, exactIndexLines, loadArrCount, scrArrStateAt } from "./common.js";
import { SCR_ARR_FIELD, loadArrCount, scrArrStateAt } from "./common.js";
import { exactInteger, widenInteger } from "./integer-values.js";
import { READ_FALLBACK } from "./dense-array-access.js";
@@ -472,78 +472,81 @@ function emitDenseReferenceArrayRead(
loadType: "ptr" | "i64" | "i8" = "ptr",
): void {
const B = host.B;
const range = B.newLabel("local.array.range"),
dense = B.newLabel("local.array.dense");
// The dense path uses the existing ScrArr ABI. Sparse indices and
// noncanonical numeric properties retain the runtime lookup semantics.
const capPtr = B.tmp(),
cap = B.tmp();
B.line(
`${capPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.cap}`,
);
host.markMemoryPointer(capPtr, "array:header");
loadArrCount(host, cap, capPtr, host.fieldAliasAttachment(capPtr));
let offset: string, inRange: string;
const header = (field: number): string => {
const ptr = B.tmp();
B.line(`${ptr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${field}`);
host.markMemoryPointer(ptr, "array:header");
return ptr;
};
const valueLabel = B.newLabel("local.array.value");
// `key` is the index compared with the array's counts and `offset` the
// index that addresses its slots. A double index that is not an exact
// integer, like a sparse or noncanonical one, keeps the runtime lookup
// semantics; an exact one compares as i64, which a 32-bit target does
// before truncating it.
let key: string, keyType: string, offset: string;
if (integerIndex) {
offset = integerIndex;
inRange = B.tmp();
B.line(`${inRange} = icmp ult ${host.sizeType} ${integerIndex}, ${cap}`);
key = offset = integerIndex;
keyType = host.sizeType;
} else {
// Capacity bounds the dense slots; the length check below still decides presence.
const exact = exactIndexLines(index.name, cap, host.sizeType, () => B.tmp());
for (const line of exact.lines) B.line(line);
inRange = exact.ok;
offset = exact.wide;
const raw = B.tmp(),
back = B.tmp(),
integral = B.tmp(),
exact = B.newLabel("local.array.exact");
key = B.tmp();
keyType = "i64";
B.line(`${raw} = fptosi double ${index.name} to i64`);
B.line(`${key} = freeze i64 ${raw}`);
B.line(`${back} = sitofp i64 ${key} to double`);
B.line(`${integral} = fcmp oeq double ${back}, ${index.name}`);
B.condBr(integral, exact, slow);
B.startBlock(exact);
offset = key;
if (host.sizeType !== "i64") {
offset = B.tmp();
B.line(`${offset} = trunc i64 ${exact.wide} to ${host.sizeType}`);
B.line(`${offset} = trunc i64 ${key} to ${host.sizeType}`);
}
}
B.condBr(inRange, range, slow);
B.startBlock(range);
B.br(dense);
B.startBlock(dense);
// The state and value paths below both index from `data`: one load here
// serves both.
const dataPtr = B.tmp(),
const widen = (count: string, type: string): string => {
if (type === keyType) return count;
const wide = B.tmp();
B.line(`${wide} = zext ${type} ${count} to ${keyType}`);
return wide;
};
// The state and value paths both index from `data`: one load serves both.
const dataPtr = header(SCR_ARR_FIELD.data),
data = B.tmp();
B.line(
`${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.data}`,
);
host.markMemoryPointer(dataPtr, "array:header");
B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
// Below the hole-free prefix bound every state is a present value.
const filledPtr = B.tmp(),
// Below the hole-free prefix every state is a present value, and the
// prefix is at most the length and the capacity, so an index below it needs
// no other test before the value load.
const filledPtr = header(SCR_ARR_FIELD.filled),
filled = B.tmp(),
belowFilled = B.tmp(),
lenLabel = B.newLabel("local.array.len");
B.line(
`${filledPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.filled}`,
);
host.markMemoryPointer(filledPtr, "array:header");
prefix = B.tmp();
B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
let filledSized = filled;
if (host.sizeType !== "i32") {
filledSized = B.tmp();
B.line(`${filledSized} = zext i32 ${filled} to ${host.sizeType}`);
}
B.line(`${belowFilled} = icmp ult ${host.sizeType} ${offset}, ${filledSized}`);
const valueLabel = B.newLabel("local.array.value");
B.line(`${prefix} = icmp ult ${keyType} ${key}, ${widen(filled, "i32")}`);
if (induction) {
// The state of slot i is at data + cap * (width + 1) - 1 - i, and an
// inline state check makes LLVM keep -1 - i as a second induction
// variable in the loop; above the prefix the runtime reads the slot.
B.condBr(belowFilled, valueLabel, slow);
B.condBr(prefix, valueLabel, slow);
} else {
B.condBr(belowFilled, valueLabel, lenLabel);
B.startBlock(lenLabel);
const lenPtr = B.tmp(),
const range = B.newLabel("local.array.range"),
dense = B.newLabel("local.array.dense");
B.condBr(prefix, valueLabel, range);
B.startBlock(range);
const capPtr = header(SCR_ARR_FIELD.cap),
cap = B.tmp(),
inRange = B.tmp();
loadArrCount(host, cap, capPtr, host.fieldAliasAttachment(capPtr));
B.line(`${inRange} = icmp ult ${keyType} ${key}, ${widen(cap, host.sizeType)}`);
B.condBr(inRange, dense, slow);
// Within the capacity and above the prefix, a slot below the length is
// present when its state is.
B.startBlock(dense);
const lenPtr = header(SCR_ARR_FIELD.len),
len = B.tmp(),
belowLen = B.tmp();
B.line(
`${lenPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.len}`,
);
host.markMemoryPointer(lenPtr, "array:header");
loadArrCount(host, len, lenPtr, host.fieldAliasAttachment(lenPtr));
B.line(`${belowLen} = icmp ult ${host.sizeType} ${offset}, ${len}`);
const stateLabel = B.newLabel("local.array.state");
@@ -583,6 +586,24 @@ export function emitBorrowedArrayRead(
const B = host.B;
const array = host.emitStableReceiver(read.arr, [read.index]);
const loopIndex = host.emitIntegerLoopIndex(read.index);
if (loopIndex !== null && host.provenDenseIndex(read.arr, read.index)) {
// In a dense array loop's fast copy, the entry test proved the counter
// below the array's hole-free prefix: the slot holds a present value.
const dataPtr = B.tmp(),
data = B.tmp(),
valuePtr = B.tmp(),
value = B.tmp();
B.line(
`${dataPtr} = getelementptr inbounds %ScrArr, ptr ${array.name}, i32 0, i32 ${SCR_ARR_FIELD.data}`,
);
host.markMemoryPointer(dataPtr, "array:header");
B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
B.line(`${valuePtr} = getelementptr inbounds i64, ptr ${data}, ${host.sizeType} ${loopIndex}`);
host.markMemoryPointer(valuePtr, "array:elements");
B.line(`${value} = load ptr, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
B.line(`store ptr ${value}, ptr ${slot}`);
return;
}
const index = host.emitExpr(read.index);
// A proven integer index (an int32 local, say) needs no double
// round-trip; a negative one compares above the capacity as unsigned.
+12 -5
View File
@@ -25,7 +25,7 @@ import type { IrClassDef, IrRecordShape, IrType, IrUnionDef, SrcLoc } from "../.
import { mapOf, STRING, typeKey } from "../../ir/ir.js";
import { mangleClassStruct, mangleRecordStruct } from "../mangle.js";
import { FN_ATTRS } from "./shapes.js";
import { COMPACT_ID_MASK } from "./classes.js";
import { COMPACT_ID_MASK, compactRefLoadLines } from "./classes.js";
import { shapeTagFn } from "./nullable-unions.js";
import type { InlineUnionFields } from "./inline-unions.js";
import { LlvmUnsupportedError } from "./unsupported.js";
@@ -69,8 +69,10 @@ export interface PublishClassInfo {
idInRc: boolean;
/** Instances have the 32-bit header (classes.ts LlClassMeta.compact). */
compact: boolean;
/** The class's own and inherited fields: GEP index and storage type. */
fields: { name: string; index: number; type: IrType }[];
/** The class's own and inherited fields: GEP index and storage type;
* `compressed` when the field holds a 32-bit reference (classes.ts
* LlClassMeta.compressed), which the walker decodes. */
fields: { name: string; index: number; type: IrType; compressed?: boolean }[];
}
/** scr_pub_mark's object kinds (SCR_PUB_KIND_* in scr_runtime.h), for the RC audit. */
@@ -536,7 +538,7 @@ export class PublishWalkers {
head: string,
struct: string,
traced: boolean,
members: { name: string; index: number; type: IrType }[],
members: PublishClassInfo["fields"],
path: string,
loc: SrcLoc,
classId = false,
@@ -559,7 +561,12 @@ export class PublishWalkers {
}
lines.push(
` %f${n} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${member.index} ; ${llvmComment(member.name)}`,
` %v${n} = load ptr, ptr %f${n}`,
...(member.compressed === true
? [
` %v${n}.i = load i32, ptr %f${n}`,
...compactRefLoadLines(`%v${n}.i`, `%v${n}`, `%v${n}.d`),
]
: [` %v${n} = load ptr, ptr %f${n}`]),
...this.push(`%v${n}`, fn, `${n}`, this.compactRef(member.type)),
);
n++;
@@ -118,7 +118,12 @@ export function emitReadReceiver(emitter: LlEmitter, e: IrExpr): LlValue {
const { ptr, type, inline } = emitter.recordFieldPtr(receiver.name, e.shapeId, e.field);
return { name: emitter.loadRecordField(ptr, type, inline), type: e.type };
}
const { ptr, type, inline } = emitter.classFieldPtr(receiver.name, e.className, e.field);
const { ptr, type, inline } = emitter.classFieldPtr(
receiver.name,
e.className,
e.field,
"compressed",
);
if (emitter.int32Slots.isField(e.className, e.field))
return emitter.loadInt32Field(ptr, e.type);
return { name: emitter.loadField(ptr, type, inline), type: e.type };
@@ -110,6 +110,25 @@ describe("regex literal matcher code", () => {
expect(body("a\\d{1,3}")).not.toContain("<16 x i8>");
});
test("fixed leading characters filter 16 start positions at once", () => {
// Both alternatives' first characters are tested at every offset and
// combined, and the reads reach seven characters past the block.
const text = body("agggtaaa|tttaccct", "gi");
expect(text.match(/load <16 x i8>/g)!.length).toBeGreaterThanOrEqual(16);
expect(text).toMatch(/and i16 /);
expect(text).toMatch(/or i16 /);
expect(text).toMatch(/add i32 %t\d+, 7\n/);
// A class sequence with a fixed count extends the filter.
expect(body("[cg]x{3}y")).toMatch(/and i16 /);
// An optional first character, a single first character and a
// variable-length start keep the first-character search.
expect(body("[ab]?cd")).not.toMatch(/and i16 /);
expect(body("[ab]*cd")).not.toMatch(/and i16 /);
expect(body("xy|zw")).toMatch(/and i16 /);
expect(body("[ab]|cd")).not.toMatch(/and i16 /);
expect(body("agggtaaa|tttaccct", "gi", false)).not.toContain("<16 x i8>");
});
test("loops no continuation can share push no backtrack entries", () => {
// \d+ then a dot; [^\]]+ then ]; the run at the end of the pattern.
expect(body("\\d+\\.\\d+")).not.toContain("@scr_regex_stack_grow");
@@ -589,6 +589,90 @@ function firstOf(node: RxNode): FirstInfo {
}
}
/** Positions the start filter tests and alternatives it keeps. */
const PREFIX_LENGTH = 8;
const PREFIX_ALTERNATIVES = 8;
/** The character sets a match's first characters must belong to: every
* match of the node begins with the characters of some entry of `alts`,
* one set per character. `complete` means every match is exactly as long as
* its entry (so a following node continues the sequences). Null when the
* node gives no usable condition (an alternative that can start with
* anything, or too many alternatives). Assertions add no characters: the
* result stays a necessary condition. */
interface Prefix {
alts: CharSet[][];
complete: boolean;
}
/** Each head followed by each tail, cut to PREFIX_LENGTH sets; null past
* PREFIX_ALTERNATIVES entries. */
function crossPrefixes(heads: CharSet[][], tails: CharSet[][]): CharSet[][] | null {
if (heads.length * tails.length > PREFIX_ALTERNATIVES) return null;
const out: CharSet[][] = [];
for (const head of heads)
for (const tail of tails) out.push([...head, ...tail].slice(0, PREFIX_LENGTH));
return out;
}
/** Whether no entry was cut at PREFIX_LENGTH. */
function uncut(alts: CharSet[][]): boolean {
return alts.every((alt) => alt.length < PREFIX_LENGTH);
}
function prefixOf(node: RxNode): Prefix | null {
switch (node.kind) {
case "set":
return { alts: [[node.set]], complete: true };
case "assert":
return { alts: [[]], complete: true };
case "cap":
return prefixOf(node.body);
case "alt": {
const alts: CharSet[][] = [];
let complete = true;
for (const alt of node.alts) {
const p = prefixOf(alt);
if (p === null) return null;
alts.push(...p.alts);
complete &&= p.complete;
}
return alts.length <= PREFIX_ALTERNATIVES ? { alts, complete } : null;
}
case "seq": {
let alts: CharSet[][] = [[]];
for (const item of node.items) {
const p = prefixOf(item);
if (p === null) return { alts, complete: false };
const joined = crossPrefixes(alts, p.alts);
if (joined === null) return { alts, complete: false };
alts = joined;
if (!p.complete || !uncut(alts)) return { alts, complete: false };
}
return { alts, complete: true };
}
case "rep": {
if (node.max === 0) return { alts: [[]], complete: true };
if (node.min === 0) return { alts: [[]], complete: false };
const body = prefixOf(node.body);
if (body === null) return null;
// The first `min` iterations are required; they extend the sequences
// while the body has a fixed length.
let alts = body.alts;
let covered = 1;
while (covered < node.min && body.complete && uncut(alts)) {
const joined = crossPrefixes(alts, body.alts);
if (joined === null) break;
alts = joined;
covered++;
}
const complete =
body.complete && covered === node.min && node.min === node.max && uncut(alts);
return { alts, complete };
}
}
}
/** Whether a node can match without consuming (assertions included). */
function canMatchEmpty(node: RxNode): boolean {
switch (node.kind) {
@@ -721,16 +805,74 @@ class RxGen {
return wide;
}
/** The vector plans of a start filter (prefixOf): per alternative, the
* plans of its first sets up to the first set without one. Null unless
* every alternative keeps at least two positions, where the filter is
* more selective than the first character alone. */
prefixPlans(prefix: Prefix | null): { ranges: number[][]; negate: boolean }[][] | null {
if (prefix === null || !this.vector) return null;
const plans: { ranges: number[][]; negate: boolean }[][] = [];
for (const alt of prefix.alts) {
const row: { ranges: number[][]; negate: boolean }[] = [];
for (const set of alt) {
const plan = this.vectorPlan(set);
if (plan === null) break;
row.push(plan);
}
if (row.length < 2) return null;
plans.push(row);
}
return plans.length === 0 ? null : plans;
}
/** Start positions s[index..index+16) where some alternative of the
* filter matches its first sets: an i16 mask. */
testPrefix(plans: { ranges: number[][]; negate: boolean }[][], index: string): string {
let any = "";
for (const row of plans) {
let all = "";
for (let k = 0; k < row.length; k++) {
let at = index;
if (k > 0) {
at = this.tmp();
this.line(`${at} = add i32 ${index}, ${k}`);
}
const bits = this.testVector(row[k]!, at);
if (all === "") all = bits;
else {
const both = this.tmp();
this.line(`${both} = and i16 ${all}, ${bits}`);
all = both;
}
}
if (any === "") any = all;
else {
const either = this.tmp();
this.line(`${either} = or i16 ${any}, ${all}`);
any = either;
}
}
return any;
}
/** The first-character search: from %at, store the next position whose
* character is in `set` and branch to %attempt, or branch to %nomatch. */
emitSeek(set: CharSet): void {
* character is in `set` and branch to %attempt, or branch to %nomatch.
* With a start filter (`prefix`), the vector loop tests the filter's
* first sets of each alternative at 16 start positions at once and stops
* only where one of them matches all of its sets; a match can start
* nowhere else, so the attempts that remain are the ones that can
* succeed, in the same order. */
emitSeek(set: CharSet, prefix: Prefix | null = null): void {
this.block("seek");
const a = this.tmp();
this.line(`${a} = load i32, ptr %at`);
const filter = this.prefixPlans(prefix);
const plan = this.vectorPlan(set);
// The filter reads s[i + reach + 15] at its farthest.
const reach = filter === null ? 0 : Math.max(...filter.map((row) => row.length)) - 1;
let from = "seek",
start = a;
if (plan !== null) {
if (plan !== null || filter !== null) {
const head = this.label("vsh"),
load = this.label("vsl"),
next = this.label("vsn"),
@@ -740,17 +882,21 @@ class RxGen {
this.block(head);
const i = this.tmp(),
i16 = this.tmp(),
rest = this.tmp(),
fits = this.tmp();
this.line(`${i} = phi i32 [ ${a}, %seek ], [ ${i16}, %${next} ]`);
this.line(`${i16} = add i32 ${i}, 16`);
this.line(`${fits} = icmp sle i32 ${i16}, %len`);
// The characters left (0 <= i <= len, so this cannot wrap; i + 16
// can for subjects near 2^31 characters).
this.line(`${rest} = sub i32 %len, ${i}`);
this.line(`${fits} = icmp sge i32 ${rest}, ${16 + reach}`);
this.line(`br i1 ${fits}, label %${load}, label %${tail}`);
this.block(load);
const mask = this.testVector(plan, i),
const mask = filter !== null ? this.testPrefix(filter, i) : this.testVector(plan!, i),
any = this.tmp();
this.line(`${any} = icmp ne i16 ${mask}, 0`);
this.line(`br i1 ${any}, label %${hit}, label %${next}`);
this.block(next);
this.line(`${i16} = add i32 ${i}, 16`);
this.line(`br label %${head}`);
this.block(hit);
const k = this.lowestBit(mask),
@@ -1001,10 +1147,10 @@ class RxGen {
check = this.label("chk");
this.block(entry);
const p = this.loadPos(),
end = this.tmp(),
rest = this.tmp(),
inb = this.tmp();
this.line(`${end} = add i32 ${p}, ${sets.length}`);
this.line(`${inb} = icmp sle i32 ${end}, %len`);
this.line(`${rest} = sub i32 %len, ${p}`);
this.line(`${inb} = icmp sge i32 ${rest}, ${sets.length}`);
this.line(`br i1 ${inb}, label %${check}, label %fail`);
this.block(check);
let acc = "";
@@ -1023,6 +1169,8 @@ class RxGen {
acc = both;
}
}
const end = this.tmp();
this.line(`${end} = add i32 ${p}, ${sets.length}`);
// Storing before the branch is safe: every resume sets the position.
this.storePos(end);
this.line(`br i1 ${acc}, label %${succ}, label %fail`);
@@ -1229,10 +1377,11 @@ class RxGen {
this.block(vhead);
const kv = this.tmp(),
kv16 = this.tmp(),
left = this.tmp(),
fits = this.tmp();
this.line(`${kv} = phi i32 [ 0, %${entry} ], [ ${kv16}, %${vnext} ]`);
this.line(`${kv16} = add i32 ${kv}, 16`);
this.line(`${fits} = icmp sle i32 ${kv16}, ${lim}`);
this.line(`${left} = sub i32 ${lim}, ${kv}`);
this.line(`${fits} = icmp sge i32 ${left}, 16`);
this.line(`br i1 ${fits}, label %${vload}, label %${tail}`);
this.block(vload);
const vat = this.tmp();
@@ -1242,6 +1391,7 @@ class RxGen {
this.line(`${all} = icmp eq i16 ${mask}, -1`);
this.line(`br i1 ${all}, label %${vnext}, label %${vexit}`);
this.block(vnext);
this.line(`${kv16} = add i32 ${kv}, 16`);
this.line(`br label %${vhead}`);
this.block(vexit);
const missing = this.tmp();
@@ -1769,7 +1919,10 @@ export function compileRegexMatcher(
fn.push(` store i32 %off.n, ptr %at`);
fn.push(` br label %attempt`);
} else {
gen.emitSeek(first.set);
// A start filter over the first few characters (a single first byte
// keeps memchr).
const prefix = prefixOf(root);
gen.emitSeek(first.set, gen.prefixPlans(prefix) === null ? null : prefix);
}
}
+5
View File
@@ -195,6 +195,11 @@ export function mangleAsyncAdoptCall(fnName: string): string {
export function mangleAsyncInlineAwaitCall(fnName: string): string {
return `sc_ai_${sanitize(fnName)}`;
}
/** A stackless coroutine body's `await f(...)` of a stackless async f: the
* spawn of f's fiber record, which keeps its promise inside (direct await). */
export function mangleAsyncDirectSpawn(fnName: string): string {
return `sc_dw_${sanitize(fnName)}`;
}
export function mangleTrampoline(fnName: string): string {
return `sc_tr_${sanitize(fnName)}`;
}
@@ -87,12 +87,22 @@ export interface NativeLinkFeatures {
foreignFfi: boolean;
nodeTest: boolean;
tls: boolean;
/** crypto.createCipheriv/createDecipheriv: scr_cipher.c plus the mbedTLS
* archive. */
/** crypto.createCipheriv/createDecipheriv and WebCrypto's encrypt and
* decrypt: scr_cipher.c plus the mbedTLS archive (and, with webcrypto,
* scr_webcrypto_aes.c). */
cipher: boolean;
/** crypto.createECDH and ECDH.convertKey: scr_ecdh.c plus the mbedTLS
* archive. */
ecdh: boolean;
/** crypto.createDiffieHellman, getDiffieHellman and
* createDiffieHellmanGroup: scr_dh.c plus the mbedTLS archive. */
dh: boolean;
/** Asymmetric KeyObjects, crypto.sign/verify and Sign/Verify: scr_pki.c
* plus the mbedTLS archive. */
pki: boolean;
/** Ed25519 keys and signatures with pki: scr_ed25519.c (the vendored
* libsodium Ed25519). */
ed25519: boolean;
/** perf_hooks event-loop metrics: scr_perf_loop.c. */
perfLoop: boolean;
tlsCa: boolean;
@@ -6,6 +6,10 @@ export interface CcOptions {
* LLVM backend's .ll — clang compiles IR text natively on the same
* command line, so both ride this one seat. */
cPath: string;
/** Where the program TU's bytes are read from, when that is not `cPath`: an invocation-private
* copy that a concurrent build of another program cannot replace. `cPath` stays the spelling
* clang sees (`__FILE__`, debug file names) and the one cache keys name. */
cSourcePath?: string;
/** Path of the native executable to produce. */
outPath: string;
/** Additional identity for a translation unit whose complete non-system
@@ -246,12 +250,23 @@ export interface CcOptions {
* The unit also compiles whenever `tls` does — scr_tls.c consults its
* default-set override and shared Windows-certificate enumerator. */
tlsCa?: boolean;
/** crypto.createCipheriv/createDecipheriv: compiles scr_cipher.c and links
* the mbedTLS archive (shared with tls). */
/** crypto.createCipheriv/createDecipheriv and WebCrypto's encrypt and
* decrypt: compiles scr_cipher.c (and, with webcrypto,
* scr_webcrypto_aes.c) and links the mbedTLS archive (shared with tls). */
cipher?: boolean;
/** crypto.createECDH and ECDH.convertKey: compiles scr_ecdh.c and links
* the mbedTLS archive (shared with tls). */
ecdh?: boolean;
/** crypto.createDiffieHellman, getDiffieHellman and
* createDiffieHellmanGroup: compiles scr_dh.c and links the mbedTLS
* archive (shared with tls). */
dh?: boolean;
/** Asymmetric KeyObjects and signatures: compiles scr_pki.c and links
* the mbedTLS archive. */
pki?: boolean;
/** Ed25519 with pki: compiles scr_ed25519.c (the vendored libsodium
* Ed25519). */
ed25519?: boolean;
/** crypto.subtle importKey/sign/verify/digest: compiles scr_webcrypto.c. */
webcrypto?: boolean;
/** perf_hooks event-loop metrics: compiles scr_perf_loop.c. */
@@ -114,6 +114,8 @@ interface RuntimeUnitContext {
tlsCa: boolean;
cipher: boolean;
ecdh: boolean;
dh: boolean;
pki: boolean;
lreObjects: readonly string[];
zlibObjects: readonly string[];
tlsArchive: string | null;
@@ -297,6 +299,10 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"),
unit((u) => u.opts.nodeTest, "scr_test.c"),
unit((u) => u.opts.webcrypto, "scr_webcrypto.c"),
unit((u) => u.opts.webcrypto === true && u.opts.pki === true, "scr_webcrypto_pki.c"),
unit((u) => u.opts.pki === true && u.opts.ed25519 === true, "scr_ed25519.c"),
// The crypto global's object extends scr_async_dyn.c's.
unit((u) => u.opts.webcrypto && u.opts.dynAsync, "scr_webcrypto_object.c"),
// The CA-store unit rides its own gate OR the tls one: scr_tls.c
// references its default-set override unconditionally.
unit((u) => u.tlsCa, "scr_tls_ca.c"),
@@ -308,6 +314,9 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
...(u.tls ? [u.rt(join(u.rtDir, "scr_tls.c"))] : []),
...(u.cipher ? [u.rt(join(u.rtDir, "scr_cipher.c"))] : []),
...(u.ecdh ? [u.rt(join(u.rtDir, "scr_ecdh.c"))] : []),
...(u.dh ? [u.rt(join(u.rtDir, "scr_dh.c"))] : []),
...(u.pki ? [u.rt(join(u.rtDir, "scr_pki.c"))] : []),
...(u.cipher && u.opts.webcrypto ? [u.rt(join(u.rtDir, "scr_webcrypto_aes.c"))] : []),
u.tlsArchive,
// mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h).
// The unconditional win32 libs above do not carry it.
@@ -374,10 +383,13 @@ export async function compileCInternal(
const http = (opts.http ?? false) || nativeFetch;
const tls = (opts.tls ?? false) || nativeFetch;
const tlsCa = (opts.tlsCa ?? false) || tls;
// Symmetric ciphers and ECDH link the same mbedTLS archive as TLS.
// Symmetric ciphers, ECDH and DiffieHellman link the same mbedTLS
// archive as TLS.
const cipher = opts.cipher ?? false;
const ecdh = opts.ecdh ?? false;
const mbedtls = tls || cipher || ecdh;
const dh = opts.dh ?? false;
const pki = opts.pki ?? false;
const mbedtls = tls || cipher || ecdh || dh || pki;
const driver = resolveCc();
const darwinDebugSymbols = needsDarwinDebugSymbols(
targetPlatform(driver),
@@ -483,7 +495,7 @@ export async function compileCInternal(
const [compiler, runtimeHash, programBytes] = await Promise.all([
resolvedTool(driver.argv[0] ?? "clang"),
runtimeFingerprint(rtDir),
readFile(opts.cPath),
readFile(opts.cSourcePath ?? opts.cPath),
]);
if (compiler !== null) {
const effectiveCompiler =
@@ -735,6 +747,8 @@ export async function compileCInternal(
tlsCa,
cipher,
ecdh,
dh,
pki,
lreObjects,
zlibObjects,
tlsArchive,
@@ -863,7 +877,12 @@ export async function compileCInternal(
);
try {
await materializeVendorPrerequisites(undefined, privateVendorRoot);
await buildExecutable((p) => p);
await buildExecutable(
(p) => p,
opts.cSourcePath === undefined
? {}
: { programPath: opts.cSourcePath, compilerVisibleSource: opts.cPath },
);
} finally {
await rm(privateVendorRoot, { recursive: true, force: true }).catch(() => undefined);
}
@@ -898,7 +917,9 @@ export async function compileCInternal(
localArtifact === null
? runtimeFingerprint(rtDir)
: Promise.resolve(localArtifact.runtimeHash),
localArtifact === null ? readFile(opts.cPath) : Promise.resolve(localArtifact.programBytes),
localArtifact === null
? readFile(opts.cSourcePath ?? opts.cPath)
: Promise.resolve(localArtifact.programBytes),
]);
compileMetadataStamp =
compileMetadataKey === null
@@ -967,7 +988,7 @@ export async function compileCInternal(
Promise.all([
ccVersion(driver.argv, toolchainEnv, true),
runtimeFingerprint(rtDir),
readFile(opts.cPath),
readFile(opts.cSourcePath ?? opts.cPath),
]));
} catch (error) {
// A version/fingerprint probe is an optimization boundary. If the compiler
+2 -2
View File
@@ -2,8 +2,8 @@
* of the link contract: a program object keeps this symbol undefined and the
* matching runtime defines it, so incompatible manual links fail before the
* program can start. */
export const RUNTIME_ABI_VERSION = 23 as const;
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v23" as const;
export const RUNTIME_ABI_VERSION = 27 as const;
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v27" as const;
/** Object consumption is useful today, but the complete scr_* surface may
* still change before 1.0. The versioned marker prevents accidental mixing;
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" },
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
flavors: {
@@ -125,6 +125,9 @@ const features = {
tlsCa: false,
cipher: false,
ecdh: false,
dh: false,
pki: false,
ed25519: false,
webcrypto: false,
perfLoop: false,
};
@@ -61,6 +61,9 @@ const BASE: NativeLinkFeatures = {
tlsCa: false,
cipher: false,
ecdh: false,
dh: false,
pki: false,
ed25519: false,
webcrypto: false,
perfLoop: false,
};
@@ -124,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
runtime_abi: { version: 23, marker: "scr_runtime_abi_v23" },
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
compiler: {
command: "clang",
identity: "fixture clang",
@@ -2,7 +2,11 @@ import { everyModuleNode, everyStmtList, transformStmtList } from "../ir/travers
import type { LlvmUnsupportedError } from "./llvm/emitter.js";
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
import { type IrModule, type SrcLoc } from "../ir/ir.js";
import { moduleUsesFetch } from "../ir/runtime-features.js";
import {
PKI_LIB_FNS,
moduleUsesFetch,
moduleUsesWebCryptoAsymmetric,
} from "../ir/runtime-features.js";
/** Reflection tables expose methods that the program never calls. On WASI,
* keep their host-only implementations as explicit runtime refusals. Typed
@@ -208,6 +212,19 @@ export function moduleWasiUnavailableSurface(
};
return false;
}
if (
node.fn === "webcrypto.encrypt" ||
node.fn === "webcrypto.decrypt" ||
node.fn === "webcrypto.wrapKey" ||
node.fn === "webcrypto.unwrapKey"
) {
found = {
surface:
"WebCrypto encrypt, decrypt, wrapKey and unwrapKey (the wasm32-wasi runtime does not include the mbedTLS library)",
loc,
};
return false;
}
if (node.fn === "crypto.ecdhNew" || node.fn === "crypto.ecdhConvertKey") {
found = {
surface: "ECDH (the wasm32-wasi runtime does not include the mbedTLS library)",
@@ -215,6 +232,21 @@ export function moduleWasiUnavailableSurface(
};
return false;
}
if (node.fn === "crypto.dhNew" || node.fn === "crypto.dhGroupNew") {
found = {
surface: "DiffieHellman (the wasm32-wasi runtime does not include the mbedTLS library)",
loc,
};
return false;
}
if (PKI_LIB_FNS.has(node.fn)) {
found = {
surface:
"asymmetric keys and signatures (the wasm32-wasi runtime does not include the mbedTLS library)",
loc,
};
return false;
}
if (node.fn === "os.networkInterfaces") {
found = {
surface: "network-interface enumeration (WASI Preview 1 has no interface API)",
@@ -241,5 +273,12 @@ export function moduleWasiUnavailableSurface(
if (moduleUsesFetch(mod)) {
return { surface: "network-backed fetch (WASI Preview 1 has no socket API)", loc: entryLoc };
}
if (moduleUsesWebCryptoAsymmetric(mod)) {
return {
surface:
"WebCrypto ECDSA and RSA (the wasm32-wasi runtime does not include the mbedTLS library)",
loc: entryLoc,
};
}
return null;
}
@@ -67,6 +67,9 @@ export async function warmNativeCaches(
tls: false,
cipher: false,
ecdh: false,
dh: false,
pki: false,
ed25519: false,
webcrypto: false,
perfLoop: false,
tlsCa: false,
+2 -1
View File
@@ -174,7 +174,8 @@ class ContractChecker {
return `${owner}.${method[2]}`;
}
if (fn?.sourceName !== undefined && fn.sourceName !== "<anonymous>") return fn.sourceName;
if (fn !== undefined && /^%fn\d+$/.test(fnName)) return `the function at ${this.where(fn.loc)}`;
if (fn !== undefined && /^%fn\.[0-9a-f]{12}$/.test(fnName))
return `the function at ${this.where(fn.loc)}`;
return fnName.replace(/^%/, "");
}
@@ -65,6 +65,9 @@ const native: EarlyExecutableNativeFeatures = {
tlsCa: false,
cipher: false,
ecdh: false,
dh: false,
pki: false,
ed25519: false,
webcrypto: false,
perfLoop: false,
};
@@ -133,8 +136,15 @@ test("early executable cache restores the emitted TU, IR, and native feature gat
const hit = await readEarlyExecutableCache(f.root, f.options);
expect(hit?.native).toEqual(native);
expect(hit?.executableRestored).toBe(false);
expect(await readFile(f.llvmPath, "utf8")).toBe("; generated llvm\n");
expect(await readFile(f.irPath, "utf8")).toContain("irVersion");
// The build compiles private copies beside the shared names and installs
// them there when it is done.
expect(hit?.llvmPath).toBe(f.llvmPath);
expect(hit?.irPath).toBe(f.irPath);
expect(hit?.llvmSourcePath).toMatch(/[/\\]\.scriptc-llvm-[^/\\]*\.ll$/);
expect(hit?.irSourcePath).toMatch(/[/\\]\.scriptc-ir-/);
expect(await readFile(hit!.llvmSourcePath!, "utf8")).toBe("; generated llvm\n");
expect(await readFile(hit!.irSourcePath!, "utf8")).toContain("irVersion");
await expect(stat(f.llvmPath)).rejects.toThrow();
expect(await readFile(alternateC, "utf8")).toBe("/* user-owned native source */\n");
});
@@ -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 { privateSiblingPath } from "../backend/build-cache.js";
import type { CompilerImplementationDependency } from "../library/compiler-self-identity.js";
import {
installDarwinDebugSymbols,
@@ -97,6 +98,9 @@ export interface EarlyExecutableNativeFeatures {
tls: boolean;
cipher: boolean;
ecdh: boolean;
dh: boolean;
pki: boolean;
ed25519: boolean;
perfLoop: boolean;
tlsCa: boolean;
webcrypto: boolean;
@@ -183,6 +187,15 @@ interface EarlyExecutableImplementationProof {
export interface EarlyExecutableCacheHit {
llvmPath: string;
irPath?: string;
/** When the executable was not restored: private copies of the cached LLVM text and IR beside
* llvmPath and irPath, which the build compiles from and installs at those names when it is done
* (index.ts BuildSources). A restored executable needs no compile, and its files are installed at
* llvmPath and irPath. */
llvmSourcePath?: string;
irSourcePath?: string;
/** llvmPath (irPath) already holds the cached bytes: the build leaves it as it is. */
llvmPathCurrent?: true;
irPathCurrent?: true;
/** True when the final executable was restored after native dependencies
* validated. False restores only frontend artifacts and must call compileC. */
executableRestored: boolean;
@@ -232,6 +245,9 @@ const BOOLEAN_NATIVE_KEYS = [
"tlsCa",
"cipher",
"ecdh",
"dh",
"pki",
"ed25519",
"webcrypto",
"perfLoop",
] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[];
@@ -581,15 +597,6 @@ async function readExecutableCache(
(await nativeArtifactDependenciesStillMatch(stamp.nativeDependencies));
if (validateRoute !== undefined && !executableRestored) return null;
const paths = outputPaths(options, stamp.native.backend);
if (!(await fileMatches(paths.llvmPath, stamp.files.translationUnit.digest))) {
await installBytes(translationUnit, paths.llvmPath);
}
if (
ir !== null &&
stamp.files.ir !== null &&
!(await fileMatches(paths.irPath, stamp.files.ir.digest))
)
await installBytes(ir, paths.irPath);
if (executableRestored) {
try {
if (debugSymbols !== null) await installDarwinDebugSymbols(debugSymbols, options.outPath);
@@ -604,6 +611,32 @@ async function readExecutableCache(
executableRestored = false;
}
}
// A build that compiles from the cached translation unit compiles its own copy: a concurrent
// build of another program with the same entry name may replace the shared names meanwhile.
let llvmSourcePath: string | undefined;
let irSourcePath: string | undefined;
let llvmPathCurrent = false;
let irPathCurrent = false;
if (executableRestored) {
if (!(await fileMatches(paths.llvmPath, stamp.files.translationUnit.digest)))
await installBytes(translationUnit, paths.llvmPath);
if (
ir !== null &&
stamp.files.ir !== null &&
!(await fileMatches(paths.irPath, stamp.files.ir.digest))
)
await installBytes(ir, paths.irPath);
} else {
// The extension keeps clang reading the copy as LLVM IR rather than as a linker input.
llvmSourcePath = `${privateSiblingPath(paths.llvmPath, "llvm")}.ll`;
await installBytes(translationUnit, llvmSourcePath);
llvmPathCurrent = await fileMatches(paths.llvmPath, stamp.files.translationUnit.digest);
if (ir !== null && stamp.files.ir !== null) {
irSourcePath = privateSiblingPath(paths.irPath, "ir");
await installBytes(ir, irSourcePath);
irPathCurrent = await fileMatches(paths.irPath, stamp.files.ir.digest);
}
}
const now = new Date();
await Promise.all(
[
@@ -625,6 +658,10 @@ async function readExecutableCache(
? { performance: stamp.performance }
: {}),
...(ir === null ? {} : { irPath: paths.irPath }),
...(llvmSourcePath === undefined ? {} : { llvmSourcePath }),
...(irSourcePath === undefined ? {} : { irSourcePath }),
...(llvmPathCurrent ? { llvmPathCurrent: true as const } : {}),
...(irPathCurrent ? { irPathCurrent: true as const } : {}),
};
} catch {
return null;
@@ -0,0 +1,258 @@
/** The HTML entity names JSX text and attribute strings decode (HTML 4's
* 253 named character references, the set TypeScript's JSX emit decodes),
* by name. */
export const JSX_ENTITIES: ReadonlyMap<string, number> = new Map<string, number>([
["quot", 34],
["amp", 38],
["apos", 39],
["lt", 60],
["gt", 62],
["nbsp", 160],
["iexcl", 161],
["cent", 162],
["pound", 163],
["curren", 164],
["yen", 165],
["brvbar", 166],
["sect", 167],
["uml", 168],
["copy", 169],
["ordf", 170],
["laquo", 171],
["not", 172],
["shy", 173],
["reg", 174],
["macr", 175],
["deg", 176],
["plusmn", 177],
["sup2", 178],
["sup3", 179],
["acute", 180],
["micro", 181],
["para", 182],
["middot", 183],
["cedil", 184],
["sup1", 185],
["ordm", 186],
["raquo", 187],
["frac14", 188],
["frac12", 189],
["frac34", 190],
["iquest", 191],
["Agrave", 192],
["Aacute", 193],
["Acirc", 194],
["Atilde", 195],
["Auml", 196],
["Aring", 197],
["AElig", 198],
["Ccedil", 199],
["Egrave", 200],
["Eacute", 201],
["Ecirc", 202],
["Euml", 203],
["Igrave", 204],
["Iacute", 205],
["Icirc", 206],
["Iuml", 207],
["ETH", 208],
["Ntilde", 209],
["Ograve", 210],
["Oacute", 211],
["Ocirc", 212],
["Otilde", 213],
["Ouml", 214],
["times", 215],
["Oslash", 216],
["Ugrave", 217],
["Uacute", 218],
["Ucirc", 219],
["Uuml", 220],
["Yacute", 221],
["THORN", 222],
["szlig", 223],
["agrave", 224],
["aacute", 225],
["acirc", 226],
["atilde", 227],
["auml", 228],
["aring", 229],
["aelig", 230],
["ccedil", 231],
["egrave", 232],
["eacute", 233],
["ecirc", 234],
["euml", 235],
["igrave", 236],
["iacute", 237],
["icirc", 238],
["iuml", 239],
["eth", 240],
["ntilde", 241],
["ograve", 242],
["oacute", 243],
["ocirc", 244],
["otilde", 245],
["ouml", 246],
["divide", 247],
["oslash", 248],
["ugrave", 249],
["uacute", 250],
["ucirc", 251],
["uuml", 252],
["yacute", 253],
["thorn", 254],
["yuml", 255],
["OElig", 338],
["oelig", 339],
["Scaron", 352],
["scaron", 353],
["Yuml", 376],
["fnof", 402],
["circ", 710],
["tilde", 732],
["Alpha", 913],
["Beta", 914],
["Gamma", 915],
["Delta", 916],
["Epsilon", 917],
["Zeta", 918],
["Eta", 919],
["Theta", 920],
["Iota", 921],
["Kappa", 922],
["Lambda", 923],
["Mu", 924],
["Nu", 925],
["Xi", 926],
["Omicron", 927],
["Pi", 928],
["Rho", 929],
["Sigma", 931],
["Tau", 932],
["Upsilon", 933],
["Phi", 934],
["Chi", 935],
["Psi", 936],
["Omega", 937],
["alpha", 945],
["beta", 946],
["gamma", 947],
["delta", 948],
["epsilon", 949],
["zeta", 950],
["eta", 951],
["theta", 952],
["iota", 953],
["kappa", 954],
["lambda", 955],
["mu", 956],
["nu", 957],
["xi", 958],
["omicron", 959],
["pi", 960],
["rho", 961],
["sigmaf", 962],
["sigma", 963],
["tau", 964],
["upsilon", 965],
["phi", 966],
["chi", 967],
["psi", 968],
["omega", 969],
["thetasym", 977],
["upsih", 978],
["piv", 982],
["ensp", 8194],
["emsp", 8195],
["thinsp", 8201],
["zwnj", 8204],
["zwj", 8205],
["lrm", 8206],
["rlm", 8207],
["ndash", 8211],
["mdash", 8212],
["lsquo", 8216],
["rsquo", 8217],
["sbquo", 8218],
["ldquo", 8220],
["rdquo", 8221],
["bdquo", 8222],
["dagger", 8224],
["Dagger", 8225],
["bull", 8226],
["hellip", 8230],
["permil", 8240],
["prime", 8242],
["Prime", 8243],
["lsaquo", 8249],
["rsaquo", 8250],
["oline", 8254],
["frasl", 8260],
["euro", 8364],
["image", 8465],
["weierp", 8472],
["real", 8476],
["trade", 8482],
["alefsym", 8501],
["larr", 8592],
["uarr", 8593],
["rarr", 8594],
["darr", 8595],
["harr", 8596],
["crarr", 8629],
["lArr", 8656],
["uArr", 8657],
["rArr", 8658],
["dArr", 8659],
["hArr", 8660],
["forall", 8704],
["part", 8706],
["exist", 8707],
["empty", 8709],
["nabla", 8711],
["isin", 8712],
["notin", 8713],
["ni", 8715],
["prod", 8719],
["sum", 8721],
["minus", 8722],
["lowast", 8727],
["radic", 8730],
["prop", 8733],
["infin", 8734],
["ang", 8736],
["and", 8743],
["or", 8744],
["cap", 8745],
["cup", 8746],
["int", 8747],
["there4", 8756],
["sim", 8764],
["cong", 8773],
["asymp", 8776],
["ne", 8800],
["equiv", 8801],
["le", 8804],
["ge", 8805],
["sub", 8834],
["sup", 8835],
["nsub", 8836],
["sube", 8838],
["supe", 8839],
["oplus", 8853],
["otimes", 8855],
["perp", 8869],
["sdot", 8901],
["lceil", 8968],
["rceil", 8969],
["lfloor", 8970],
["rfloor", 8971],
["lang", 9001],
["rang", 9002],
["loz", 9674],
["spades", 9824],
["clubs", 9827],
["hearts", 9829],
["diams", 9830],
]);
@@ -0,0 +1,244 @@
import ts5 from "typescript5";
import { afterAll, describe, expect, it } from "vitest";
import { jsxEmitPlan, decodeEntities, jsxPragmas, jsxTextValue, lowerJsx } from "./jsx-source.js";
import { closeSourceParser, parseSourceFile } from "./ts7/source-parser-node.js";
afterAll(() => closeSourceParser());
/** Evaluates a program whose last statement is `result = ...`, with `h` and
* `Fragment` recording the calls the JSX makes. */
function evaluate(code: string): unknown {
const run = new Function(
"h",
"Fragment",
"React",
`"use strict"; let result; ${code}; return result;`,
) as (...args: unknown[]) => unknown;
const h = (tag: unknown, props: unknown, ...children: unknown[]): unknown => ({
tag: typeof tag === "function" ? `fn:${tag.name}` : tag === Fragment ? "#fragment" : tag,
props,
children,
});
const Fragment = { fragment: true };
return run(h, Fragment, { createElement: h, Fragment });
}
/** TypeScript's own classic-runtime emit of the snippet. */
function typescriptEmit(source: string, factory?: string): string {
return ts5.transpileModule(source, {
fileName: "case.tsx",
compilerOptions: {
target: ts5.ScriptTarget.ES2022,
module: ts5.ModuleKind.ESNext,
jsx: ts5.JsxEmit.React,
...(factory === undefined ? {} : { jsxFactory: factory, jsxFragmentFactory: "Fragment" }),
},
}).outputText;
}
function rewrite(source: string, factory?: string): string {
const sf = parseSourceFile("/case.tsx", source, "tsx");
const plan = jsxEmitPlan(
{
jsx: "react",
...(factory === undefined ? {} : { jsxFactory: factory, jsxFragmentFactory: "Fragment" }),
},
jsxPragmas(source),
);
return lowerJsx(sf, plan).text;
}
const cases: Record<string, string> = {
"attributes and text": `const name = "w"; const p = { id: 1 };
result = <div class="hello" data-x='1' disabled {...p} title={name}>Hello {name} &amp; &#x41; &copy;
second line
{/* comment */}
{name}{" "}<b>bold</b>
</div>;`,
fragments: `function Foo() {} Foo.Bar = function Bar() {};
result = <><i /><Foo.Bar x={1} /></>;`,
namespaced: `result = <ns:tag a:b="c" />;`,
"keys and nested maps": `const p = { id: 1 };
result = <div key="k" {...p}>{[1, 2].map((n) => <span key={n}>{n}</span>)}</div>;`,
"white space only": `result = [<div> </div>, <div>
</div>, <div> a {"b"} c </div>];`,
"spreads": `const p = { id: 1 }; const kids = [1, 2];
result = [<div {...p} />, <div {...p} {...p} />, <div>{...kids}</div>, <div {...{ a: 1, b: 2 }} c={3} />, <div {...{ __proto__: null, a: 1 }} />];`,
"attribute strings": `result = <div title="a &amp; b &unknown; &#65; &#x1F600;" alt="line
two" empty="" />;`,
"attribute values": `result = <div a={} b=<i /> c={<b>{1}</b>} d={[<p />]} />;`,
"member and this tags": `class X { Tag = function Tag() {}; render() { return <this.Tag x="1" />; } }
result = new X().render();`,
"dashed and capitalized names": `function Comp() {}
result = [<a-b />, <Comp $x="1" _y="2" z-w="3" />];`,
"nested JSX in expressions": `const items = ["a", "b"];
result = <ul>
{items.length > 1 ? <li>{items[0]}</li> : null}
{items.map((item) =>
<li
key={item}
title={item}
>
{item}
</li>)}
</ul>;`,
};
describe("lowerJsx", () => {
for (const [name, source] of Object.entries(cases)) {
it(`matches TypeScript's emit: ${name}`, () => {
const lowered = rewrite(source);
expect(evaluate(lowered)).toEqual(evaluate(typescriptEmit(source)));
expect(lowered.split("\n").length).toBe(source.split("\n").length);
});
}
it("uses jsxFactory and the pragmas", () => {
const source = `result = <><x a="1" /></>;`;
expect(evaluate(rewrite(source, "h"))).toEqual(evaluate(typescriptEmit(source, "h")));
const pragma = `/** @jsx h */\n/** @jsxFrag Fragment */\nresult = <><x /></>;`;
expect(rewrite(pragma)).toContain(`h(Fragment, null`);
expect(evaluate(rewrite(pragma))).toEqual(evaluate(typescriptEmit(pragma)));
});
it("keeps every byte outside JSX", () => {
const source = `const a = 1; // <not jsx>\nconst b = "<div>";\nresult = [a, b, <i />];\n`;
const lowered = rewrite(source);
expect(
lowered.startsWith(`const a = 1; // <not jsx>\nconst b = "<div>";\nresult = [a, b, `),
).toBe(true);
});
it("maps rewritten positions back to the source", () => {
const source = `const x = 1;\nresult = <div a={x}>{x}</div>;\nconst y = 2;\n`;
const sf = parseSourceFile("/case.tsx", source, "tsx");
const lowered = lowerJsx(sf, { runtime: "classic", factory: "h", fragment: "Fragment" });
const after = lowered.text.indexOf("const y");
expect(lowered.originalPosition(after)).toBe(source.indexOf("const y"));
const copied = lowered.text.indexOf("{ a: x }") + "{ a: ".length;
expect(lowered.originalPosition(copied)).toBe(source.indexOf("{x}>") + 1);
expect(lowered.originalPosition(lowered.text.indexOf("h("))).toBe(source.indexOf("<div"));
});
});
/** Evaluates automatic-runtime output: each runtime import becomes a
* binding of a recording function (`jsx`, `jsxs`, `jsxDEV`, `createElement`)
* or of the fragment marker, under the local name the output chose. */
function evaluateAutomatic(code: string): unknown {
const body = code
.replace(/import \{([^}]*)\} from "[^"]*";/g, (_match, list: string) =>
list
.split(",")
.map((item) => {
const [name, local] = item.trim().split(/\s+as\s+/);
return `const ${local} = runtime.${name};`;
})
.join(" "),
)
.replace(/(^|\s)export /gm, "$1");
const record =
(kind: string) =>
(...args: unknown[]): unknown => ({ kind, args: args.map(named) });
const runtime = {
jsx: record("jsx"),
jsxs: record("jsxs"),
jsxDEV: record("jsxDEV"),
createElement: record("createElement"),
Fragment: "#fragment",
};
const run = new Function("runtime", `"use strict"; let result; ${body}; return result;`) as (
runtime: unknown,
) => unknown;
return run(runtime);
}
/** Functions compare by name: each evaluation declares its own. */
function named(value: unknown): unknown {
if (typeof value === "function") return `fn:${value.name}`;
if (Array.isArray(value)) return value.map(named);
if (value !== null && typeof value === "object")
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, named(item)]));
return value;
}
function typescriptAutomatic(source: string, development: boolean): string {
return ts5.transpileModule(source, {
fileName: "/case.tsx",
compilerOptions: {
target: ts5.ScriptTarget.ES2022,
module: ts5.ModuleKind.ESNext,
jsx: development ? ts5.JsxEmit.ReactJSXDev : ts5.JsxEmit.ReactJSX,
},
}).outputText;
}
function rewriteAutomatic(source: string, development: boolean): string {
const sf = parseSourceFile("/case.tsx", source, "tsx");
const plan = jsxEmitPlan({ jsx: development ? "react-jsxdev" : "react-jsx" }, jsxPragmas(source));
return lowerJsx(sf, plan).text;
}
const automaticCases: Record<string, string> = {
...cases,
"keys": `const p = { id: 1 };
result = [<li key="a">x</li>, <li {...p} key="b" />, <li key="c" {...p} />, <li {...{ a: 1 }} key="d" />, <li {...{ ...p }} key="e" />];`,
"children shapes": `const kids = [1, 2];
result = [<p />, <p>{}</p>, <p>
</p>, <p>one</p>, <p>{1}{2}</p>, <p>{...kids}</p>, <p> </p>, <><b /></>, <></>];`,
"names in use": `const _jsx = "taken"; const _jsxs = 1;
result = [<p>{_jsx}</p>, <p>{_jsxs}{_jsx}</p>];`,
};
describe("lowerJsx with the automatic runtime", () => {
for (const [name, source] of Object.entries(automaticCases)) {
for (const development of [false, true]) {
it(`matches TypeScript's ${development ? "react-jsxdev" : "react-jsx"} emit: ${name}`, () => {
const lowered = rewriteAutomatic(`export {};\n${source}`, development);
const expected = typescriptAutomatic(`export {};\n${source}`, development);
expect(evaluateAutomatic(lowered)).toEqual(evaluateAutomatic(expected));
expect(lowered.split("\n").length).toBe(source.split("\n").length + 1);
});
}
}
it("imports the runtime functions on the first statement's line", () => {
const lowered = rewriteAutomatic(
`"use strict";\nexport const a = <><b key="k" /></>;\n`,
false,
);
expect(lowered).toBe(
`"use strict";import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; \nexport const a = _jsx(_Fragment, { children: _jsx("b", {}, "k") });\n`,
);
});
it("reads @jsxImportSource and @jsxRuntime", () => {
const pragma = `/** @jsxImportSource preact */\nexport const a = <b />;\n`;
expect(rewriteAutomatic(pragma, false)).toContain(`from "preact/jsx-runtime"`);
const classic = `/** @jsxRuntime classic */\n/** @jsx h */\nexport const a = <b />;\n`;
expect(rewriteAutomatic(classic, false)).toContain(`h("b", null)`);
});
});
describe("JSX text", () => {
it("trims lines as TypeScript does", () => {
expect(jsxTextValue(" a \n b \n ")).toBe(" a b");
expect(jsxTextValue("\n \n")).toBeUndefined();
expect(jsxTextValue(" ")).toBe(" ");
});
it("decodes entities", () => {
expect(decodeEntities("&lt;&gt;&amp;&nbsp;&#65;&#x42;&bogus;&#x110000;")).toBe(
"<>& AB&bogus;&#x110000;",
);
});
});
describe("jsxPragmas", () => {
it("reads leading multi-line comments, first @jsx and last @jsxImportSource", () => {
expect(
jsxPragmas(
"#!/usr/bin/env node\n/** @jsx h\n * @jsx g */\n// @jsxFrag F\n/* @jsxImportSource a */ /* @jsxImportSource b */\nconst x = 1; /** @jsx late */",
),
).toEqual({ jsx: "h", jsxImportSource: "b" });
});
});
@@ -0,0 +1,856 @@
/* JSX lowering at the source level, before IR lowering.
*
* A program that contains JSX is checked as written: the checker types JSX
* with the project's jsx settings and reports the errors tsc reports. Each
* file that contains JSX is then rewritten with every JSX expression
* replaced by the calls TypeScript emits for it, and the rewritten program is
* what the lowering compiles, through the same import, call and object
* literal lowering as ordinary code.
*
* The rewrite copies every byte outside JSX expressions, and copies the
* expressions inside them (attribute values, children, spreads) with their
* own JSX rewritten. It keeps every line break of a JSX expression, between
* the arguments it emits, so a position keeps its line in the rewritten
* text. */
import * as ts from "./ts7/adapter.js";
import { jsxOptionName } from "./ts7/enums.js";
import { JSX_ENTITIES } from "./jsx-entities.js";
/** The project's jsx compiler options, as the tsconfig spells them. */
export interface JsxOptions {
jsx?: string | number | undefined;
jsxFactory?: string | undefined;
jsxFragmentFactory?: string | undefined;
jsxImportSource?: string | undefined;
reactNamespace?: string | undefined;
}
/** The tsconfig name of a `jsx` option value (TS7 options carry JsxEmit
* numbers, parsed tsconfigs may carry the name). */
export function jsxEmitName(value: string | number | undefined): string | undefined {
if (typeof value === "number") return jsxOptionName(value);
return value?.toLowerCase();
}
/** The `@jsx`, `@jsxFrag`, `@jsxImportSource` and `@jsxRuntime` pragmas of
* a file's leading comments, as TypeScript reads them: multi-line comments
* only, the first `@jsx` and `@jsxFrag` and the last `@jsxImportSource` and
* `@jsxRuntime`. */
export interface JsxPragmas {
jsx?: string;
jsxFrag?: string;
jsxImportSource?: string;
jsxRuntime?: string;
}
export function jsxPragmas(text: string): JsxPragmas {
const pragmas: JsxPragmas = {};
for (const comment of leadingMultiLineComments(text)) {
const pattern = /@(\S+)(\s+(?:\S.*)?)?$/gm;
for (let match = pattern.exec(comment); match !== null; match = pattern.exec(comment)) {
const argument = match[2]?.trim().split(/\s+/)[0];
if (!argument) continue;
switch (match[1]!.toLowerCase()) {
case "jsx":
pragmas.jsx ??= argument;
break;
case "jsxfrag":
pragmas.jsxFrag ??= argument;
break;
case "jsximportsource":
pragmas.jsxImportSource = argument;
break;
case "jsxruntime":
pragmas.jsxRuntime = argument;
break;
}
}
}
return pragmas;
}
/** The `/* ... *\/` comments before a file's first token (after a shebang). */
function leadingMultiLineComments(text: string): string[] {
const comments: string[] = [];
let i = text.startsWith("#!") ? text.indexOf("\n") : 0;
if (i < 0) return comments;
while (i < text.length) {
const ch = text.charCodeAt(i);
if (isLineBreak(ch) || isWhiteSpaceSingleLine(ch)) {
i++;
} else if (text.startsWith("//", i)) {
while (i < text.length && !isLineBreak(text.charCodeAt(i))) i++;
} else if (text.startsWith("/*", i)) {
const end = text.indexOf("*/", i + 2);
const close = end < 0 ? text.length : end + 2;
comments.push(text.slice(i, close));
i = close;
} else {
break;
}
}
return comments;
}
function isLineBreak(ch: number): boolean {
return ch === 10 || ch === 13 || ch === 0x2028 || ch === 0x2029;
}
function isWhiteSpaceSingleLine(ch: number): boolean {
return (
ch === 32 ||
ch === 9 ||
ch === 11 ||
ch === 12 ||
ch === 160 ||
ch === 133 ||
ch === 5760 ||
(ch >= 8192 && ch <= 8203) ||
ch === 8239 ||
ch === 8287 ||
ch === 12288 ||
ch === 65279
);
}
/** How a file's JSX is emitted, after its pragmas. */
export type JsxEmitPlan =
| {
runtime: "classic";
/** The factory call (`h`, `React.createElement`). */
factory: string;
/** The fragment tag (`Fragment`, `React.Fragment`). */
fragment: string;
}
| {
runtime: "automatic";
/** The module `jsx-runtime` (or `jsx-dev-runtime`) is under, and that
* a `createElement` fallback imports from (`react`). */
importSource: string;
/** jsx: react-jsxdev: `jsxDEV` calls with source locations. */
development: boolean;
};
/** The plan of a file: the automatic runtime with jsx: react-jsx or
* react-jsxdev, an import source or an `@jsxRuntime automatic` pragma (an
* `@jsxRuntime classic` pragma overrides all of them), the classic runtime
* otherwise. */
export function jsxEmitPlan(options: JsxOptions, pragmas: JsxPragmas): JsxEmitPlan {
const mode = jsxEmitName(options.jsx);
const automatic =
pragmas.jsxRuntime !== "classic" &&
(mode === "react-jsx" ||
mode === "react-jsxdev" ||
options.jsxImportSource !== undefined ||
pragmas.jsxImportSource !== undefined ||
pragmas.jsxRuntime === "automatic");
if (automatic) {
return {
runtime: "automatic",
importSource: pragmas.jsxImportSource ?? options.jsxImportSource ?? "react",
development: mode === "react-jsxdev",
};
}
const namespace = options.reactNamespace ?? "React";
return {
runtime: "classic",
factory: pragmas.jsx ?? options.jsxFactory ?? `${namespace}.createElement`,
fragment: pragmas.jsxFrag ?? options.jsxFragmentFactory ?? `${namespace}.Fragment`,
};
}
/** True when the file contains a JSX expression. */
export function containsJsx(sf: ts.SourceFile): boolean {
let found = false;
const visit = (node: ts.Node): void => {
if (found) return;
if (isJsxExpressionRoot(node)) {
found = true;
return;
}
ts.forEachChild(node, visit);
};
visit(sf);
return found;
}
/** The first JSX expression of the file (for diagnostics), or null. */
export function firstJsx(sf: ts.SourceFile): ts.Node | null {
let found: ts.Node | null = null;
const visit = (node: ts.Node): void => {
if (found !== null) return;
if (isJsxExpressionRoot(node)) {
found = node;
return;
}
ts.forEachChild(node, visit);
};
visit(sf);
return found;
}
type JsxRoot = ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment;
function isJsxExpressionRoot(node: ts.Node): node is JsxRoot {
return ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node);
}
/** A file's text with its JSX lowered, and the way back from a position of
* that text to the source position it stands for. */
export interface LoweredJsx {
text: string;
/** A rewritten-text offset as an offset of the original text: copied text
* keeps its offset; emitted call text maps to the JSX it stands for. */
originalPosition(pos: number): number;
/** An original-text offset in the rewritten text: copied text keeps its
* offset; a position inside replaced JSX maps to the call text emitted
* for the JSX at or before it (its end, for the `end` of a span). */
rewrittenPosition(pos: number, end: boolean): number;
}
/** The file's text with its JSX replaced by the calls TypeScript emits for
* the plan, and for the automatic runtime the imports of the runtime
* functions those calls use. */
export function lowerJsx(sf: ts.SourceFile, plan: JsxEmitPlan): LoweredJsx {
return new JsxRewriter(sf, plan).file();
}
/** A run of rewritten text: `copied` text starts at `original`; emitted
* text stands for the source position `original`. */
interface Segment {
rewritten: number;
original: number;
/** The copied length, 0 for emitted text. */
copied: number;
}
class JsxRewriter {
private readonly text: string;
/** Output pieces; `cursor` is the source position whose line breaks the
* output has reached. */
private readonly pieces: string[] = [];
private readonly segments: Segment[] = [];
private length = 0;
private cursor = 0;
constructor(
private readonly sf: ts.SourceFile,
private readonly plan: JsxEmitPlan,
) {
this.text = sf.text;
}
file(): LoweredJsx {
const insertAt = this.plan.runtime === "automatic" ? this.prologueEnd() : -1;
this.copy(0, this.text.length);
if (insertAt >= 0) this.insertImports(insertAt);
const segments = this.segments;
const total = this.length;
return {
text: this.pieces.join(""),
originalPosition: (pos: number): number => {
let low = 0;
let high = segments.length - 1;
while (low < high) {
const mid = (low + high + 1) >> 1;
if (segments[mid]!.rewritten <= pos) low = mid;
else high = mid - 1;
}
const segment = segments[low];
if (segment === undefined) return pos;
return segment.copied > 0
? segment.original + Math.min(pos - segment.rewritten, segment.copied)
: segment.original;
},
rewrittenPosition: (pos: number, end: boolean): number => {
// Segments are in source order: the last one starting at or before
// `pos` holds it, or is the emitted text for the JSX holding it.
let low = 0;
let high = segments.length - 1;
while (low < high) {
const mid = (low + high + 1) >> 1;
if (segments[mid]!.original <= pos) low = mid;
else high = mid - 1;
}
const segment = segments[low];
if (segment === undefined) return pos;
if (segment.copied > 0)
return segment.rewritten + Math.min(pos - segment.original, segment.copied);
return end ? (segments[low + 1]?.rewritten ?? total) : segment.rewritten;
},
};
}
/** Local names of the automatic runtime's functions, by module and
* export, in the order of first use (TypeScript's implicit imports). */
private readonly imports = new Map<string, Map<string, string>>();
private names: Set<string> | null = null;
private fileNameLocal: string | null = null;
/** The local name bound to `name` of `module`: `_name`, or `_name_1`,
* `_name_2`, ... past an identifier the file already uses. */
private importName(name: string, module: string): string {
let names = this.imports.get(module);
if (names === undefined) {
names = new Map();
this.imports.set(module, names);
}
let local = names.get(name);
if (local === undefined) {
local = this.uniqueName(`_${name}`);
names.set(name, local);
}
return local;
}
private uniqueName(base: string): string {
if (this.names === null) {
const names = new Set<string>();
const visit = (node: ts.Node): void => {
if (ts.isIdentifier(node)) names.add(node.text);
ts.forEachChild(node, visit);
};
visit(this.sf);
this.names = names;
}
let name = base;
for (let i = 1; this.names.has(name); i++) name = `${base}_${i}`;
this.names.add(name);
return name;
}
/** Where the runtime imports go: after the leading "use strict"-style
* directives, else before the first statement, so the inserted text
* shares that statement's line. */
private prologueEnd(): number {
const statements = this.sf.statements.nodes;
let end = -1;
for (const statement of statements) {
if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression)) break;
end = statement.end;
}
if (end >= 0) return end;
const first = statements[0];
return first === undefined ? this.text.length : first.getStart(this.sf);
}
/** Inserts the imports (or, in a CommonJS file, the requires) of the
* runtime functions the file's JSX used, and react-jsxdev's file name
* constant, at `at`: imports in the reverse order of their modules' first
* use, as TypeScript inserts each after the prologue. */
private insertImports(at: number): void {
if (this.imports.size === 0 && this.fileNameLocal === null) return;
const esm = ts.isExternalModule(this.sf);
const parts: string[] = [];
for (const [module, names] of [...this.imports].reverse()) {
const specifier = JSON.stringify(module);
if (esm) {
const list = [...names].map(([name, local]) => `${name} as ${local}`).join(", ");
parts.push(`import { ${list} } from ${specifier};`);
} else if (/\.[cm]?jsx?$/.test(this.sf.fileName)) {
const list = [...names].map(([name, local]) => `${name}: ${local}`).join(", ");
parts.push(`const { ${list} } = require(${specifier});`);
}
}
if (this.fileNameLocal !== null)
parts.push(`const ${this.fileNameLocal} = ${JSON.stringify(this.sf.fileName)};`);
if (parts.length === 0) return;
const text = `${parts.join(" ")} `;
let index = 0;
let rewritten = 0;
for (; index < this.segments.length; index++) {
const segment = this.segments[index]!;
if (segment.copied > 0 && segment.original <= at && at <= segment.original + segment.copied) {
rewritten = segment.rewritten + (at - segment.original);
break;
}
}
// Split the copied segment holding `at` and shift what follows.
const holder = this.segments[index];
const joined = this.pieces.join("");
this.pieces.length = 0;
this.pieces.push(joined.slice(0, rewritten), text, joined.slice(rewritten));
const following: Segment[] = [{ rewritten, original: at, copied: 0 }];
if (holder !== undefined && holder.copied > 0) {
const head = at - holder.original;
const tail = holder.copied - head;
holder.copied = head;
if (tail > 0)
following.push({ rewritten: rewritten + text.length, original: at, copied: tail });
}
for (let i = index + 1; i < this.segments.length; i++)
this.segments[i]!.rewritten += text.length;
this.segments.splice(index + 1, 0, ...following);
if (holder !== undefined && holder.copied === 0) this.segments.splice(index, 1);
this.length += text.length;
}
/** Emits source text [start, end) unchanged. */
private copied(start: number, end: number): void {
if (end <= start) return;
this.segments.push({ rewritten: this.length, original: start, copied: end - start });
this.pieces.push(this.text.slice(start, end));
this.length += end - start;
}
/** Emits call text standing for the source position the cursor reached. */
private emit(...texts: string[]): void {
for (const text of texts) {
if (text.length === 0) continue;
const last = this.segments[this.segments.length - 1];
if (last === undefined || last.copied > 0 || last.original !== this.cursor)
this.segments.push({ rewritten: this.length, original: this.cursor, copied: 0 });
this.pieces.push(text);
this.length += text.length;
}
}
/** Copies the source range [start, end), with its JSX expressions
* replaced. */
private copy(start: number, end: number): void {
let at = start;
const visit = (node: ts.Node): void => {
if (node.end <= at || (node !== this.sf && node.getStart(this.sf) >= end)) return;
if (isJsxExpressionRoot(node)) {
const nodeStart = node.getStart(this.sf);
this.copied(at, nodeStart);
if (nodeStart > this.cursor) this.cursor = nodeStart;
this.element(node);
this.sync(node.end);
at = node.end;
return;
}
ts.forEachChild(node, visit);
};
// The cursor never moves back: a range copied after source that follows
// it (jsx()'s key argument, after the children) emits no line break
// twice.
if (start > this.cursor) this.cursor = start;
visit(this.enclosing(start, end));
this.copied(at, end);
if (end > this.cursor) this.cursor = end;
}
/** The smallest node around [start, end) whose children cover it. */
private enclosing(start: number, end: number): ts.Node {
let node: ts.Node = this.sf;
for (;;) {
let next: ts.Node | undefined;
ts.forEachChild(node, (child) => {
if (next === undefined && child.pos <= start && child.end >= end) next = child;
});
if (next === undefined || next === node) return node;
node = next;
}
}
/** Emits the line breaks of the source between the cursor and `pos`. */
private sync(pos: number): void {
for (let i = this.cursor; i < pos; i++) {
const ch = this.text.charCodeAt(i);
if (ch === 13 && this.text.charCodeAt(i + 1) === 10) {
this.cursor = i;
this.emit("\r\n");
i++;
} else if (isLineBreak(ch)) {
this.cursor = i;
this.emit(this.text[i]!);
}
}
if (pos > this.cursor) this.cursor = pos;
}
/** Copies an expression in a JSX expression's place. */
private expression(node: ts.Node): void {
const start = node.getStart(this.sf);
this.sync(start);
this.copy(start, node.end);
}
private element(node: JsxRoot): void {
const plan = this.plan;
if (plan.runtime === "classic") {
this.createElement(node, plan.factory, plan.fragment);
return;
}
const opening = ts.isJsxFragment(node)
? null
: ts.isJsxElement(node)
? node.openingElement
: node;
if (opening !== null && hasKeyAfterPropsSpread(opening.attributes)) {
// A key after a spread of props is not jsx()'s key argument: the
// element is the import source's createElement call.
this.createElement(node, this.importName("createElement", plan.importSource), "");
return;
}
this.automaticElement(node, opening, plan.importSource, plan.development);
}
/** The classic-runtime call: `factory(tag, props or null, ...children)`. */
private createElement(node: JsxRoot, factory: string, fragment: string): void {
const children = ts.isJsxSelfClosingElement(node) ? [] : node.children.nodes;
if (ts.isJsxFragment(node)) {
this.emit(`${factory}(${fragment}, null`);
} else {
const opening = ts.isJsxElement(node) ? node.openingElement : node;
this.emit(`${factory}(`);
this.tagName(opening.tagName);
this.emit(", ");
this.attributes(opening.attributes.properties.nodes, null);
}
for (const child of children) {
if (ts.isJsxText(child)) {
const value = jsxTextValue(child.text);
if (value === undefined) continue;
this.emit(", ");
this.sync(child.getStart(this.sf));
this.emit(JSON.stringify(value));
} else if (ts.isJsxExpression(child)) {
if (child.expression === undefined) continue;
this.emit(", ");
if (child.dotDotDotToken !== undefined) this.emit("...");
this.expression(child.expression);
} else if (isJsxExpressionRoot(child)) {
this.emit(", ");
this.sync(child.getStart(this.sf));
this.element(child);
}
this.sync(child.end);
}
this.sync(node.end);
this.emit(")");
}
/** The automatic-runtime call: `jsx(tag, props[, key])` with the
* children in props, `jsxs` for more than one child or a spread child, and
* react-jsxdev's `jsxDEV(tag, props, key, isStatic, source, self)`. */
private automaticElement(
node: JsxRoot,
opening: ts.JsxOpeningElement | ts.JsxSelfClosingElement | null,
importSource: string,
development: boolean,
): void {
const children: readonly ts.JsxChild[] = ts.isJsxSelfClosingElement(node)
? []
: node.children.nodes;
const semantic = children.filter(isSemanticJsxChild);
const first = semantic[0];
const isStatic =
semantic.length > 1 ||
(first !== undefined && ts.isJsxExpression(first) && first.dotDotDotToken !== undefined);
const module = `${importSource}/${development ? "jsx-dev-runtime" : "jsx-runtime"}`;
this.emit(`${this.importName(development ? "jsxDEV" : isStatic ? "jsxs" : "jsx", module)}(`);
// TypeScript's children prop: the one semantic child itself, else an
// array of every child that emits a value.
const single = semantic.length === 1 && !isStatic ? first! : null;
const listed = single === null ? children.filter((child) => jsxChildEmits(child)) : [];
const childrenProp =
children.length > 0 && (single !== null ? jsxChildEmits(single) : listed.length > 0)
? (): void => {
this.emit("children: ");
if (single !== null) {
this.child(single);
} else {
this.emit("[");
listed.forEach((child, index) => {
if (index > 0) this.emit(", ");
this.child(child);
this.sync(child.end);
});
this.emit("]");
}
}
: null;
let key: ts.JsxAttribute | undefined;
if (opening === null) {
this.emit(this.importName("Fragment", module), ", ");
if (childrenProp === null) this.emit("{}");
else this.attributes([], childrenProp);
} else {
this.tagName(opening.tagName);
this.emit(", ");
const properties = opening.attributes.properties.nodes;
key = properties.find(
(property): property is ts.JsxAttribute =>
ts.isJsxAttribute(property) &&
ts.isIdentifier(property.name) &&
property.name.text === "key",
);
const attributes = properties.filter((property) => property !== key);
if (attributes.length === 0 && childrenProp === null) this.emit("{}");
else this.attributes(attributes, childrenProp);
}
if (key !== undefined) {
this.emit(", ");
this.attributeValue(key.initializer);
}
if (development) {
if (key === undefined) this.emit(", void 0");
const { line, column } = lineAndColumn(this.text, node.pos);
this.fileNameLocal ??= this.uniqueName("_jsxFileName");
this.emit(
`, ${isStatic}, { fileName: ${this.fileNameLocal}, lineNumber: ${line}, columnNumber: ${column} }, `,
jsxSelf(node),
);
}
this.sync(node.end);
this.emit(")");
}
/** A child's value in the children prop (it emits one: jsxChildEmits). */
private child(child: ts.JsxChild): void {
if (ts.isJsxText(child)) {
this.sync(child.getStart(this.sf));
this.emit(JSON.stringify(jsxTextValue(child.text)!));
} else if (ts.isJsxExpression(child)) {
if (child.dotDotDotToken !== undefined) this.emit("...");
this.expression(child.expression!);
} else {
this.sync(child.getStart(this.sf));
this.element(child as JsxRoot);
}
}
private tagName(tag: ts.JsxTagNameExpression): void {
this.sync(tag.getStart(this.sf));
if (ts.isIdentifier(tag) && isIntrinsicJsxName(tag.text)) {
this.emit(JSON.stringify(tag.text));
} else if (ts.isJsxNamespacedName(tag)) {
this.emit(JSON.stringify(`${tag.namespace.text}:${tag.name.text}`));
} else {
this.emit(this.text.slice(tag.getStart(this.sf), tag.end));
}
this.cursor = tag.end;
}
/** The props object of `properties`, then `children` (the automatic
* runtime's children prop), or `null` for no properties and no children
* prop. */
private attributes(
properties: readonly ts.JsxAttributeLike[],
children: (() => void) | null,
): void {
if (properties.length === 0 && children === null) {
this.emit("null");
return;
}
if (properties.length === 0) {
this.emit("{ ");
children!();
this.emit(" }");
return;
}
this.emit("{ ");
let first = true;
const separate = (): void => {
if (!first) this.emit(", ");
first = false;
};
for (const attribute of properties) {
if (ts.isJsxSpreadAttribute(attribute)) {
const value = attribute.expression;
if (ts.isObjectLiteralExpression(value) && !hasProtoProperty(value)) {
// TypeScript inlines the properties of a spread object literal.
for (const property of value.properties.nodes) {
separate();
this.expression(property);
}
} else {
separate();
this.emit("...");
this.expression(value);
}
} else {
separate();
this.sync(attribute.getStart(this.sf));
this.emit(attributeName(attribute.name), ": ");
this.attributeValue(attribute.initializer);
}
this.sync(attribute.end);
}
if (children !== null) {
this.emit(", ");
children();
}
this.emit(" }");
}
private attributeValue(value: ts.JsxAttributeValue | undefined): void {
if (value === undefined) {
this.emit("true");
} else if (ts.isStringLiteral(value)) {
this.sync(value.getStart(this.sf));
this.emit(JSON.stringify(decodeEntities(value.text)));
} else if (ts.isJsxExpression(value)) {
if (value.expression === undefined) this.emit("true");
else this.expression(value.expression);
} else {
this.sync(value.getStart(this.sf));
this.element(value as JsxRoot);
}
}
}
/** A child TypeScript counts: an expression container with an expression,
* text that is not only white space spanning lines, or an element. */
function isSemanticJsxChild(child: ts.JsxChild): boolean {
if (ts.isJsxExpression(child)) return child.expression !== undefined;
if (ts.isJsxText(child)) return !child.containsOnlyTriviaWhiteSpaces;
return true;
}
/** True when the child emits a value (text that trims to nothing and empty
* expression containers emit none). */
function jsxChildEmits(child: ts.JsxChild): boolean {
if (ts.isJsxText(child)) return jsxTextValue(child.text) !== undefined;
if (ts.isJsxExpression(child)) return child.expression !== undefined;
return true;
}
/** A `key` attribute after a spread that is not an object literal free of
* spreads: TypeScript emits createElement for such an element. */
function hasKeyAfterPropsSpread(attributes: ts.JsxAttributes): boolean {
let spread = false;
for (const property of attributes.properties.nodes) {
if (
ts.isJsxSpreadAttribute(property) &&
(!ts.isObjectLiteralExpression(property.expression) ||
property.expression.properties.nodes.some((p: ts.ObjectLiteralElementLike) =>
ts.isSpreadAssignment(p),
))
) {
spread = true;
} else if (
spread &&
ts.isJsxAttribute(property) &&
ts.isIdentifier(property.name) &&
property.name.text === "key"
) {
return true;
}
}
return false;
}
/** The 1-based line and column of `pos`, counting the line breaks
* TypeScript counts (\r\n, \n, \r, U+2028, U+2029) and UTF-16 units. */
function lineAndColumn(text: string, pos: number): { line: number; column: number } {
let line = 1;
let start = 0;
for (let i = 0; i < pos; i++) {
const ch = text.charCodeAt(i);
if (ch === 13 && text.charCodeAt(i + 1) === 10) continue;
if (isLineBreak(ch)) {
line++;
start = i + 1;
}
}
return { line, column: pos - start + 1 };
}
/** react-jsxdev's `self` argument. TypeScript passes `this`; where `this`
* has no declared type (the top level of an ES module and a function that is
* not a method and declares no `this` parameter) the argument is the
* `undefined` such code observes when called without a receiver, since an
* untyped `this` does not type-check under noImplicitThis. */
function jsxSelf(node: ts.Node): string {
for (let at = node.parent; at !== undefined; at = at.parent) {
if (ts.isArrowFunction(at)) continue;
if (
ts.isMethodDeclaration(at) ||
ts.isConstructorDeclaration(at) ||
ts.isGetAccessorDeclaration(at) ||
ts.isSetAccessorDeclaration(at) ||
ts.isPropertyDeclaration(at) ||
ts.isClassStaticBlockDeclaration(at)
)
return "this";
if (ts.isFunctionDeclaration(at) || ts.isFunctionExpression(at)) {
const declaresThis = at.parameters.nodes.some(
(parameter: ts.ParameterDeclaration) =>
ts.isIdentifier(parameter.name) && parameter.name.text === "this",
);
const objectMethod =
ts.isFunctionExpression(at) &&
at.parent !== undefined &&
ts.isPropertyAssignment(at.parent);
return declaresThis || objectMethod ? "this" : "undefined";
}
if (ts.isSourceFile(at)) return ts.isExternalModule(at) ? "undefined" : "this";
}
return "undefined";
}
/** TypeScript's intrinsic-element test: a lowercase first letter or a dash. */
function isIntrinsicJsxName(name: string): boolean {
const ch = name.charCodeAt(0);
return (ch >= 97 && ch <= 122) || name.includes("-");
}
function attributeName(name: ts.JsxAttributeName): string {
if (ts.isJsxNamespacedName(name))
return JSON.stringify(`${name.namespace.text}:${name.name.text}`);
return /^[A-Z_]\w*$/i.test(name.text) ? name.text : JSON.stringify(name.text);
}
function hasProtoProperty(object: ts.ObjectLiteralExpression): boolean {
return object.properties.nodes.some(
(property: ts.ObjectLiteralElementLike) =>
ts.isPropertyAssignment(property) &&
((ts.isIdentifier(property.name) && property.name.text === "__proto__") ||
(ts.isStringLiteral(property.name) && property.name.text === "__proto__")),
);
}
/** A JSX text child's string value: each line trimmed (the first line keeps
* its leading and the last line its trailing white space), lines without
* text dropped, the rest joined with one space and entities decoded.
* Undefined when nothing remains. */
export function jsxTextValue(text: string): string | undefined {
let value: string | undefined;
let firstNonWhitespace = 0;
let lastNonWhitespace = -1;
const add = (line: string): void => {
const decoded = decodeEntities(line);
value = value === undefined ? decoded : `${value} ${decoded}`;
};
for (let i = 0; i < text.length; i++) {
const ch = text.charCodeAt(i);
if (isLineBreak(ch)) {
if (firstNonWhitespace !== -1 && lastNonWhitespace !== -1)
add(text.slice(firstNonWhitespace, lastNonWhitespace + 1));
firstNonWhitespace = -1;
} else if (!isWhiteSpaceSingleLine(ch)) {
lastNonWhitespace = i;
if (firstNonWhitespace === -1) firstNonWhitespace = i;
}
}
if (firstNonWhitespace !== -1) add(text.slice(firstNonWhitespace));
return value;
}
/** Decodes `&name;`, `&#ddd;` and `&#xhh;` references. Unknown names and
* numbers past U+10FFFF stay as written. */
export function decodeEntities(text: string): string {
return text.replace(
/&((#((\d+)|x([\da-fA-F]+)))|(\w+));/g,
(
match: string,
_all: string | undefined,
_number: string | undefined,
_digits: string | undefined,
decimal: string | undefined,
hex: string | undefined,
word: string | undefined,
) => {
const code = decimal
? parseInt(decimal, 10)
: hex
? parseInt(hex, 16)
: JSX_ENTITIES.get(word!);
return code === undefined || code > 0x10ffff ? match : String.fromCodePoint(code);
},
);
}
@@ -8,16 +8,15 @@ import {
function harness() {
const members = new Map<number, SemanticTypeData[]>();
const snapshot = new SemanticSnapshot(1, {
text(method, payload) {
const query = JSON.parse(payload) as { objectId: number };
if (method !== "getTypesOfType" || !members.has(query.objectId))
throw new Error(`unexpected request: ${method}`);
return JSON.stringify(members.get(query.objectId));
},
binary(method) {
const reply = (method: string, payload: string) => {
const query = JSON.parse(payload) as { objectId: number };
if (method !== "getTypesOfType" || !members.has(query.objectId))
throw new Error(`unexpected request: ${method}`);
},
return JSON.stringify(members.get(query.objectId));
};
const snapshot = new SemanticSnapshot(1, {
text: reply,
binary: (method, payload) => new TextEncoder().encode(reply(method, payload)),
});
const project = snapshot.addProject("literal-values", () => undefined);
let id = 0;
@@ -9,6 +9,7 @@ import {
type SrcLoc,
typeEquals,
} from "../../ir/ir.js";
import { keyedHelperName } from "./stable-names.js";
import { numLit, varRef } from "../../ir/build.js";
import { typeKey } from "../type-mapper.js";
import type { Lowerer } from "./lowerer.js";
@@ -125,7 +126,7 @@ export function lowerArrayFill(
writeNull;
let name = lowerer.arrHofHelpers.get(key);
if (!name) {
name = "%arr.fill." + lowerer.arrHofHelpers.size;
name = keyedHelperName(lowerer, "%arr.fill", key);
lowerer.arrHofHelpers.set(key, name);
const a = varRef("a.0", arrType, loc);
const n = varRef("n.0", F64, loc);
@@ -1,3 +1,4 @@
import { keyedHelperName } from "./stable-names.js";
import { InternalCompilerError } from "../../errors.js";
import { dynUndefinedExpr, helperLoc, varRef } from "../../ir/build.js";
import {
@@ -252,7 +253,7 @@ export function lowerSafeIndexRead(
let name = lowerer.arrHofHelpers.get(key);
if (!name) {
const at = helperLoc(loc);
name = `%arr.idxOr.${lowerer.arrHofHelpers.size}`;
name = keyedHelperName(lowerer, "%arr.idxOr", key);
lowerer.arrHofHelpers.set(key, name);
const arrT = arr.type;
lowerer.liftedFns.push({
@@ -298,7 +299,7 @@ export function lowerSafeBytesRead(
let name = lowerer.arrHofHelpers.get(key);
if (!name) {
const at = helperLoc(loc);
name = `%bytes.idxOr.${lowerer.arrHofHelpers.size}`;
name = keyedHelperName(lowerer, "%bytes.idxOr", key);
lowerer.arrHofHelpers.set(key, name);
const b = varRef("b.0", bytesT, at);
const i = varRef("i.0", F64, at);
@@ -10,7 +10,13 @@ import { isGenericCallableMemberType } from "../type-mapper.js";
* Sound file-locally for module-scope bindings because ESM import
* bindings are read-only — no other file can write one. Cached per
* symbol (the scan walks the whole file once). */
export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: ts.Node): boolean {
export function bindingNeverReassigned(
lowerer: Lowerer,
sym: ts.Symbol,
decl: ts.Node,
resolve: (ident: ts.Identifier) => ts.Symbol | null | undefined = (ident) =>
lowerer.resolveValueSymbol(ident),
): boolean {
const cached = lowerer.neverReassignedCache.get(sym);
if (cached !== undefined) return cached;
let written = false;
@@ -18,7 +24,7 @@ export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: t
// identifiers pay a symbol resolution.
const symText = ts.symbolName(sym);
const namesSym = (e: ts.Node): boolean =>
ts.isIdentifier(e) && e.text === symText && lowerer.resolveValueSymbol(e) === sym;
ts.isIdentifier(e) && e.text === symText && resolve(e) === sym;
const scanTarget = (t: ts.Expression): void => {
let e: ts.Expression = t;
while (ts.isParenthesizedExpression(e)) e = e.expression;
@@ -1,3 +1,4 @@
import { keyedHelperName } from "../stable-names.js";
import {
BYTES_U8,
BOOL,
@@ -72,7 +73,7 @@ export function lowerOptionalNumberPredicate(
const key = `number.optionalPredicate:${fn}:${typeKey(widened)}`;
let helper = lowerer.valueHelpers.get(key);
if (!helper) {
helper = `%number.optionalPredicate.${lowerer.valueHelpers.size}`;
helper = keyedHelperName(lowerer, "%number.optionalPredicate", key);
lowerer.valueHelpers.set(key, helper);
const input = varRef("value.0", widened, loc);
lowerer.liftedFns.push({
@@ -60,10 +60,14 @@ export function lowerAtomicsCall(
// has a concrete typed-array binding. Validate the actual lowered storage.
const array = lowerer.lowerExpr(source);
const type = array.type;
// An untyped array (a JavaScript `workerData` or field): the runtime
// checks its kind and throws Node's TypeError for any other value.
const dynamic = type?.kind === "dyn";
if (
type?.kind !== "bytes" ||
!["i8", "u8", "i16", "u16", "i32", "u32"].includes(type.elem) ||
((member === "wait" || member === "notify") && type.elem !== "i32")
!dynamic &&
(type?.kind !== "bytes" ||
!["i8", "u8", "i16", "u16", "i32", "u32"].includes(type.elem) ||
((member === "wait" || member === "notify") && type.elem !== "i32"))
) {
lowerer.noLowering(
`Atomics.${member} over this array`,
@@ -82,7 +86,7 @@ export function lowerAtomicsCall(
if (member === "wait")
return {
kind: "libCall",
fn: "atomics.wait",
fn: dynamic ? "atomics.waitDyn" : "atomics.wait",
args: [array, index, arg(2, 0), arg(3, Infinity)],
type: STRING,
loc,
@@ -90,14 +94,14 @@ export function lowerAtomicsCall(
if (member === "notify")
return {
kind: "libCall",
fn: "atomics.notify",
fn: dynamic ? "atomics.notifyDyn" : "atomics.notify",
args: [array, index, arg(2, Infinity)],
type: F64,
loc,
};
return {
kind: "libCall",
fn: "atomics.op",
fn: dynamic ? "atomics.opDyn" : "atomics.op",
args: [array, index, arg(2, 0), arg(3, 0), number(operation)],
type: F64,
loc,
@@ -1,3 +1,4 @@
import { keyedHelperName } from "../stable-names.js";
import { recordToEnvPairs } from "./environment.js";
import { lowerChildArgsArg } from "./child-process.js";
import { boolLit, numLit, varRef } from "../../../ir/build.js";
@@ -218,7 +219,7 @@ function execFileAsyncHelper(
const key = shell ? "execAsync" : "execFileAsync";
const existing = lowerer.valueHelpers.get(key);
if (existing) return { name: existing, shapeId, envShapeId };
const name = `%${key}.${lowerer.valueHelpers.size}`;
const name = keyedHelperName(lowerer, `%${key}`, key);
lowerer.valueHelpers.set(key, name);
const recT: IrType = { kind: "record", shapeId };
const envRecT: IrType = { kind: "record", shapeId: envShapeId };
@@ -1,3 +1,4 @@
import { keyedHelperName } from "../stable-names.js";
import { recordToEnvPairs } from "./environment.js";
import { lowerChildArgsArg } from "./child-process.js";
import { boolLit, numLit, strLit, varRef } from "../../../ir/build.js";
@@ -541,7 +542,7 @@ function execStdioModeHelper(
const key = `cp.stdiomode:${shapeId}:${fd}`;
const existing = lowerer.arrHofHelpers.get(key);
if (existing) return existing;
const name = `%cp.stdioMode.${fd}.${lowerer.arrHofHelpers.size}`;
const name = keyedHelperName(lowerer, `%cp.stdioMode.${fd}`, key);
lowerer.arrHofHelpers.set(key, name);
const optsT: IrType = { kind: "record", shapeId };
@@ -1,12 +1,14 @@
import { lowerBuiltinByteInput, errorFirstBytesCallback, errorFirstCallback } from "./arguments.js";
import { isKeyObjectValue } from "./key-object.js";
import { lowerCreateEcdh } from "./ecdh.js";
import { lowerCreateDiffieHellman, lowerGetDiffieHellman } from "./dh.js";
import { boolLit, dynUndefinedExpr, nodeThrowExpr, varRef } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import { type Lowerer, own } from "../lowerer.js";
import { locOf } from "../../program.js";
import { CIPHER_TYPE_NAMES } from "../../type-mapper.js";
import { CRYPTO_CIPHERS, CRYPTO_CURVES, CRYPTO_HASHES } from "../crypto-tables.js";
import { stdlibGlobalNameOf } from "../surfaces.js";
import {
BOOL,
BYTES_U8,
@@ -410,6 +412,9 @@ export function lowerCryptoModuleCall(
};
}
if (bi.member === "createECDH") return lowerCreateEcdh(lowerer, expr);
if (bi.member === "createDiffieHellman") return lowerCreateDiffieHellman(lowerer, expr);
if (bi.member === "getDiffieHellman" || bi.member === "createDiffieHellmanGroup")
return lowerGetDiffieHellman(lowerer, expr);
if (bi.member === "createHmac") {
if (args.length < 1 || args.length > 3 || args.some(ts.isSpreadElement)) {
lowerer.noLowering(
@@ -695,6 +700,8 @@ export function lowerCryptoModuleCall(
loc,
};
}
const pki = lowerPkiCall(lowerer, expr, bi.member, args, loc);
if (pki) return pki;
if (bi.member === "createSecretKey") {
if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement))
lowerer.noLowering(
@@ -907,7 +914,8 @@ export function v8CalleeText(node: ts.Expression): string {
/** Calls on native Cipheriv/Decipheriv handles dispatch through the
* handle's methods with checked-dynamic arguments. The stream.Transform
* members the classes inherit are refused. */
* members the classes inherit lower over the cipher's Transform
* (lowerHashStreamCall, which runs first); other members are refused. */
export function lowerCipherMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -920,7 +928,7 @@ export function lowerCipherMethodCall(
lowerer.noLowering(
`Cipheriv/Decipheriv.${name}`,
call,
"update(), final(), setAAD(), setAutoPadding(), getAuthTag(), and setAuthTag() are supported; cipher streams are not",
"update(), final(), setAAD(), setAutoPadding(), getAuthTag(), setAuthTag(), and the stream members are supported",
);
}
if (call.arguments.nodes.some(ts.isSpreadElement))
@@ -1316,8 +1324,10 @@ export function lowerCryptoGetRandomValues(
}
/** Whether `receiver` names a WebCrypto Crypto object: the `crypto`
* global (also through globalThis) or node:crypto's `webcrypto`. */
* global (also through globalThis or an alias of it) or node:crypto's
* `webcrypto`. */
export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression): boolean {
if (stdlibGlobalNameOf(lowerer, receiver) === "crypto") return true;
if (ts.isIdentifier(receiver)) {
if (receiver.text === "crypto" && lowerer.isStdlibGlobal(receiver, "crypto")) return true;
const imported = lowerer.builtinImportOf(receiver);
@@ -1335,3 +1345,55 @@ export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression):
}
return false;
}
/** The asymmetric-key and signature members (scr_pki.c): checked-dynamic
* arguments, and the result converted to the call site's type
* (KeyObject, Sign and Verify are checked-dynamic handles; key pairs,
* signatures and verdicts convert to their records, Buffers and
* booleans). Null for other members. */
function lowerPkiCall(
lowerer: Lowerer,
expr: ts.CallExpression,
member: string,
args: readonly ts.Expression[],
loc: SrcLoc,
): IrExpr | null {
const forms: Readonly<Record<string, readonly [IrLibFn, number, number, string]>> = {
createPrivateKey: ["crypto.createPrivateKey", 1, 1, "createPrivateKey(key)"],
createPublicKey: ["crypto.createPublicKey", 1, 1, "createPublicKey(key)"],
generateKeyPairSync: ["crypto.generateKeyPairSync", 1, 2, "generateKeyPairSync(type, options)"],
generateKeyPair: [
"crypto.generateKeyPairCb",
2,
3,
"generateKeyPair(type, [options,] callback)",
],
sign: ["crypto.signOneshot", 3, 3, "sign(algorithm, data, key)"],
verify: ["crypto.verifyOneshot", 4, 4, "verify(algorithm, data, key, signature)"],
createSign: ["crypto.createSign", 1, 1, "createSign(algorithm)"],
createVerify: ["crypto.createVerify", 1, 1, "createVerify(algorithm)"],
};
const form = forms[member];
if (!form) return null;
const [fn, min, max, supported] = form;
if (args.length < min || args.length > max)
lowerer.noLowering(
`crypto.${member} with ${args.length} arguments`,
expr,
`${supported} is supported`,
);
// generateKeyPair(type, callback): Node reads a function in the options
// position as the callback.
const shifted = fn === "crypto.generateKeyPairCb" && args.length === 2;
const values: IrExpr[] = [];
for (let i = 0; i < max; i++) {
const node = shifted ? (i === 1 ? undefined : i === 2 ? args[1] : args[i]) : args[i];
values.push(dynArg(lowerer, node, loc));
}
if (fn === "crypto.generateKeyPairCb")
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;
return lowerer.coerceInto(expr, call, result);
}
@@ -0,0 +1,552 @@
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
import { type ClassInfo } from "../lower-classes.js";
import { DATE_METHOD_HINT, dateJsonType, dateValueMethod } from "./dates.js";
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
import {
BOOL,
DATE_T,
DYN,
F64,
NULL_T,
STRING,
VOID,
typeKey,
type IrExpr,
type IrStmt,
type IrType,
type SrcLoc,
} from "../../../ir/ir.js";
/** The hidden field holding the time value of a class declared `extends
* Date`, the first field of its layout. */
export const DATE_TIME_FIELD = "%time";
/** The class declared `extends Date` at the root of `info`'s base chain, or
* null. */
export function dateRootOf(info: ClassInfo | null | undefined): ClassInfo | null {
for (let c = info ?? null; c; c = c.base) if (c.extendsDate) return c;
return null;
}
/** Whether the program declares a class that extends Date. */
export function programDeclaresDateClass(lowerer: Lowerer): boolean {
for (const info of lowerer.classes.values()) if (info.extendsDate) return true;
return false;
}
/** The Date.prototype methods a class declared `extends Date` inherits.
* Those without a lowering refuse at the call. */
const DATE_PROTOTYPE_METHODS: ReadonlySet<string> = new Set([
"getTime",
"valueOf",
"toISOString",
"toUTCString",
"toJSON",
"toString",
"toDateString",
"toTimeString",
"toGMTString",
"toLocaleString",
"toLocaleDateString",
"toLocaleTimeString",
"getFullYear",
"getUTCFullYear",
"getYear",
"getMonth",
"getUTCMonth",
"getDate",
"getUTCDate",
"getDay",
"getUTCDay",
"getHours",
"getUTCHours",
"getMinutes",
"getUTCMinutes",
"getSeconds",
"getUTCSeconds",
"getMilliseconds",
"getUTCMilliseconds",
"getTimezoneOffset",
"setTime",
"setFullYear",
"setUTCFullYear",
"setYear",
"setMonth",
"setUTCMonth",
"setDate",
"setUTCDate",
"setHours",
"setUTCHours",
"setMinutes",
"setUTCMinutes",
"setSeconds",
"setUTCSeconds",
"setMilliseconds",
"setUTCMilliseconds",
]);
export const DATE_PROTOTYPE_METHOD_NAMES: readonly string[] = [...DATE_PROTOTYPE_METHODS];
export function isDatePrototypeMethod(name: string): boolean {
return DATE_PROTOTYPE_METHODS.has(name);
}
/** Whether `name` is a Date method an instance of `info` inherits from
* Date.prototype: its class chain is rooted at a class declared `extends
* Date` and declares no member of that name. The runtime's view of the
* instance answers it (scr_dyn_date_class_method). */
export function dateInheritedMember(lowerer: Lowerer, info: ClassInfo, name: string): boolean {
return (
dateRootOf(info) !== null &&
DATE_PROTOTYPE_METHODS.has(name) &&
!lowerer.findMethodOn(info, name) &&
!info.fields.has(name)
);
}
/** super(...args) in a class declared `extends Date`: the time value the
* Date constructor computes from the argument vector `args` (one dyn
* array), stored in the hidden field. */
export function dateSuperCall(thisRef: IrExpr, info: ClassInfo, args: IrExpr, loc: SrcLoc): IrStmt {
return {
kind: "fieldSet",
obj: thisRef,
className: info.def.name,
field: DATE_TIME_FIELD,
value: { kind: "libCall", fn: "date.argumentsTime", args: [args], type: DATE_T, loc },
loc,
};
}
/** The time value of `receiver`, an instance of a class rooted at `root`. */
function timeOf(lowerer: Lowerer, receiver: IrExpr, root: ClassInfo, loc: SrcLoc): IrExpr {
return {
kind: "fieldGet",
obj: lowerer.upcastTo(receiver, root.def.name),
className: root.def.name,
field: DATE_TIME_FIELD,
type: DATE_T,
loc,
};
}
/** `receiver.name()` with no arguments where the static class `info` does
* not declare `name`: the subclasses of `info` that declare it run theirs,
* any other instance the Date method. `receiver` is a variable read. */
function instanceMethodCall(
lowerer: Lowerer,
blame: ts.Node,
info: ClassInfo,
receiver: IrExpr,
name: string,
inherited: IrExpr,
loc: SrcLoc,
): IrExpr {
const found = lowerer.findMethodOn(info, name);
if (found) {
if (found.sig.params.length > 0)
lowerer.unsupported(
"SC1090",
blame,
`a '${name}' declaration with parameters in a class that extends Date, called by an inherited Date method`,
);
const args: IrExpr[] = [];
if (lowerer.overrideBelow(info, name)) {
lowerer.noteVirtualEdge(info, name);
return {
kind: "virtualCall",
className: info.def.name,
method: name,
args: [lowerer.upcastTo(receiver, info.def.name), ...args],
type: found.sig.ret,
loc,
};
}
lowerer.noteEdge(`%${found.declarer.def.name}.${name}`);
return {
kind: "call",
callee: `%${found.declarer.def.name}.${name}`,
args: [lowerer.upcastTo(receiver, found.declarer.def.name), ...args],
type: found.sig.ret,
loc,
};
}
let result = inherited;
// Deeper declarations are tested first.
const declaring: ClassInfo[] = [];
const visit = (c: ClassInfo): void => {
for (const sub of c.subclasses) {
if (sub.methods.has(name)) declaring.push(sub);
else visit(sub);
}
};
visit(info);
for (const sub of declaring) {
const call = instanceMethodCall(
lowerer,
blame,
sub,
{ kind: "downcast", value: receiver, type: { kind: "object", className: sub.def.name }, loc },
name,
inherited,
loc,
);
const type = result.type;
result = {
kind: "ternary",
cond: { kind: "instanceOf", value: receiver, className: sub.def.name, type: BOOL, loc },
then: lowerer.coerceToExpected(call, type),
else_: result,
type,
loc,
};
}
return result;
}
/** ToNumber of `value`, the lowered value of `node`. */
function numberValue(lowerer: Lowerer, node: ts.Expression, value: IrExpr, loc: SrcLoc): IrExpr {
if (value.type.kind === "f64") return value;
if (value.type.kind === "dyn")
return { kind: "libCall", fn: "dyn.toNumberCoerce", args: [value], type: F64, loc };
if (lowerer.dynConvertible(value.type))
return {
kind: "libCall",
fn: "dyn.toNumberCoerce",
args: [{ kind: "dynFrom", value, type: DYN, loc }],
type: F64,
loc,
};
return lowerer.coerceInto(node, value, F64);
}
/** A Date.prototype method a class rooted at `root` does not declare,
* called on `receiver` (an instance of `info`, evaluated here once). The
* method reads or sets the hidden time value; toJSON calls the instance's
* toISOString, as Date.prototype.toJSON does. */
export function lowerInheritedDateMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
name: string,
info: ClassInfo,
receiver: IrExpr,
): IrExpr {
const root = dateRootOf(info)!;
const loc = locOf(call);
const local = lowerer.declareHiddenLocal("%dateReceiver", receiver.type);
const value = varRef(local.id, receiver.type, loc);
const stmts: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: receiver, loc }];
const args = call.arguments.nodes;
if (args.some(ts.isSpreadElement))
lowerer.unsupported("SC1090", call, `spread arguments to 'Date.prototype.${name}'`);
const done = (result: IrExpr): IrExpr => ({
kind: "seqExpr",
stmts,
result,
type: result.type,
loc,
});
if (name === "setTime") {
// Every argument is evaluated before setTime converts the first one.
let ms: IrExpr = { kind: "numLit", value: Number.NaN, type: F64, loc };
if (args[0]) {
const first = lowerer.lowerExpr(args[0]);
const saved = lowerer.declareHiddenLocal("%dateArgument", first.type);
stmts.push({ kind: "varDecl", localId: saved.id, init: first, loc: locOf(args[0]) });
ms = numberValue(lowerer, args[0], varRef(saved.id, first.type, loc), loc);
}
for (const arg of args.slice(1))
stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) });
const time = lowerer.declareHiddenLocal("%dateTime", DATE_T);
stmts.push(
{
kind: "varDecl",
localId: time.id,
init: { kind: "libCall", fn: "date.newMs", args: [ms], type: DATE_T, loc },
loc,
},
{
kind: "fieldSet",
obj: lowerer.upcastTo(value, root.def.name),
className: root.def.name,
field: DATE_TIME_FIELD,
value: varRef(time.id, DATE_T, loc),
loc,
},
);
return done({
kind: "libCall",
fn: "date.getTime",
args: [varRef(time.id, DATE_T, loc)],
type: F64,
loc,
});
}
for (const arg of args) {
if (name !== "toJSON")
lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT);
stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) });
}
if (name === "toJSON") return done(dateClassToJson(lowerer, call, info, value, loc));
const result = dateValueMethod(name, timeOf(lowerer, value, root, loc), loc);
if (!result) lowerer.noLowering(`Date.prototype.${name}`, call, DATE_METHOD_HINT);
return done(result);
}
/** Date.prototype.toJSON on `value` (a variable read of an instance of
* `info`): null for an invalid time, else the instance's toISOString(). */
function dateClassToJson(
lowerer: Lowerer,
blame: ts.Node,
info: ClassInfo,
value: IrExpr,
loc: SrcLoc,
): IrExpr {
const root = dateRootOf(info)!;
const time: IrExpr = {
kind: "libCall",
fn: "date.getTime",
args: [timeOf(lowerer, value, root, loc)],
type: F64,
loc,
};
const type = dateJsonType(lowerer);
const iso = instanceMethodCall(
lowerer,
blame,
info,
value,
"toISOString",
{
kind: "libCall",
fn: "date.toISOStringValue",
args: [timeOf(lowerer, value, root, loc)],
type: STRING,
loc,
},
loc,
);
return {
kind: "ternary",
// An invalid date's time value is NaN, the one value unequal to itself.
cond: { kind: "bin", op: "!==", left: time, right: time, type: BOOL, loc },
then: lowerer.coerceToExpected({ kind: "unitLit", unit: "null", type: NULL_T, loc }, type),
else_: lowerer.coerceToExpected(iso, type),
type,
loc,
};
}
/** `super.name(...)` in a class whose base chain declares no `name`, when
* the chain is rooted at a class declared `extends Date`. */
export function lowerDateSuperMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
name: string,
cls: ClassInfo,
thisRef: IrExpr,
): IrExpr | null {
if (!dateRootOf(cls) || !isDatePrototypeMethod(name)) return null;
// Date.prototype.toJSON reads this.toISOString, which this class or a
// subclass may declare.
return lowerInheritedDateMethodCall(lowerer, call, name, cls, thisRef);
}
const DATE_CLASS_TIME = "%date.classTime";
const DATE_CLASS_METHOD = "%date.classMethod";
/** The classes of a program rooted at a class declared `extends Date`. */
function dateClasses(lowerer: Lowerer): ClassInfo[] {
return [...lowerer.classes.values()].filter(
(info) => !info.def.runtime && !info.generic && dateRootOf(info) !== null,
);
}
const PARAMS = ["op", "value", "argument"];
/** A lifted `(op, value, argument)` function over checked-dynamic values. */
function liftOperation(lowerer: Lowerer, name: string, body: IrStmt[], loc: SrcLoc): void {
lowerer.liftedFns.push({
name,
params: PARAMS.map((p) => ({ localId: p, name: p, type: DYN })),
returnType: DYN,
locals: PARAMS.map((p) => ({ id: p, name: p, type: DYN, mutable: false })),
body,
loc,
});
}
function opIs(code: number, loc: SrcLoc): IrExpr {
return {
kind: "bin",
op: "===",
left: { kind: "dynCheck", value: varRef("op", DYN, loc), type: F64, loc },
right: { kind: "numLit", value: code, type: F64, loc },
type: BOOL,
loc,
};
}
/** Lifts `%date.classMethod(op, value, argument)` when the program declares
* a class that extends Date: ops 2 to 5 call `value`'s toISOString,
* toJSON(argument), toString and valueOf as checked-dynamic method calls,
* which the class dispatch resolves to each class's declarations and the
* runtime to the inherited Date methods. Called before the class dispatch
* runs over the lowered functions. */
export function noteDateClassMethods(lowerer: Lowerer): void {
if (lowerer.liftedFnNamed(DATE_CLASS_METHOD)) return;
const classes = dateClasses(lowerer);
if (classes.length === 0) return;
const loc = classes[0]!.def.loc;
const value = varRef("value", DYN, loc);
const body: IrStmt[] = ["toISOString", "toJSON", "toString", "valueOf"].map(
(method, index): IrStmt => ({
kind: "if",
cond: opIs(index + 2, loc),
then: [
{
kind: "return",
value: {
kind: "dynInvoke",
recv: value,
method,
calleeName: method,
args: method === "toJSON" ? [varRef("argument", DYN, loc)] : [],
type: DYN,
loc,
},
loc,
},
],
else_: null,
loc,
}),
);
body.push({ kind: "return", value: dynUndefinedExpr(loc), loc });
liftOperation(lowerer, DATE_CLASS_METHOD, body, loc);
}
/** %main's installation of the functions the runtime calls for a
* checked-dynamic value that holds an instance of a class declared `extends
* Date` (scr_date_class_operation_install). `%date.classTime(op, value,
* argument)` answers the time value for op 0 (undefined for any other
* value), sets it from the number `argument` for op 1 and answers the
* class's name for op 6; `%date.classMethod` (noteDateClassMethods) calls
* its methods. */
export function dateClassOperationPrelude(lowerer: Lowerer, loc: SrcLoc): IrStmt[] {
const classes = dateClasses(lowerer);
if (classes.length === 0) return [];
noteDateClassMethods(lowerer);
const value = varRef("value", DYN, loc);
const argument = varRef("argument", DYN, loc);
const body: IrStmt[] = [];
for (const info of classes) {
const root = dateRootOf(info)!;
const type: IrType = { kind: "object", className: info.def.name };
const instance: IrExpr = { kind: "dynCheck", value, type, loc };
body.push({
kind: "if",
cond: {
kind: "libCall",
fn: "dyn.typedRefIs",
args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }],
type: BOOL,
loc,
},
then: [
{
kind: "if",
cond: opIs(6, loc),
then: [
{
kind: "return",
value: {
kind: "dynFrom",
value: {
kind: "strLit",
value: info.def.jsName ?? info.def.name,
type: STRING,
loc,
},
type: DYN,
loc,
},
loc,
},
],
else_: null,
loc,
},
{
kind: "if",
cond: opIs(1, loc),
then: [
{
kind: "fieldSet",
obj: lowerer.upcastTo(instance, root.def.name),
className: root.def.name,
field: DATE_TIME_FIELD,
value: {
kind: "libCall",
fn: "date.newMs",
args: [{ kind: "dynCheck", value: argument, type: F64, loc }],
type: DATE_T,
loc,
},
loc,
},
],
else_: null,
loc,
},
{
kind: "return",
value: {
kind: "dynFrom",
value: {
kind: "libCall",
fn: "date.getTime",
args: [timeOf(lowerer, instance, root, loc)],
type: F64,
loc,
},
type: DYN,
loc,
},
loc,
},
],
else_: null,
loc,
});
}
body.push({ kind: "return", value: dynUndefinedExpr(loc), loc });
liftOperation(lowerer, DATE_CLASS_TIME, body, loc);
const closure = (fnName: string): IrExpr => ({
kind: "dynFrom",
value: {
kind: "closure",
fnName,
captures: [],
type: { kind: "func", params: [DYN, DYN, DYN], ret: DYN },
loc,
},
type: DYN,
loc,
});
return [
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "date.classOperation",
args: [closure(DATE_CLASS_TIME), closure(DATE_CLASS_METHOD)],
type: VOID,
loc,
},
loc,
},
];
}
@@ -1,6 +1,8 @@
import * as ts from "../../ts7/adapter.js";
import { type Lowerer, own } from "../lowerer.js";
import { locOf } from "../../program.js";
import { tryLowerExpression } from "../expressions/try-lower-expression.js";
import { programDeclaresDateClass } from "./date-classes.js";
import {
BOOL,
F64,
@@ -9,6 +11,7 @@ import {
type IrStmt,
type IrType,
NULL_T,
type SrcLoc,
STRING,
typeKey,
} from "../../../ir/ir.js";
@@ -39,7 +42,7 @@ const DATE_TEXT_FNS: ReadonlyMap<string, IrLibFn> = new Map([
["toTimeString", "date.toTimeString"],
]);
const DATE_METHOD_HINT =
export const DATE_METHOD_HINT =
"getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale formatters have no lowering";
/** The Date slice: statics plus read-only Date values backed by one
@@ -52,7 +55,52 @@ export function lowerDateCall(
): IrExpr | null {
if (call.questionDotToken || access.questionDotToken) return null;
const loc = locOf(call);
if (lowerer.stdlibGlobalMember(access, "Date") === "now") {
const member = lowerer.stdlibGlobalMember(access, "Date");
if (member === "now" || member === "parse" || member === "UTC")
return lowerDateStaticCall(lowerer, call, member);
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date") return null;
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
// A Date a JavaScript file constructs is a mutable object; setTime on
// it takes the dynamic method call. So does every Date method on a
// checked-dynamic receiver when the program declares a class that extends
// Date: the value can be an instance, whose class may declare the method.
if (
(name === "setTime" || programDeclaresDateClass(lowerer)) &&
tryLowerExpression(lowerer, access.expression)?.type.kind === "dyn"
)
return null;
if (name === "toJSON") return lowerDateToJson(lowerer, call, access);
if (call.arguments.nodes.length !== 0) {
lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT);
}
const raw = lowerer.lowerExpr(access.expression);
const receiver: IrExpr =
raw.type.kind === "dyn"
? { kind: "libCall", fn: "date.checkedValue", args: [raw], type: { kind: "date" }, loc }
: raw;
if (receiver.type.kind !== "date")
lowerer.badType(access.expression, lowerer.typeOf(access.expression));
const result = dateValueMethod(name, receiver, loc);
if (result) return result;
lowerer.noLowering(
`Date.prototype.${name}`,
call,
DATE_METHOD_HINT,
lowerer.checker.getSymbolAtLocation(access.name),
);
}
/** Date.now(), Date.parse(text) and Date.UTC(...), also called through a
* class declared `extends Date`, which inherits them. Null for other
* names. */
export function lowerDateStaticCall(
lowerer: Lowerer,
call: ts.CallExpression,
name: string,
): IrExpr | null {
const loc = locOf(call);
if (name === "now") {
if (call.arguments.nodes.length !== 0) {
lowerer.noLowering(`Date.now with ${call.arguments.nodes.length} arguments`, call);
}
@@ -60,7 +108,7 @@ export function lowerDateCall(
}
// Date.parse uses the same date-string parser as new Date(dateString)
// (V8's grammar in the runtime), so their NaN and TimeClip behavior agree.
if (lowerer.stdlibGlobalMember(access, "Date") === "parse") {
if (name === "parse") {
if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) {
lowerer.noLowering(`Date.parse with ${call.arguments.nodes.length} arguments`, call);
}
@@ -72,7 +120,7 @@ export function lowerDateCall(
// TimeClip. Omitted trailing arguments complete with the spec's
// defaults (month 0, date 1, time parts 0); tsc pins every present
// argument to number, so the seven-f64 ABI is exact.
if (lowerer.stdlibGlobalMember(access, "Date") === "UTC") {
if (name === "UTC") {
if (
call.arguments.nodes.length < 1 ||
call.arguments.nodes.length > 7 ||
@@ -92,49 +140,38 @@ export function lowerDateCall(
}
return { kind: "libCall", fn: "date.utc", args, type: F64, loc };
}
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date") return null;
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
if (name === "toJSON") return lowerDateToJson(lowerer, call, access);
if (call.arguments.nodes.length !== 0) {
lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT);
}
const raw = lowerer.lowerExpr(access.expression);
const receiver: IrExpr =
raw.type.kind === "dyn"
? { kind: "libCall", fn: "date.checkedValue", args: [raw], type: { kind: "date" }, loc }
: raw;
if (receiver.type.kind !== "date")
lowerer.badType(access.expression, lowerer.typeOf(access.expression));
return null;
}
/** The Date methods without arguments that have a lowering, applied to the
* time value `value` (a `date`): getTime, valueOf, toISOString,
* toUTCString, the calendar getters and the text forms. Null for other
* names. */
export function dateValueMethod(name: string, value: IrExpr, loc: SrcLoc): IrExpr | null {
if (name === "getTime" || name === "valueOf") {
return {
kind: "libCall",
fn: name === "getTime" ? "date.getTime" : "date.valueOf",
args: [receiver],
args: [value],
type: F64,
loc,
};
}
if (name === "toISOString") {
return { kind: "libCall", fn: "date.toISOStringValue", args: [receiver], type: STRING, loc };
return { kind: "libCall", fn: "date.toISOStringValue", args: [value], type: STRING, loc };
}
if (name === "toUTCString") {
return { kind: "libCall", fn: "date.toUTCString", args: [receiver], type: STRING, loc };
return { kind: "libCall", fn: "date.toUTCString", args: [value], type: STRING, loc };
}
const fn = own(DATE_GETTER_FNS, name);
if (fn !== undefined) {
return { kind: "libCall", fn, args: [receiver], type: F64, loc };
return { kind: "libCall", fn, args: [value], type: F64, loc };
}
const textFn = DATE_TEXT_FNS.get(name);
if (textFn !== undefined) {
return { kind: "libCall", fn: textFn, args: [receiver], type: STRING, loc };
return { kind: "libCall", fn: textFn, args: [value], type: STRING, loc };
}
lowerer.noLowering(
`Date.prototype.${name}`,
call,
DATE_METHOD_HINT,
lowerer.checker.getSymbolAtLocation(access.name),
);
return null;
}
/** `d.toJSON(key?)`: Node answers null for an invalid date and the
@@ -0,0 +1,166 @@
import * as ts from "../../ts7/adapter.js";
import { nodeThrowExpr } from "../../../ir/build.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
import { DYN, type IrExpr, type IrStmt } from "../../../ir/ir.js";
import { v8CalleeText } from "./crypto.js";
import { dynArguments, refuseUnmatchedOverload } from "./ecdh.js";
/* node:crypto's DiffieHellman and DiffieHellmanGroup are native
* checked-dynamic handles (scr_dh.c): both types map to dyn, their methods
* dispatch through the handle, and these helpers lower
* createDiffieHellman, getDiffieHellman, createDiffieHellmanGroup and the
* method calls. The runtime applies Node's argument checks, so the
* constructors take any arguments; method calls are refused when their
* argument types leave the Buffer or string result undetermined. */
/** Whether a checker type is node:crypto's DiffieHellman class or its
* DiffieHellmanGroup alias, or a union of them. */
export function isDiffieHellmanType(lowerer: Lowerer, type: ts.Type): boolean {
if (type.isUnionType())
return ts.constituentTypes(type).every((part) => isDiffieHellmanType(lowerer, part));
const symbol = type.getAliasSymbol() ?? type.getSymbol();
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return false;
const name = ts.symbolName(symbol);
return name === "DiffieHellman" || name === "DiffieHellmanGroup";
}
function isDiffieHellmanGroupType(type: ts.Type): boolean {
if (type.isUnionType()) return ts.constituentTypes(type).some(isDiffieHellmanGroupType);
const symbol = type.getAliasSymbol();
return !!symbol && ts.symbolName(symbol) === "DiffieHellmanGroup";
}
function newHandle(
lowerer: Lowerer,
call: ts.CallExpression,
fn: "crypto.dhNew" | "crypto.dhGroupNew",
): IrExpr {
const loc = locOf(call);
const stmts: IrStmt[] = [];
const values = dynArguments(lowerer, call, fn === "crypto.dhNew" ? 4 : 1, stmts, "%dhInput");
return {
kind: "seqExpr",
stmts,
result: { kind: "libCall", fn, args: values, type: DYN, loc },
type: DYN,
loc,
};
}
/** crypto.createDiffieHellman(sizeOrKey, keyEncoding, generator,
* generatorEncoding): a native DiffieHellman handle. */
export function lowerCreateDiffieHellman(lowerer: Lowerer, call: ts.CallExpression): IrExpr {
return newHandle(lowerer, call, "crypto.dhNew");
}
/** crypto.getDiffieHellman(groupName) and createDiffieHellmanGroup(name):
* a native DiffieHellmanGroup handle. */
export function lowerGetDiffieHellman(lowerer: Lowerer, call: ts.CallExpression): IrExpr {
return newHandle(lowerer, call, "crypto.dhGroupNew");
}
const DH_METHODS = new Set([
"generateKeys",
"computeSecret",
"getPrime",
"getGenerator",
"getPublicKey",
"getPrivateKey",
"setPublicKey",
"setPrivateKey",
]);
/** Object.prototype methods, which a DiffieHellman inherits. */
const OBJECT_METHODS = new Set([
"constructor",
"hasOwnProperty",
"isPrototypeOf",
"propertyIsEnumerable",
"toLocaleString",
"toString",
"valueOf",
]);
/** The argument that selects each method's Buffer or string result: the
* output encoding. */
const OUTPUT_ENCODING_ARG: Record<string, number> = {
generateKeys: 0,
computeSecret: 2,
getPrime: 0,
getGenerator: 0,
getPublicKey: 0,
getPrivateKey: 0,
};
/** An output encoding typed any: the checker's chosen overload then types
* the result as a Buffer or a string that Node may not answer, so the call
* is refused rather than checked against that type at run time. */
function refuseAnyOutputEncoding(lowerer: Lowerer, call: ts.CallExpression, name: string): void {
const index = OUTPUT_ENCODING_ARG[name];
const arg = index === undefined ? undefined : call.arguments.nodes[index];
if (!arg || (lowerer.typeOf(arg).flags & ts.TypeFlags.Any) === 0) return;
lowerer.noLowering(
`DiffieHellman.${name} with an output encoding typed any`,
arg,
"type the encoding as a BinaryToTextEncoding such as 'hex', or as null or undefined for a Buffer",
);
}
/** Calls on native DiffieHellman and DiffieHellmanGroup handles. Methods
* dispatch through the handle with checked-dynamic arguments. */
export function lowerDiffieHellmanCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null;
const access = call.expression;
if (access.questionDotToken) return null;
const receiver = lowerer.typeOf(access.expression);
if (!isDiffieHellmanType(lowerer, receiver)) return null;
const name = access.name.text;
const loc = locOf(call);
const setter = name === "setPublicKey" || name === "setPrivateKey";
if (!DH_METHODS.has(name) || (setter && isDiffieHellmanGroupType(receiver))) {
if (OBJECT_METHODS.has(name))
lowerer.noLowering(
`DiffieHellman.${name}`,
call,
"generateKeys(), computeSecret(), getPrime(), getGenerator(), getPublicKey(), getPrivateKey(), " +
"setPublicKey(), setPrivateKey() and verifyError are supported",
);
// Node reads the member as undefined (a DiffieHellmanGroup has no key
// setters) and throws a TypeError once the receiver and arguments are
// evaluated.
const evaluated = [access.expression, ...call.arguments.nodes].map((node): IrStmt => ({
kind: "exprStmt",
expr: lowerer.lowerExpr(node),
loc,
}));
return {
kind: "seqExpr",
stmts: evaluated,
result: nodeThrowExpr(1, "", `${v8CalleeText(access)} is not a function`, DYN, loc),
type: DYN,
loc,
};
}
if (call.arguments.nodes.some(ts.isSpreadElement))
lowerer.noLowering(`DiffieHellman.${name} with spread arguments`, call);
refuseAnyOutputEncoding(lowerer, call, name);
refuseUnmatchedOverload(lowerer, call, `DiffieHellman.${name}`);
const invoke: IrExpr = {
kind: "dynInvoke",
recv: lowerer.lowerExprExpecting(access.expression, DYN),
method: name,
calleeName: v8CalleeText(access),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
};
// setPublicKey and setPrivateKey answer the receiver where @types/node
// declares void. Other results take the static representation of the
// declared overload the arguments match.
if (setter) return invoke;
const result = lowerer.irTypeOf(call);
return result.kind === "bytes" || result.kind === "string"
? lowerer.coerceInto(call, invoke, result)
: invoke;
}
@@ -1,3 +1,4 @@
import { keyedHelperName } from "../stable-names.js";
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
@@ -84,7 +85,7 @@ export function lowerDisposableStackUseCall(
const key = `disposableStack.use:${className}`;
let helper = lowerer.valueHelpers.get(key);
if (!helper) {
helper = `%disposableStack.use.${lowerer.valueHelpers.size}`;
helper = keyedHelperName(lowerer, "%disposableStack.use", key);
lowerer.valueHelpers.set(key, helper);
const context = newFnCtx(false, null, null, VOID);
lowerer.fnStack.push(context);
@@ -28,15 +28,16 @@ export function isEcdhType(lowerer: Lowerer, type: ts.Type): boolean {
return !!symbol && ts.symbolName(symbol) === "ECDH" && lowerer.isStdlibSymbol(symbol);
}
/** The call's arguments as checked-dynamic values in hidden locals, the
* first `count` of them padded with undefined, so every argument is
* evaluated left to right before the runtime checks them. Surplus
* arguments are evaluated and dropped. */
function dynArguments(
/** The call's arguments as checked-dynamic values in hidden locals named
* `local`, the first `count` of them padded with undefined, so every
* argument is evaluated left to right before the runtime checks them.
* Surplus arguments are evaluated and dropped. */
export function dynArguments(
lowerer: Lowerer,
call: ts.CallExpression,
count: number,
stmts: IrStmt[],
local = "%ecdhInput",
): IrExpr[] {
const args = call.arguments.nodes;
const loc = locOf(call);
@@ -45,7 +46,7 @@ function dynArguments(
const input: IrExpr = args[i]
? lowerer.lowerExprExpecting(args[i]!, DYN)
: dynUndefinedExpr(loc);
const saved = lowerer.declareHiddenLocal("%ecdhInput", DYN);
const saved = lowerer.declareHiddenLocal(local, DYN);
stmts.push({ kind: "varDecl", localId: saved.id, init: input, loc });
values.push(varRef(saved.id, DYN, loc));
}
@@ -137,7 +138,11 @@ function refuseViewKeyWithEncoding(lowerer: Lowerer, call: ts.CallExpression, na
* not follow (getPublicKey("HEX") answers a string where the encoding-less
* overload declares a Buffer), so the call is refused rather than checked
* against that type at run time. Arguments typed any are accepted. */
function refuseUnmatchedOverload(lowerer: Lowerer, call: ts.CallExpression, what: string): void {
export function refuseUnmatchedOverload(
lowerer: Lowerer,
call: ts.CallExpression,
what: string,
): void {
const signature = lowerer.checker.getResolvedSignature(call);
if (signature === undefined) return;
const params = signature.getParameters();
@@ -1,6 +1,8 @@
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
import { tryLowerExpression } from "../expressions/try-lower-expression.js";
import { dynUndefinedExpr } from "../../../ir/build.js";
import {
BOOL,
CAUGHT,
@@ -8,6 +10,7 @@ import {
DYN_CLASS_PROPERTIES,
F64,
type IrExpr,
type IrStmt,
type IrType,
STRING,
} from "../../../ir/ir.js";
@@ -267,6 +270,114 @@ function isErrorClassType(lowerer: Lowerer, type: IrType): boolean {
);
}
/** Error classes that construct when called without `new`. */
const CALLABLE_ERROR_CLASSES: ReadonlySet<string> = new Set([
"%Error",
"%TypeError",
"%RangeError",
"%SyntaxError",
"%ReferenceError",
"%EvalError",
"%URIError",
]);
/** `Error(message?, options?)` and `Error.call(thisArg, message?, options?)`
* (an ES5 constructor function's `Error.call(this)`), and the same for the
* other standard error classes: called as a function, the constructor
* returns a new error and ignores the receiver. `thisArg` is evaluated
* first. Null for other callees. */
export function lowerErrorFunctionCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
if (call.questionDotToken) return null;
let callee = call.expression;
let args = call.arguments.nodes;
let thisArg: ts.Expression | undefined;
const viaCall =
ts.isPropertyAccessExpression(callee) &&
!callee.questionDotToken &&
callee.name.text === "call" &&
ts.isIdentifier(callee.expression);
if (viaCall) {
callee = (callee as ts.PropertyAccessExpression).expression;
thisArg = args[0];
args = args.slice(1);
}
if (!ts.isIdentifier(callee) || args.some(ts.isSpreadElement)) return null;
if (thisArg && ts.isSpreadElement(thisArg)) return null;
const info = lowerer.builtinErrorInfoOf(lowerer.checker.getSymbolAtLocation(callee));
if (!info || !CALLABLE_ERROR_CLASSES.has(info.def.name)) return null;
const loc = locOf(call);
const stmts: IrStmt[] = [];
if (thisArg) {
const receiver = lowerer.lowerExpr(thisArg);
if (receiver.kind !== "varRef" && receiver.kind !== "unitLit")
stmts.push({ kind: "exprStmt", expr: receiver, loc: receiver.loc });
}
const error: IrExpr = {
kind: "libCall",
fn: "error.newOptions",
args: lowerer.errorConstructorArgs(args, loc, call),
type: { kind: "object", className: info.def.name },
loc,
};
return stmts.length > 0
? { kind: "seqExpr", stmts, result: error, type: error.type, loc }
: error;
}
/** `Error.captureStackTrace(target[, constructorOpt])` (also called as
* `Error.captureStackTrace?.(...)`, which always calls it). On a
* checked-dynamic target (`this` in a JavaScript constructor function) the
* runtime defines target's `stack`. An instance of a class that extends
* Error derives its stack from its name and message when the stack is
* read, so the call only evaluates its arguments. Both arguments are
* evaluated; the second only selects frames in Node. Null for other
* targets and callees. */
export function lowerErrorCaptureStackTraceCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (access.questionDotToken) return null;
if (lowerer.stdlibGlobalMember(access, "Error") !== "captureStackTrace") return null;
const args = call.arguments.nodes;
if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) return null;
const probed = tryLowerExpression(lowerer, args[0]!);
const loc = locOf(call);
if (
probed?.type.kind === "object" &&
(probed.type.className === "%Error" || lowerer.isSubclassOf(probed.type.className, "%Error"))
) {
const stmts: IrStmt[] = [];
for (const arg of args) {
const value = lowerer.lowerExpr(arg);
if (value.kind !== "varRef") stmts.push({ kind: "exprStmt", expr: value, loc: value.loc });
}
const result = dynUndefinedExpr(loc);
return stmts.length > 0 ? { kind: "seqExpr", stmts, result, type: DYN, loc } : result;
}
if (probed?.type.kind !== "dyn") return null;
const target = lowerer.lowerExpr(args[0]!);
const result: IrExpr = {
kind: "libCall",
fn: "dyn.errorCaptureStackTrace",
args: [target],
type: DYN,
loc,
};
if (args.length === 1) return result;
const local = lowerer.declareHiddenLocal("%captureTarget", DYN);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: local.id, init: target, loc },
{ kind: "exprStmt", expr: lowerer.lowerExprExpecting(args[1]!, DYN), loc },
],
result: { ...result, args: [{ kind: "varRef", localId: local.id, type: DYN, loc }] },
type: DYN,
loc,
};
}
/** `Error.isError(value)`. A catch binding tests its payload against the
* Error class, an `unknown` value tests the checked-dynamic tree's error
* encoding, and a union tests its runtime tag against the arms whose class
@@ -1,3 +1,4 @@
import { keyedHelperName } from "../stable-names.js";
import { dynUndefinedExpr, boolLit, numLit, varRef } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import { type Lowerer, ladderFenceExpr } from "../lowerer.js";
@@ -442,7 +443,7 @@ export function lowerStringOrBytesWrite(
const key = `fs.${append ? "append" : "write"}:${dataType.unionId}`;
let helper = lowerer.valueHelpers.get(key);
if (!helper) {
helper = `%fs.writeData.${lowerer.valueHelpers.size}`;
helper = keyedHelperName(lowerer, "%fs.writeData", key);
lowerer.valueHelpers.set(key, helper);
const body: IrStmt[] = [];
for (let tag = 0; tag < arms.length; tag++) {
@@ -1,17 +1,27 @@
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
import type { SrcLoc } from "../../../ir/ir.js";
import { varRef } from "../../../ir/build.js";
import { type IrExpr, type IrType } from "../../../ir/ir.js";
import { type IrExpr, type IrStmt, type IrType } from "../../../ir/ir.js";
import { withStreamReceiver } from "../dyn-streams.js";
import { EMITTER_API_MEMBERS } from "../lower-classes.js";
import { lowerEmitterMethodCall } from "../lower-event-emitter.js";
import { STREAM_API_MEMBERS, STREAM_OPERATORS, lowerStreamMethodCall } from "../lower-stream.js";
import {
STREAM_API_MEMBERS,
STREAM_OPERATORS,
lowerStreamMethodCall,
lowerStreamProperty,
} from "../lower-stream.js";
import { isCipherType } from "./crypto.js";
/* node:crypto's Hash and Hmac are Transforms: a value written, read,
* listened to, or piped as a stream reads and writes through its own
* native Transform (scr_stream.c), created on first use. update() and
* digest() keep the value's static lowering; the stream members lower
* through the stream and emitter spokes over that Transform. */
/* node:crypto's Hash, Hmac, Cipheriv and Decipheriv are Transforms: a
* value written, read, listened to, or piped as a stream reads and writes
* through its own native Transform, which scr_stream.c creates on first
* use. update(), digest() and final() keep the value's own lowering; the
* stream members lower through the stream and emitter spokes over that
* Transform. A Hash or Hmac is known by its IR type; a cipher is a
* checked-dynamic handle and is known by its checker type. */
const TRANSFORM_CLASS = "%Transform";
const TRANSFORM: IrType = { kind: "object", className: TRANSFORM_CLASS };
@@ -20,6 +30,12 @@ export function isHashValueType(type: IrType | null | undefined): boolean {
return type?.kind === "cryptoHash" || type?.kind === "cryptoHmac";
}
/** Whether `node` evaluates to a Hash, Hmac, Cipheriv or Decipheriv. */
export function isValueStream(lowerer: Lowerer, node: ts.Expression): boolean {
const type = lowerer.typeOf(node);
return isHashValueType(lowerer.mapTypeOf(type)) || isCipherType(lowerer, type);
}
/** The Transform of a lowered Hash or Hmac value (+1). A pipe
* destination or pipeline stage (`piped`) also keeps the value alive
* until its stream flushes or is destroyed. */
@@ -33,9 +49,27 @@ export function hashStreamOf(value: IrExpr, piped = false): IrExpr {
};
}
/** A stream member called on a Hash or Hmac (`hash.write(chunk)`,
* `hash.on("data", cb)`, `hash.setEncoding("hex")`). Members that answer
* the stream (`this`) answer the Hash itself. */
/** The Transform of a lowered Hash, Hmac, Cipheriv or Decipheriv value
* (+1), with hashStreamOf's `piped` (a cipher's stream always holds its
* cipher). */
export function valueStreamOf(value: IrExpr, piped = false): IrExpr {
if (isHashValueType(value.type)) return hashStreamOf(value, piped);
// A value already lowered as its stream (addAbortSignal answers the
// Transform it was given, which its generic signature types as the
// cipher).
if (value.type.kind === "object") return value;
return {
kind: "libCall",
fn: "stream.handleStream",
args: [value],
type: TRANSFORM,
loc: value.loc,
};
}
/** A stream member called on a Hash, Hmac, Cipheriv or Decipheriv
* (`hash.write(chunk)`, `cipher.on("data", cb)`, `hash.setEncoding("hex")`).
* Members that answer the stream (`this`) answer the value itself. */
export function lowerHashStreamCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -45,39 +79,96 @@ export function lowerHashStreamCall(
const name = access.name.text;
const streamMember = STREAM_API_MEMBERS.has(name) || STREAM_OPERATORS.has(name);
if (!streamMember && !EMITTER_API_MEMBERS.has(name)) return null;
if (!isHashValueType(lowerer.mapTypeOf(lowerer.typeOf(access.expression)))) return null;
if (!isValueStream(lowerer, access.expression)) return null;
const info = lowerer.classes.get(TRANSFORM_CLASS);
if (!info) return null;
const loc = locOf(call);
const hash = lowerer.lowerExpr(access.expression);
const local = lowerer.declareHiddenLocal("%hash", hash.type);
// The spokes lower the receiver to the value's Transform.
lowerer.hashStreamReceivers.set(access.expression, { localId: local.id, type: hash.type });
// pipe(dest) answers its destination, which may be a Transform too;
// the other members typed as the Transform answer `this`
const answersThis = name !== "pipe";
return withValueStreamReceiver(lowerer, access.expression, locOf(call), answersThis, () =>
streamMember
? lowerStreamMethodCall(lowerer, call, access, info)
: lowerEmitterMethodCall(lowerer, call, access, info),
);
}
/** A stream property read on a Hash, Hmac, Cipheriv or Decipheriv
* (`cipher.destroyed`, `hash.readableEnded`). */
export function lowerValueStreamProperty(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
): IrExpr | null {
if (expr.questionDotToken || !isValueStream(lowerer, expr.expression)) return null;
const info = lowerer.classes.get(TRANSFORM_CLASS);
if (!info) return null;
return withValueStreamReceiver(lowerer, expr.expression, locOf(expr), true, () =>
lowerStreamProperty(lowerer, expr, info),
);
}
/** Evaluates the value `receiver` names once into a hidden local and runs
* `lower` with the receiver lowering to that value's Transform. When
* `answersThis`, a result typed as the Transform (the member answered
* `this`) answers the value itself. */
function withValueStreamReceiver(
lowerer: Lowerer,
receiver: ts.Expression,
loc: SrcLoc,
answersThis: boolean,
lower: () => IrExpr | null,
): IrExpr | null {
const value = lowerer.lowerExpr(receiver);
const local = lowerer.declareHiddenLocal("%streamValue", value.type);
lowerer.valueStreamReceivers.set(receiver, { localId: local.id, type: value.type });
let lowered: IrExpr | null;
try {
lowered = streamMember
? lowerStreamMethodCall(lowerer, call, access, info)
: lowerEmitterMethodCall(lowerer, call, access, info);
lowered = lower();
} finally {
lowerer.hashStreamReceivers.delete(access.expression);
lowerer.valueStreamReceivers.delete(receiver);
}
if (!lowered) return null;
const answersStream =
lowered.type.kind === "object" && lowered.type.className === TRANSFORM_CLASS;
answersThis && lowered.type.kind === "object" && lowered.type.className === TRANSFORM_CLASS;
const result: IrExpr = answersStream
? {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: lowered, loc }],
result: varRef(local.id, hash.type, loc),
type: hash.type,
result: varRef(local.id, value.type, loc),
type: value.type,
loc,
}
: lowered;
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: local.id, init: hash, loc }],
stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }],
result,
type: result.type,
loc,
};
}
/** `for await (const chunk of value)` over a Hash, Hmac, Cipheriv or
* Decipheriv: the loop iterates the value's Transform. */
export function lowerForAwaitValueStream(
lowerer: Lowerer,
stmt: ts.ForOfStatement,
lowerLoop: () => IrStmt | null,
): IrStmt | null {
if (!isValueStream(lowerer, stmt.expression)) return null;
const loc = locOf(stmt.expression);
const local = lowerer.declareHiddenLocal("%streamIterated", TRANSFORM);
const init = valueStreamOf(lowerer.lowerExpr(stmt.expression));
const loop = withStreamReceiver(
lowerer,
stmt.expression,
varRef(local.id, TRANSFORM, loc),
TRANSFORM,
lowerLoop,
);
if (loop === null) return null;
return {
kind: "block",
body: [{ kind: "varDecl", localId: local.id, init, loc }, loop],
loc: locOf(stmt),
};
}
@@ -1,3 +1,4 @@
import { keyedHelperName } from "../stable-names.js";
import { isDateJsonSource } from "./dates.js";
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
import { InternalCompilerError } from "../../../errors.js";
@@ -253,7 +254,7 @@ function lowerOptionalStringifyRoot(
const key = `json.optionalString:${value.type.unionId}:${JSON.stringify(indent)}`;
let helper = lowerer.valueHelpers.get(key);
if (!helper) {
helper = `%json.optionalString.${lowerer.valueHelpers.size}`;
helper = keyedHelperName(lowerer, "%json.optionalString", key);
lowerer.valueHelpers.set(key, helper);
const input = varRef("value.0", value.type, loc);
const serialized: IrExpr = {

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