Merge branch 'main' of https://github.com/vercel-labs/scriptc into spli-commonjs-require-bindings

This commit is contained in:
Luis Alvarez D
2026-10-22 23:05:49 -05:00
1641 changed files with 102817 additions and 6502 deletions
+26 -7
View File
@@ -364,6 +364,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:
@@ -759,17 +773,22 @@ jobs:
run: >-
pnpm exec vitest run tests/harness/worktree-files.test.ts
--testNamePattern "workspace reset keeps Linux Sandbox paths POSIX"
# The Node host checks with tsc-ts, the default, over the named pipe it
# serves on Windows, and with tsgo.
- name: Windows scriptc run smoke
shell: pwsh
run: |
zig version
$output = & node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend llvm
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$actual = ($output -join "`n").Trim()
if ($actual -ne "hello world") {
throw "unexpected scriptc output: $actual"
foreach ($checker in @("tsc-ts", "tsgo")) {
$env:SCRIPTC_CHECKER = $checker
$output = & node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend llvm
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$actual = ($output -join "`n").Trim()
if ($actual -ne "hello world") {
throw "unexpected scriptc output with ${checker}: $actual"
}
}
- name: Windows PE subsystem selection
env:
+1
View File
@@ -9,6 +9,7 @@ node_modules/
!tests/corpus/*/node_modules/
!tests/fixtures/npm-static/node_modules/
!tests/coverage-fixtures/wasm/node_modules/
!tests/coverage-fixtures/dynamic-tier/package/node_modules/
!tests/fixtures/npm-static/nested/inner/node_modules/
!tests/fixtures/npm-static/node_modules/*/node_modules/
!tests/fixtures/npm/store/*/node_modules/
+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
+2
View File
@@ -11,6 +11,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
| `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any |
| `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` |
| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner |
| `dictionary-walks` | cpu | `Record<string, number>` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted |
| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
@@ -26,6 +27,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/dictionary-walks.ts",
"args": [
"1"
]
}
@@ -0,0 +1,53 @@
// String-keyed dictionaries (`Record<string, number>`) walked with for-in,
// Object.keys, Object.values and Object.entries while entries are added,
// updated and deleted: per-category totals, a pruning pass and a merge.
let seed = 777;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
const scale = Number(process.argv[2] ?? "1");
const categories = 4000;
const counts: Record<string, number> = {};
for (let i = 0; i < Math.floor(200000 * scale); i++) {
const key = "c" + Math.floor(random() * categories);
counts[key] = (counts[key] ?? 0) + 1;
}
let walked = 0;
let sum = 0;
for (let pass = 0; pass < 40; pass++) {
for (const key in counts) {
walked++;
sum += counts[key]!;
}
}
// Prune: delete the small categories during the walk, including keys the
// walk has not reached yet.
let pruned = 0;
for (const key in counts) {
const n = counts[key]!;
if (n < 45) {
delete counts[key];
pruned++;
}
const ahead = "c" + ((Number(key.slice(1)) + 1) % categories);
if (n > 60 && ahead in counts && counts[ahead]! < 45) {
delete counts[ahead];
pruned++;
}
}
// Merge into a second dictionary and read it back every way.
const merged: Record<string, number> = {};
for (const key of Object.keys(counts)) merged[key.toUpperCase()] = counts[key]! * 2;
let values = 0;
for (const v of Object.values(merged)) values += v;
let entries = 0;
for (const [key, v] of Object.entries(merged)) entries += key.length + v;
console.log(
`walked ${walked}, sum ${sum}, pruned ${pruned}, kept ${Object.keys(counts).length}, values ${values}, entries ${entries}`,
);
@@ -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)}`);
+6 -3
View File
@@ -114,8 +114,8 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
<dt><code>--print &lt;native-link-info|diagnostics&gt;</code></dt>
<dd><code>native-link-info</code>: build an object (equivalent to <code>--emit=obj</code>) and print its machine-readable external link recipe as JSON instead of printing the artifact path. The document names the exact installed precompiled runtime pack and all link inputs, but does not invoke a linker. <code>diagnostics</code>: for <code>build</code> and <code>coverage</code>, print the versioned JSON diagnostics document described in <a href="/docs/coverage#machine-readable-diagnostics">Coverage Reports</a> instead of the human output.</dd>
<dt><code>--fail-on &lt;blockers|divergences&gt;</code></dt>
<dd>Coverage only. Exit with status 1 when the entry has blockers that would fail a build, or with <code>divergences</code>, also when it has Node divergence warnings. See <a href="/docs/coverage#gating-builds-and-ci">Gating builds and CI</a>.</dd>
<dt><code>--fail-on &lt;blockers|divergences|dynamic&gt;</code></dt>
<dd>Coverage only. Exit with status 1 when the entry has blockers that would fail a build, with <code>divergences</code>, also when it has Node divergence warnings, or with <code>dynamic</code>, also when it has the <code>SC6101</code> warning for untyped code that likely runs often. See <a href="/docs/coverage#gating-builds-and-ci">Gating builds and CI</a>.</dd>
<dt><code>--ffi &lt;file&gt;</code></dt>
<dd>Bind signature-only TypeScript declarations to native C ABI symbols and link the manifest's archive, object, and system-library inputs. See <a href="/docs/ffi">Native FFI</a>.</dd>
@@ -156,6 +156,9 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
<dt><code>--explain-dynamic[=&lt;name&gt;]</code></dt>
<dd>Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as <code>x</code>, <code>Point</code>, or <code>Point.x</code>, show only the matching units. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
<dt><code>--no-dynamic-tier-warning</code></dt>
<dd>Build and coverage. Leave out <code>SC6101</code>, the warning that names functions that likely run often and use untyped values. Not allowed with <code>--fail-on=dynamic</code>. See <a href="/docs/coverage#untyped-code-that-likely-runs-often">Untyped code that likely runs often</a>.</dd>
<dt><code>--wasm &lt;file.wasm&gt;</code></dt>
<dd>Coverage only. Report a WebAssembly module the program loads from a path its sources do not spell out. The report already lists modules whose paths the compiler resolves, modules inlined as base64 literals, and <code>.wasm</code> files shipped by packages that use WebAssembly; a <code>scriptc.wasm</code> array in <code>package.json</code> adds files the same way as this flag. Each module is listed with its size, whether it validates, and the WebAssembly proposals it uses. Builds translate the modules they find to native code; a build reads the <code>scriptc.wasm</code> array but not this flag. Repeatable.</dd>
@@ -203,7 +206,7 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
<dd>A runtime pack directory to use instead of an installed <code>@scriptc/runtime-&lt;target&gt;</code> package.</dd>
<dt><code>SCRIPTC_CHECKER</code></dt>
<dd>The TypeScript 7 API server that checks the program: <code>tsc-ts</code> (the default), which scriptc includes, or <code>tsgo</code>, from the <code>typescript</code> package scriptc also includes. Both give the same diagnostics and generated code. On Windows the default is <code>tsgo</code>, because tsc-ts's API server does not yet accept the named pipe scriptc connects over there.</dd>
<dd>The TypeScript 7 API server that checks the program: <code>tsc-ts</code> (the default), which scriptc includes, or <code>tsgo</code>, from the <code>typescript</code> package scriptc also includes. Both give the same diagnostics and generated code.</dd>
</dl>
```console
+3 -1
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, as does each element of an array of such a type and each value of a `Map` with `string` keys and such a value type. 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;
@@ -358,7 +360,7 @@ A class can be both `ProgramLifetime` and `Acyclic`. `Acyclic` is still checked
Instances can be published to other threads with `@scriptc/threads` like other class instances: `publish()` makes them immutable and shares them by reference, writes to a published instance throw the same `TypeError` as under Node.js, and instances that were not published stay writable and are cloned by `postMessage` and `workerData`.
Static code has no `WeakRef` or `FinalizationRegistry`, and a `WeakMap` or `WeakSet` entry whose key is never freed is never observable as removed, so nothing else can tell the difference.
Static code has no `WeakRef`, a `FinalizationRegistry` never runs its cleanup callbacks, and a `WeakMap` or `WeakSet` entry whose key is never freed is never observable as removed, so nothing else can tell the difference.
### Lifetime regions
+28 -3
View File
@@ -38,6 +38,8 @@ When a program has unsupported operations, the report adds a group for each root
<tr><td><code>compile statically</code></td><td>Statements that compile to native instructions.</td></tr>
<tr><td><code>in dynamic tier</code></td><td>Of those, the statements in functions that operate on values typed only at runtime, such as <code>any</code> values and untyped JavaScript. Shown when there are any. See <a href="#dynamic-tier">Dynamic tier</a>.</td></tr>
<tr><td><code>blockers</code></td><td>Unsupported operations with counts, diagnostic codes, reasons, locations, and hints.</td></tr>
<tr><td><code>runs at startup</code></td><td>Deferred operations in a JavaScript module's top-level code, which runs when the module loads: at startup for a module the program imports.</td></tr>
<tr><td><code>deferred to runtime</code></td><td>Other deferred operations in JavaScript code. Each throws only if it runs.</td></tr>
<tr><td><code>in unreached code</code></td><td>Blockers in code the entry never reaches. A build never compiles that code, so these cannot fail it.</td></tr>
<tr><td><code>differs from Node</code></td><td>Sites that compile but can behave differently from Node.js. See <a href="#node-divergences">Node divergences</a>.</td></tr>
<tr><td><code>npm package compilation</code></td><td>Native package compilation outcomes and refusal reasons.</td></tr>
@@ -46,7 +48,7 @@ When a program has unsupported operations, the report adds a group for each root
Blockers in unreached code and deferred operations in JavaScript packages are listed separately because they don't fail a build. A deferred operation throws a runtime error if it runs, so a successful build doesn't mean every API of a package works; see [unsupported code inside a package](/docs/dependencies#unsupported-code-inside-a-package).
If a function's signature can't compile, the statements in its body count as not compiling, so the percentage reflects how much of the program compiles.
If a function's signature can't compile, the statements in its body count as not compiling, so the percentage reflects how much of the program compiles. The report still lists the diagnostics of those statements; a use of a parameter whose type can't compile reports `SC2004` back to the signature.
<span id="programs-that-require-dynamic-execution" />
<span id="analyze-with-dynamic" />
@@ -62,6 +64,25 @@ Code that operates on values whose types are known only at runtime, such as `any
`--tier=static-only` keeps every unsupported construct a blocker instead of moving it to the dynamic tier; code that already operates on runtime-typed values compiles as before. Today `auto`, the default, behaves the same way.
### Untyped code that likely runs often
When functions of the program's own files use untyped values and likely run often, `scriptc build` and coverage report them as an `SC6101` warning. For the Benchmarks Game n-body program in plain JavaScript:
```console
$ scriptc build nbody.js -o nbody
warning SC6101: 3 functions that likely run often use untyped values, which runs slower than typed code:
advance (nbody.js:22): untyped JavaScript file, in a loop
energy (nbody.js:40): untyped JavaScript file, in a loop
offsetMomentum (nbody.js:12): untyped JavaScript file, in a loop
hint: add types to compile these statically; see scriptc coverage --explain-dynamic
The program compiled. Adding types to these functions lets scriptc compile them to faster code.
```
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. 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
Coverage and `scriptc build` report the same diagnostic codes. Common ones include:
@@ -88,7 +109,7 @@ By default, `scriptc coverage` exits with status 0 whenever the program type-che
$ scriptc coverage src/main.ts --fail-on=blockers
```
`--fail-on=blockers` exits with status 1 when the program has blockers that would fail `scriptc build`. Blockers in unreached code and deferred operations don't count. `--fail-on=divergences` also fails on Node divergences. A program that doesn't type-check always exits with status 1.
`--fail-on=blockers` exits with status 1 when the program has blockers that would fail `scriptc build`. Blockers in unreached code and deferred operations don't count. `--fail-on=divergences` also fails on Node divergences, and `--fail-on=dynamic` also fails on the `SC6101` warning for [untyped code that likely runs often](#untyped-code-that-likely-runs-often). A program that doesn't type-check always exits with status 1.
Coverage caches its result. Running it again on an unchanged program, or after edits that only touch comments, reuses the previous verdict. Set `SCRIPTC_NO_CACHE=1` to turn the cache off.
@@ -174,7 +195,7 @@ For editors, CI annotations, and other tools, `scriptc coverage --print=diagnost
</tr>
<tr>
<td><code>category</code></td>
<td><code>invalid-source</code> (the program has a TypeScript or JSON error), <code>unsupported</code> (valid code scriptc cannot compile yet), <code>divergence</code> (compiles, but can behave differently from Node.js), <code>environment</code> (project configuration, scriptc's type world, toolchain, or target), or <code>internal</code> (a compiler bug).</td>
<td><code>invalid-source</code> (the program has a TypeScript or JSON error), <code>unsupported</code> (valid code scriptc cannot compile yet), <code>divergence</code> (compiles, but can behave differently from Node.js), <code>performance</code> (compiles and behaves as under Node.js, but runs slower than typed code would), <code>environment</code> (project configuration, scriptc's type world, toolchain, or target), or <code>internal</code> (a compiler bug).</td>
</tr>
<tr>
<td><code>severity</code></td>
@@ -192,6 +213,10 @@ For editors, CI annotations, and other tools, `scriptc coverage --print=diagnost
<td><code>group</code></td>
<td>The <code>id</code> of the root-cause group the diagnostic belongs to. A group's <code>primary</code> site is its first in source order.</td>
</tr>
<tr>
<td><code>startup</code></td>
<td><code>true</code> on a <code>deferred</code> diagnostic in a module's top-level code, which runs when the module loads. Absent otherwise.</td>
</tr>
</tbody>
</table>
+1 -1
View File
@@ -132,7 +132,7 @@ CommonJS packages compile with Node.js's module semantics. Each `require()` eval
## Unsupported code inside a package
A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses.
A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses. Sites in a module's top-level code are listed under `runs at startup`: that code runs when the module loads, at startup for a module the program imports.
## Provenance sources (experimental)
+6
View File
@@ -24,6 +24,8 @@ title: "Limitations"
<include>../limitations/language-features/async-iteration.mdx</include>
<include>../limitations/language-features/iterable-values.mdx</include>
<include>../limitations/language-features/generics.mdx</include>
<include>../limitations/language-features/function-values.mdx</include>
@@ -34,6 +36,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>
@@ -82,6 +86,8 @@ title: "Limitations"
<include>../limitations/builtin-apis/filesystem-flags.mdx</include>
<include>../limitations/builtin-apis/filesystem-url-paths.mdx</include>
<include>../limitations/builtin-apis/copy.mdx</include>
<include>../limitations/builtin-apis/glob.mdx</include>
@@ -4,16 +4,45 @@
`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, GCM, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede3 in CBC, ECB, CFB, CFB8, CFB1, and OFB modes, des-ede in CBC, ECB, CFB, and OFB modes, chacha20, 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.
`checkPrime`, `checkPrimeSync`, `generatePrime`, and `generatePrimeSync` support bigint, ArrayBuffer, typed-array, Buffer, and DataView values and the `checks`, `safe`, `add`, `rem`, and `bigint` options on native targets, with Node's errors; the callback forms compute before they return and call back in a later macrotask.
Candidates and sizes longer than 262144 bits throw a coded SC2020 error, and wasm32-wasi builds are refused.
Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`.
RSA, EC, and Ed25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets.
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.
@@ -0,0 +1,3 @@
- **Filesystem URL paths:** Synchronous `fs` functions accept `file:` URL paths and convert them as `fileURLToPath` does.
`existsSync` and the `fs.promises` functions refuse URL paths; pass `fileURLToPath(url)` instead.
A URL path after a string path, or with options that are not literals, is refused, because Node checks those arguments before it converts the URL.
@@ -7,4 +7,6 @@
They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`.
They also support the iterator helper methods, such as `.map()` and `.take()`.
Detached collection methods are unsupported.
Constructing a Map, Set, WeakMap or WeakSet, or calling `Object.fromEntries`, with a checked-dynamic iterable (a native iterator, a generator, or an object with a `Symbol.iterator` method) takes all of its values before adding the first one.
When an entry of a Map seed is not an object, the TypeError comes after the iterable is exhausted; Node closes the iterator at that entry.
The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported.
@@ -3,4 +3,6 @@
Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly.
Named reads such as `value.valueOf` answer these inherited members; computed-key reads such as `value[key]` do not.
`Object.prototype`'s own members read as values; its mutation and other reflection, such as `isPrototypeOf` and `__proto__`, are unsupported.
The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called, and `instanceof` and `new` against them remain unsupported.
The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called.
`new` through a stored `Object`, `Date`, `Map`, `Set`, `WeakMap`, or `WeakSet` builds the same value as the `new` expression.
`new` through a stored `RegExp` or `Promise`, `new` of a stored `Object` with a primitive argument, and `instanceof` against these constructors are unsupported.
@@ -1,24 +1,42 @@
- **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 any value it holds.
`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 })`), an async generator function as the destination, 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.
With a function destination, `pipeline()` returns an object-mode PassThrough that receives an async generator destination's values, or an async function destination's value unless it is `null` or `undefined`.
`@types/node` types that result as `NodeJS.WritableStream`, so `for await` over it needs the `PassThrough` type (`pipeline(...) as PassThrough`, or a JSDoc `@type`).
In typed code, the callback of a pipeline with an async function destination receives `null` instead of `undefined` when there is no error.
`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.
`stream.compose` accepts two or more streams, or one `Duplex`; functions, iterables, and web streams as stages are unsupported, and the `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream.
`pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags.
The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options.
Their callbacks may take the `{ signal }` argument, `map` may return any value, and `flatMap` may return any iterable, async iterable, or stream.
`Duplex.from` accepts an async generator function, which reads what is written to the Duplex, a `Duplex`, `Transform` or `PassThrough`, which it returns, and the values `Readable.from` accepts, which give a Duplex that is not writable.
It also accepts a lone `Readable` or `Writable` and a `{ readable, writable }` pair, which the Duplex reads and writes through, a promise, whose value it reads, and an async function, which reads what is written and must resolve to `null` or `undefined`.
A `Duplex.from` body function may take the `{ signal }` argument, which is aborted with the error the Duplex is destroyed with.
`stream.compose` accepts streams and async generator functions in any position, an async function as the last stage, and an array, a string, or a generator as the first stage; one stage is `Duplex.from` of it.
A web `ReadableStream` is read as an async iterable by `Readable.from` and as the first stage of `compose` or `pipeline`, and through its reader by `Readable.fromWeb` (with `objectMode`, `highWaterMark`, `encoding`, and `signal` in an options object literal) and `Duplex.from`.
A web stream in a later compose stage, `Readable.toWeb`, and web `WritableStream` and `TransformStream` interop are unsupported.
The `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream.
`readable.compose(stage, { signal })` is `stream.compose(readable, stage)` with the signal added; the options are an object literal whose `signal` is a variable or a property read.
`pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags.
Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream.
The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on Writable and Duplex; `writev` on a Transform or PassThrough, assigning `_construct` after construction, and a non-literal `decodeStrings` or `defaultEncoding` in an options object literal are unsupported.
`setDefaultEncoding()` takes any encoding Node knows.
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,8 @@ 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 (`signal` and others) are refused here too, as are `_writev` on a Transform, `_final` on a Transform, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, and pipes into a destination that overrides `write()` or `end()`.
`_construct` and `_writev` are read when `Readable.call(this, options)` or `Writable.call(this, options)` runs, as Node's constructors read them.
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,2 @@
- **Iterable values:** A value typed `Iterable<T>` can hold an array, a Set, a Map (for `Iterable<[K, V]>`), a string (for `Iterable<string>`), a generator, or a Map, Set or array iterator.
Class instances with their own `[Symbol.iterator]` method and typed arrays can't be stored in an `Iterable<T>` and are reported at the conversion.
@@ -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,6 @@
- **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, and generic members are unsupported.
Static methods read the enclosing function's bindings and are inherited by subclasses.
A named class expression's methods and static members can refer to the class by its name.
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.
@@ -3,6 +3,15 @@ Copied values, other native handles, and symbol keys are unsupported.
Constructor seeds support arrays; custom iterators and detached methods are unsupported.
Weak collections do not retain their keys, but values that refer back to a key can keep it alive until deletion or collection release.
`FinalizationRegistry` keeps its registrations: `register` and `unregister` check their arguments and answer as in Node.js.
Its cleanup callbacks never run, which the ECMAScript specification allows.
Program output matches Node.js, which runs cleanup callbacks only when its garbage collector decides to.
Memory that a package frees only from a cleanup callback is not reclaimed until the program exits.
For example, a wasm-bindgen package frees the WebAssembly memory behind a wrapper object from its cleanup callback, so a long-running server that drops such wrappers without calling their `free()` method keeps that memory.
Call `free()` on wrappers the program no longer needs.
Symbol targets and unregister tokens are unsupported.
`WeakRef` is unsupported.
Memory uses reference counting.
Acyclic values are freed deterministically, and cycles are collected at defined collection points.
Native checked objects, arrays, Sets, and captured closures participate in cycle collection.
+168 -168
View File
@@ -11085,9 +11085,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 call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -11108,9 +11108,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 call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -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"
}
},
{
@@ -11614,9 +11614,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 call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -11637,9 +11637,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 call shapes accepted by the compiler lowering.",
"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.";
+21 -7
View File
@@ -42,8 +42,12 @@
"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.checkPrime": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"],
"node-builtin.crypto.checkPrimeSync": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"],
"node-builtin.crypto.createCipheriv": ["tests/corpus/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "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/7150-cipher-legacy-modes.ts", "tests/corpus/7151-cipher-legacy-errors.cjs", "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 +122,9 @@
"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.generatePrime": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"],
"node-builtin.crypto.generatePrimeSync": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.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 +194,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 +376,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 +422,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 -1
View File
@@ -20,6 +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 names",
"test:sandbox": "node scripts/sandbox-test.mjs",
"test:sandbox:image": "node scripts/sandbox-image.mjs",
"test:watch": "vitest",
@@ -39,10 +40,11 @@
"@scriptc/types": "workspace:*",
"@types/node": "^24.0.0",
"effect": "3.22.2",
"js-yaml": "4.3.0",
"js-yaml": "4.3.2",
"oxfmt": "0.71.0",
"oxlint": "1.86.0",
"rollup": "4.62.2",
"smol-toml": "1.7.1",
"three": "0.186.1",
"tsx": "^4.19.0",
"typescript": "5.9.3",
+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",
+5
View File
@@ -160,6 +160,10 @@ async function tryFastPath(): Promise<number | null> {
if (hit === null) return null;
if (!values["keep-llvm"]) await rm(hit.llvmPath, { force: true });
if (command === "build") {
// A cached build repeats its performance warning (SC6101).
const performance = hit.performance ?? [];
if (performance.length > 0 && values["dynamic-tier-warning"] !== false)
process.stderr.write(startup.renderPerformanceWarnings(performance) + "\n");
process.stdout.write(`${outPath}\n`);
return 0;
}
@@ -217,6 +221,7 @@ async function coverageFastPath(
const hit = await startup.readCoverageVerdict(resolve(inputArg), {}).catch(() => null);
if (hit === null) return null;
const { coverage, sourceTexts } = hit;
if (values["dynamic-tier-warning"] === false) delete coverage.performance;
if (print === "diagnostics") {
const envelope = startup.coverageEnvelope(coverage, {
compilerVersion: packageVersion(),
@@ -0,0 +1,120 @@
import { execFile } from "node:child_process";
import { createRequire } from "node:module";
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { expect, test } from "vitest";
const execFileAsync = promisify(execFile);
const require = createRequire(import.meta.url);
const repoRoot = join(import.meta.dirname, "../../..");
const cliEntry = join(repoRoot, "packages/cli/src/main.ts");
const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs");
const fixtures = join(repoRoot, "tests/coverage-fixtures/dynamic-tier");
const HEADER =
"warning SC6101: 3 functions that likely run often use untyped values, which runs slower than typed code:";
const CLOSING =
"The program compiled. Adding types to these functions lets scriptc compile them to faster code.";
async function cli(
args: string[],
cacheRoot: string,
): Promise<{ code: number; stdout: string; stderr: string }> {
try {
const { stdout, stderr } = await execFileAsync(
process.execPath,
["--import", tsxLoader, cliEntry, ...args],
{
env: { ...process.env, SCRIPTC_CACHE_DIR: cacheRoot, SCRIPTC_TIMING: "1" },
maxBuffer: 4 * 1024 * 1024,
},
);
return { code: 0, stdout, stderr };
} catch (err) {
const e = err as { code?: number; stdout?: string; stderr?: string };
return { code: e.code ?? 1, stdout: e.stdout ?? "", stderr: e.stderr ?? "" };
}
}
test("a build names its likely-hot untyped functions, also when it comes from the cache", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-dynamic-tier-warning-"));
const cacheRoot = join(dir, "cache");
const outPath = join(dir, process.platform === "win32" ? "program.exe" : "program");
try {
await mkdir(cacheRoot, { mode: 0o700 });
const build = (extra: string[] = []) =>
cli(["build", join(fixtures, "hot.js"), "-o", outPath, ...extra], cacheRoot);
const first = await build();
expect(first.code).toBe(0);
expect(first.stderr).toContain("scriptc lowering");
expect(first.stderr).toContain(
[
HEADER,
" centroid (hot.js:7): untyped JavaScript file, in a loop",
" spread (hot.js:23): untyped JavaScript file, in a loop",
" distance (hot.js:17): untyped JavaScript file, called in a loop",
" hint: add types to compile these statically; see scriptc coverage --explain-dynamic",
"",
CLOSING,
].join("\n"),
);
// The cache hit skips lowering and repeats the warning.
const cached = await build();
expect(cached.code).toBe(0);
expect(cached.stderr).not.toContain("scriptc lowering");
expect(cached.stderr).toContain(HEADER);
expect(cached.stderr).toContain(CLOSING);
const silenced = await build(["--no-dynamic-tier-warning"]);
expect(silenced.code).toBe(0);
expect(silenced.stderr).not.toContain("SC6101");
} finally {
await rm(dir, { recursive: true, force: true });
}
}, 300_000);
test("untyped setup code builds without the warning", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-dynamic-tier-quiet-"));
try {
await mkdir(join(dir, "cache"), { mode: 0o700 });
const result = await cli(
["build", join(fixtures, "quiet.ts"), "-o", join(dir, "program")],
join(dir, "cache"),
);
expect(result.code).toBe(0);
expect(result.stderr).not.toContain("SC6101");
} finally {
await rm(dir, { recursive: true, force: true });
}
}, 300_000);
test("coverage --fail-on=dynamic fails on the warning, and --no-dynamic-tier-warning cannot hide it", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-dynamic-tier-fail-on-"));
try {
await mkdir(join(dir, "cache"), { mode: 0o700 });
const coverage = (file: string, extra: string[]) =>
cli(["coverage", join(fixtures, file), ...extra], join(dir, "cache"));
const hot = await coverage("hot.js", ["--fail-on=dynamic"]);
expect(hot.code).toBe(1);
expect(hot.stdout).toContain("untyped and likely hot");
expect((await coverage("hot.js", ["--fail-on=divergences"])).code).toBe(0);
expect((await coverage("quiet.ts", ["--fail-on=dynamic"])).code).toBe(0);
const silenced = await coverage("hot.js", ["--no-dynamic-tier-warning"]);
expect(silenced.code).toBe(0);
expect(silenced.stdout).not.toContain("untyped and likely hot");
const conflict = await coverage("hot.js", ["--fail-on=dynamic", "--no-dynamic-tier-warning"]);
expect(conflict.code).toBe(1);
expect(conflict.stderr).toContain(
"--fail-on=dynamic checks the warning --no-dynamic-tier-warning leaves out",
);
} finally {
await rm(dir, { recursive: true, force: true });
}
}, 300_000);
+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: 33, marker: "scr_runtime_abi_v33" },
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_v33");
});
});
File diff suppressed because it is too large Load Diff
Binary file not shown.
+75
View File
@@ -56,3 +56,78 @@ void scr_collect_cycles_phase(void);
void scriptc_native_collect_cycles(void) {
scr_collect_cycles_phase();
}
/* Forked lowering workers (frontend/process-fork.ts). A worker shares the
* parent's heap copy-on-write, writes its results to a file and leaves with
* _exit, so no buffered output or exit handler of the parent runs twice.
* Checker requests from workers hold an exclusive flock on a file each
* worker opens after the fork, so every worker has its own open file
* description and the lock excludes the other workers. */
#ifdef _WIN32
int32_t scriptc_native_fork(void) { return -1; }
void scriptc_native_exit(int32_t code) { ExitProcess((UINT)code); }
int32_t scriptc_native_wait(int32_t pid) { (void)pid; return -1; }
uint8_t scriptc_native_lock_open(const uint8_t *bytes, size_t size) { (void)bytes; (void)size; return 0; }
void scriptc_native_lock(void) {}
void scriptc_native_unlock(void) {}
#else
#include <errno.h>
#include <fcntl.h>
#include <sys/file.h>
#include <sys/wait.h>
int32_t scriptc_native_fork(void) { return (int32_t)fork(); }
void scriptc_native_exit(int32_t code) { _exit(code); }
/* The worker's exit status, 128 plus the signal for a worker a signal
* ended, or -1 when waitpid fails. */
int32_t scriptc_native_wait(int32_t pid) {
int status = 0;
while (waitpid((pid_t)pid, &status, 0) < 0) {
if (errno != EINTR) return -1;
}
if (WIFEXITED(status)) return WEXITSTATUS(status);
if (WIFSIGNALED(status)) return 128 + WTERMSIG(status);
return -1;
}
static int scriptc_native_lock_fd = -1;
uint8_t scriptc_native_lock_open(const uint8_t *bytes, size_t size) {
if (size == 0 || memchr(bytes, 0, size) != NULL) return 0;
char *path = malloc(size + 1);
if (!path) return 0;
memcpy(path, bytes, size);
path[size] = 0;
if (scriptc_native_lock_fd >= 0) close(scriptc_native_lock_fd);
scriptc_native_lock_fd = open(path, O_CREAT | O_RDWR | O_CLOEXEC, 0600);
free(path);
return scriptc_native_lock_fd >= 0;
}
void scriptc_native_lock(void) {
if (scriptc_native_lock_fd < 0) return;
while (flock(scriptc_native_lock_fd, LOCK_EX) < 0 && errno == EINTR) {
}
}
void scriptc_native_unlock(void) {
if (scriptc_native_lock_fd >= 0) flock(scriptc_native_lock_fd, LOCK_UN);
}
#endif
/* The runtime's growth cap for full cycle passes (scr_rc.h). */
void scr_cyc_set_growth_cap(long cap);
/* Bounds the cyclic garbage a build can hold to twice its live heap.
* Lowering a large program leaves hundreds of megabytes of cyclic garbage,
* and whether a scheduled full pass reclaims it before the build's peak
* depends on where the growth rule fires: with the default cap of 16, small
* changes to the compiler moved the peak of one build by 390 MB. With a cap
* of 2 a full pass runs once the heap has grown to three times its size
* after the last one; a cap of 1 bounds the garbage further but runs more
* passes over the live heap. SCR_CYCLE_GROWTH_CAP still overrides it. */
void scriptc_native_bound_cycle_growth(void) {
scr_cyc_set_growth_cap(2);
}
+8 -1
View File
@@ -2,6 +2,13 @@
"ffi_format": 6,
"functions": [
{ "name": "compilerNativePrivateDirectory", "symbol": "scriptc_native_private_directory", "params": ["string", "bool"], "returns": "bool" },
{ "name": "compilerNativeCollectCycles", "symbol": "scriptc_native_collect_cycles", "params": [], "returns": "void" }
{ "name": "compilerNativeCollectCycles", "symbol": "scriptc_native_collect_cycles", "params": [], "returns": "void" },
{ "name": "compilerNativeFork", "symbol": "scriptc_native_fork", "params": [], "returns": "i32" },
{ "name": "compilerNativeExit", "symbol": "scriptc_native_exit", "params": ["i32"], "returns": "void" },
{ "name": "compilerNativeWait", "symbol": "scriptc_native_wait", "params": ["i32"], "returns": "i32" },
{ "name": "compilerNativeLockOpen", "symbol": "scriptc_native_lock_open", "params": ["string"], "returns": "bool" },
{ "name": "compilerNativeLock", "symbol": "scriptc_native_lock", "params": [], "returns": "void" },
{ "name": "compilerNativeUnlock", "symbol": "scriptc_native_unlock", "params": [], "returns": "void" },
{ "name": "compilerNativeBoundCycleGrowth", "symbol": "scriptc_native_bound_cycle_growth", "params": [], "returns": "void" }
]
}
+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.8",
"typescript": "7.0.2",
"typescript5": "npm:typescript@5.9.3"
},
+688
View File
@@ -0,0 +1,688 @@
// scriptc's built-in implementation of the `chalk` package, 5.x API.
//
// Programs whose installed chalk is 5.x compile this module in its place
// (packages/compiler/src/frontend/package-shims.ts). chalk builds its
// styles as functions whose prototypes it swaps at run time; here a style
// chain is an instance of one class whose style members are cached
// getters, and the instance is callable through its `%call` field (the
// merged interface below gives it chalk's call signature; the lowerer calls
// the field). The output, color-level detection (supports-color 10:
// FORCE_COLOR, the --color/--no-color flags, TTY, TERM, CI variables) and
// the shared, settable `level` follow chalk 5.6 byte for byte.
import * as os from "node:os";
import * as tty from "node:tty";
export type ColorSupportLevel = 0 | 1 | 2 | 3;
export interface ColorSupport {
level: ColorSupportLevel;
hasBasic: boolean;
has256: boolean;
has16m: boolean;
}
export type ColorInfo = ColorSupport | false;
export interface Options {
readonly level?: ColorSupportLevel;
}
export type ModifierName =
| "reset"
| "bold"
| "dim"
| "italic"
| "underline"
| "overline"
| "inverse"
| "hidden"
| "strikethrough";
export type ForegroundColorName =
| "black"
| "red"
| "green"
| "yellow"
| "blue"
| "cyan"
| "magenta"
| "white"
| "gray"
| "grey"
| "blackBright"
| "redBright"
| "greenBright"
| "yellowBright"
| "blueBright"
| "cyanBright"
| "magentaBright"
| "whiteBright";
export type BackgroundColorName =
| "bgBlack"
| "bgRed"
| "bgGreen"
| "bgYellow"
| "bgBlue"
| "bgCyan"
| "bgMagenta"
| "bgWhite"
| "bgGray"
| "bgGrey"
| "bgBlackBright"
| "bgRedBright"
| "bgGreenBright"
| "bgYellowBright"
| "bgBlueBright"
| "bgCyanBright"
| "bgMagentaBright"
| "bgWhiteBright";
export type ColorName = ForegroundColorName | BackgroundColorName;
export type Modifiers = ModifierName;
export type ForegroundColor = ForegroundColorName;
export type BackgroundColor = BackgroundColorName;
export type Color = ColorName;
// ── ansi-styles ──────────────────────────────────────────────────────────
export const modifierNames: ModifierName[] = [
"reset",
"bold",
"dim",
"italic",
"underline",
"overline",
"inverse",
"hidden",
"strikethrough",
];
export const foregroundColorNames: ForegroundColorName[] = [
"black",
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"white",
"blackBright",
"gray",
"grey",
"redBright",
"greenBright",
"yellowBright",
"blueBright",
"magentaBright",
"cyanBright",
"whiteBright",
];
export const backgroundColorNames: BackgroundColorName[] = [
"bgBlack",
"bgRed",
"bgGreen",
"bgYellow",
"bgBlue",
"bgMagenta",
"bgCyan",
"bgWhite",
"bgBlackBright",
"bgGray",
"bgGrey",
"bgRedBright",
"bgGreenBright",
"bgYellowBright",
"bgBlueBright",
"bgMagentaBright",
"bgCyanBright",
"bgWhiteBright",
];
export const colorNames: ColorName[] = [...foregroundColorNames, ...backgroundColorNames];
export const modifiers: Modifiers[] = modifierNames;
export const foregroundColors: ForegroundColor[] = foregroundColorNames;
export const backgroundColors: BackgroundColor[] = backgroundColorNames;
export const colors: Color[] = colorNames;
const ESC = "\u001B[";
/** Open and close codes, indexed by style id (the order of the getters). */
const OPEN_CODES: number[] = [
0, 1, 2, 3, 4, 53, 7, 8, 9, 30, 31, 32, 33, 34, 35, 36, 37, 90, 90, 90, 91, 92, 93, 94, 95, 96,
97, 40, 41, 42, 43, 44, 45, 46, 47, 100, 100, 100, 101, 102, 103, 104, 105, 106, 107,
];
const CLOSE_CODES: number[] = [
0, 22, 22, 23, 24, 55, 27, 28, 29, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39,
39, 39, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49,
];
const VISIBLE = 45;
function rgbToAnsi256(red: number, green: number, blue: number): number {
if (red === green && green === blue) {
if (red < 8) return 16;
if (red > 248) return 231;
return Math.round(((red - 8) / 247) * 24) + 232;
}
return (
16 +
36 * Math.round((red / 255) * 5) +
6 * Math.round((green / 255) * 5) +
Math.round((blue / 255) * 5)
);
}
function hexToRgb(hex: string): number[] {
const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex);
if (matches === null) return [0, 0, 0];
let colorString = matches[0];
if (colorString.length === 3) {
let doubled = "";
for (const character of colorString) doubled += character + character;
colorString = doubled;
}
const integer = Number.parseInt(colorString, 16);
return [(integer >> 16) & 0xff, (integer >> 8) & 0xff, integer & 0xff];
}
function ansi256ToAnsi(code: number): number {
if (code < 8) return 30 + code;
if (code < 16) return 90 + (code - 8);
let red: number;
let green: number;
let blue: number;
if (code >= 232) {
red = ((code - 232) * 10 + 8) / 255;
green = red;
blue = red;
} else {
code -= 16;
const remainder = code % 36;
red = Math.floor(code / 36) / 5;
green = Math.floor(remainder / 6) / 5;
blue = (remainder % 6) / 5;
}
const value = Math.max(red, green, blue) * 2;
if (value === 0) return 30;
let result = 30 + ((Math.round(blue) << 2) | (Math.round(green) << 1) | Math.round(red));
if (value === 2) result += 60;
return result;
}
/** The open code of a color model at a level: offset 0 is the foreground,
* 10 the background (ansi-styles' wrapAnsi16/256/16m). */
function rgbOpen(level: number, offset: number, red: number, green: number, blue: number): string {
if (level === 3) return `${ESC}${38 + offset};2;${red};${green};${blue}m`;
if (level === 2) return `${ESC}${38 + offset};5;${rgbToAnsi256(red, green, blue)}m`;
return `${ESC}${ansi256ToAnsi(rgbToAnsi256(red, green, blue)) + offset}m`;
}
/** chalk's ansi256 model emits the 256-color code at every level (only
* the rgb and hex models downsample). */
function ansi256Open(offset: number, code: number): string {
return `${ESC}${38 + offset};5;${code}m`;
}
// ── supports-color ───────────────────────────────────────────────────────
function hasFlag(flag: string): boolean {
const argv = process.argv;
const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
const position = argv.indexOf(prefix + flag);
const terminatorPosition = argv.indexOf("--");
return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
}
let flagForceColor: number | undefined = undefined;
if (
hasFlag("no-color") ||
hasFlag("no-colors") ||
hasFlag("color=false") ||
hasFlag("color=never")
) {
flagForceColor = 0;
} else if (
hasFlag("color") ||
hasFlag("colors") ||
hasFlag("color=true") ||
hasFlag("color=always")
) {
flagForceColor = 1;
}
function envForceColor(): number | undefined {
const force = process.env["FORCE_COLOR"];
if (force !== undefined) {
if (force === "true") return 1;
if (force === "false") return 0;
return force.length === 0 ? 1 : Math.min(Number.parseInt(force, 10), 3);
}
return undefined;
}
function translateLevel(level: number): ColorInfo {
if (level === 0) return false;
return {
level: level as ColorSupportLevel,
hasBasic: true,
has256: level >= 2,
has16m: level >= 3,
};
}
function inEnv(name: string): boolean {
return process.env[name] !== undefined;
}
function detectLevel(streamIsTTY: boolean): number {
const noFlagForceColor = envForceColor();
if (noFlagForceColor !== undefined) flagForceColor = noFlagForceColor;
const forceColor = flagForceColor;
if (forceColor === 0) return 0;
if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) return 3;
if (hasFlag("color=256")) return 2;
if (inEnv("TF_BUILD") && inEnv("AGENT_NAME")) return 1;
if (!streamIsTTY && forceColor === undefined) return 0;
const min =
forceColor === undefined || forceColor === 0 || Number.isNaN(forceColor) ? 0 : forceColor;
const env = process.env;
const term = env["TERM"];
if (term === "dumb") return min;
if (process.platform === "win32") {
const osRelease = os.release().split(".");
if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10_586) {
return Number(osRelease[2]) >= 14_931 ? 3 : 2;
}
return 1;
}
if (inEnv("CI")) {
if (inEnv("GITHUB_ACTIONS") || inEnv("GITEA_ACTIONS") || inEnv("CIRCLECI")) return 3;
if (
inEnv("TRAVIS") ||
inEnv("APPVEYOR") ||
inEnv("GITLAB_CI") ||
inEnv("BUILDKITE") ||
inEnv("DRONE") ||
env["CI_NAME"] === "codeship"
) {
return 1;
}
return min;
}
const teamcity = env["TEAMCITY_VERSION"];
if (teamcity !== undefined) {
return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(teamcity) ? 1 : 0;
}
if (env["COLORTERM"] === "truecolor") return 3;
if (term === "xterm-kitty") return 3;
if (term === "xterm-ghostty") return 3;
if (term === "wezterm") return 3;
const termProgram = env["TERM_PROGRAM"];
if (termProgram !== undefined) {
const version = Number.parseInt((env["TERM_PROGRAM_VERSION"] || "").split(".")[0]!, 10);
if (termProgram === "iTerm.app") return version >= 3 ? 3 : 2;
if (termProgram === "Apple_Terminal") return 2;
}
// supports-color tests these against `env.TERM`, which is the string
// "undefined" when TERM is unset.
const termText = term === undefined ? "undefined" : term;
if (/-256(color)?$/i.test(termText)) return 2;
if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(termText)) return 1;
if (inEnv("COLORTERM")) return 1;
return min;
}
const stdoutColor: ColorInfo = translateLevel(detectLevel(tty.isatty(1)));
const stderrColor: ColorInfo = translateLevel(detectLevel(tty.isatty(2)));
export const supportsColor: ColorInfo = stdoutColor;
export const supportsColorStderr: ColorInfo = stderrColor;
// ── the builder ──────────────────────────────────────────────────────────
/** One link of a style chain (chalk's styler). */
class Styler {
open: string;
close: string;
openAll: string;
closeAll: string;
parent: Styler | undefined;
constructor(open: string, close: string, parent: Styler | undefined) {
this.open = open;
this.close = close;
this.parent = parent;
if (parent === undefined) {
this.openAll = open;
this.closeAll = close;
} else {
this.openAll = parent.openAll + open;
this.closeAll = close + parent.closeAll;
}
}
}
/** The level every builder of one chalk instance shares. */
class LevelBox {
level: number;
constructor(level: number) {
this.level = level;
}
}
function stringReplaceAll(string: string, substring: string, replacer: string): string {
let index = string.indexOf(substring);
if (index === -1) return string;
const substringLength = substring.length;
let endIndex = 0;
let returnValue = "";
do {
returnValue += string.slice(endIndex, index) + substring + replacer;
endIndex = index + substringLength;
index = string.indexOf(substring, endIndex);
} while (index !== -1);
returnValue += string.slice(endIndex);
return returnValue;
}
function stringEncaseCRLFWithFirstIndex(
string: string,
prefix: string,
postfix: string,
index: number,
): string {
let endIndex = 0;
let returnValue = "";
do {
const gotCR = string[index - 1] === "\r";
returnValue +=
string.slice(endIndex, gotCR ? index - 1 : index) +
prefix +
(gotCR ? "\r\n" : "\n") +
postfix;
endIndex = index + 1;
index = string.indexOf("\n", endIndex);
} while (index !== -1);
returnValue += string.slice(endIndex);
return returnValue;
}
function applyStyle(self: ChalkInstance, string: string): string {
if (self.box.level <= 0 || !string) return self.isEmpty ? "" : string;
let styler = self.styler;
if (styler === undefined) return string;
const openAll = styler.openAll;
const closeAll = styler.closeAll;
if (string.includes("\u001B")) {
while (styler !== undefined) {
string = stringReplaceAll(string, styler.close, styler.open);
styler = styler.parent;
}
}
const lfIndex = string.indexOf("\n");
if (lfIndex !== -1) string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
return openAll + string + closeAll;
}
function joinArguments(args: unknown[]): string {
let out = "";
for (let i = 0; i < args.length; i++) {
if (i > 0) out += " ";
const value = args[i];
if (value !== undefined && value !== null) out += String(value);
}
return out;
}
/** A chalk instance or one of its style chains: callable (chalk's call
* signature merges in from the interface), with every style a cached
* getter. */
// The merge is the point: the interface gives instances chalk's call
// signature, and the class's `%call` field implements it.
// oxlint-disable-next-line typescript/no-unsafe-declaration-merging
export interface ChalkInstance {
(...text: unknown[]): string;
}
// oxlint-disable-next-line typescript/no-unsafe-declaration-merging
export class ChalkInstance {
"%call": (...text: unknown[]) => string = (...text: unknown[]): string =>
this.root
? joinArguments(text)
: applyStyle(this, text.length === 1 ? "" + String(text[0]) : joinArguments(text));
box: LevelBox;
styler: Styler | undefined;
isEmpty: boolean;
private readonly root: boolean;
private readonly cache: (ChalkInstance | undefined)[] = [];
constructor(box: LevelBox, styler: Styler | undefined, isEmpty: boolean, root: boolean) {
this.box = box;
this.styler = styler;
this.isEmpty = isEmpty;
this.root = root;
}
/** One string argument: the compiler calls this instead of `%call`
* (no variadic arguments to box). */
"%call1"(text: string): string {
return this.root ? text : applyStyle(this, text);
}
get level(): ColorSupportLevel {
return this.box.level as ColorSupportLevel;
}
set level(level: ColorSupportLevel) {
this.box.level = level;
}
private style(id: number): ChalkInstance {
const cached = this.cache[id];
if (cached !== undefined) return cached;
let builder: ChalkInstance;
if (id === VISIBLE) {
builder = new ChalkInstance(this.box, this.styler, true, false);
} else {
const styler = new Styler(
`${ESC}${OPEN_CODES[id]!}m`,
`${ESC}${CLOSE_CODES[id]!}m`,
this.styler,
);
builder = new ChalkInstance(this.box, styler, this.isEmpty, false);
}
while (this.cache.length <= id) this.cache.push(undefined);
this.cache[id] = builder;
return builder;
}
private model(open: string, background: boolean): ChalkInstance {
const styler = new Styler(open, background ? `${ESC}49m` : `${ESC}39m`, this.styler);
return new ChalkInstance(this.box, styler, this.isEmpty, false);
}
rgb(red: number, green: number, blue: number): ChalkInstance {
return this.model(rgbOpen(this.box.level, 0, red, green, blue), false);
}
hex(color: string): ChalkInstance {
const [red, green, blue] = hexToRgb(color);
return this.model(rgbOpen(this.box.level, 0, red!, green!, blue!), false);
}
ansi256(index: number): ChalkInstance {
return this.model(ansi256Open(0, index), false);
}
bgRgb(red: number, green: number, blue: number): ChalkInstance {
return this.model(rgbOpen(this.box.level, 10, red, green, blue), true);
}
bgHex(color: string): ChalkInstance {
const [red, green, blue] = hexToRgb(color);
return this.model(rgbOpen(this.box.level, 10, red!, green!, blue!), true);
}
bgAnsi256(index: number): ChalkInstance {
return this.model(ansi256Open(10, index), true);
}
get reset(): ChalkInstance {
return this.style(0);
}
get bold(): ChalkInstance {
return this.style(1);
}
get dim(): ChalkInstance {
return this.style(2);
}
get italic(): ChalkInstance {
return this.style(3);
}
get underline(): ChalkInstance {
return this.style(4);
}
get overline(): ChalkInstance {
return this.style(5);
}
get inverse(): ChalkInstance {
return this.style(6);
}
get hidden(): ChalkInstance {
return this.style(7);
}
get strikethrough(): ChalkInstance {
return this.style(8);
}
get visible(): ChalkInstance {
return this.style(VISIBLE);
}
get black(): ChalkInstance {
return this.style(9);
}
get red(): ChalkInstance {
return this.style(10);
}
get green(): ChalkInstance {
return this.style(11);
}
get yellow(): ChalkInstance {
return this.style(12);
}
get blue(): ChalkInstance {
return this.style(13);
}
get magenta(): ChalkInstance {
return this.style(14);
}
get cyan(): ChalkInstance {
return this.style(15);
}
get white(): ChalkInstance {
return this.style(16);
}
get blackBright(): ChalkInstance {
return this.style(17);
}
get gray(): ChalkInstance {
return this.style(18);
}
get grey(): ChalkInstance {
return this.style(19);
}
get redBright(): ChalkInstance {
return this.style(20);
}
get greenBright(): ChalkInstance {
return this.style(21);
}
get yellowBright(): ChalkInstance {
return this.style(22);
}
get blueBright(): ChalkInstance {
return this.style(23);
}
get magentaBright(): ChalkInstance {
return this.style(24);
}
get cyanBright(): ChalkInstance {
return this.style(25);
}
get whiteBright(): ChalkInstance {
return this.style(26);
}
get bgBlack(): ChalkInstance {
return this.style(27);
}
get bgRed(): ChalkInstance {
return this.style(28);
}
get bgGreen(): ChalkInstance {
return this.style(29);
}
get bgYellow(): ChalkInstance {
return this.style(30);
}
get bgBlue(): ChalkInstance {
return this.style(31);
}
get bgMagenta(): ChalkInstance {
return this.style(32);
}
get bgCyan(): ChalkInstance {
return this.style(33);
}
get bgWhite(): ChalkInstance {
return this.style(34);
}
get bgBlackBright(): ChalkInstance {
return this.style(35);
}
get bgGray(): ChalkInstance {
return this.style(36);
}
get bgGrey(): ChalkInstance {
return this.style(37);
}
get bgRedBright(): ChalkInstance {
return this.style(38);
}
get bgGreenBright(): ChalkInstance {
return this.style(39);
}
get bgYellowBright(): ChalkInstance {
return this.style(40);
}
get bgBlueBright(): ChalkInstance {
return this.style(41);
}
get bgMagentaBright(): ChalkInstance {
return this.style(42);
}
get bgCyanBright(): ChalkInstance {
return this.style(43);
}
get bgWhiteBright(): ChalkInstance {
return this.style(44);
}
}
function optionsLevel(options: Options | undefined): number {
const level = options === undefined ? undefined : options.level;
if (
level !== undefined &&
level !== 0 &&
!(Number.isInteger(level) && level >= 0 && level <= 3)
) {
throw new Error("The `level` option should be an integer from 0 to 3");
}
if (level === undefined) return stdoutColor === false ? 0 : stdoutColor.level;
return level;
}
/** `new Chalk(options)`: a fresh chalk instance with its own level. */
export class Chalk extends ChalkInstance {
constructor(options?: Options) {
super(new LevelBox(optionsLevel(options)), undefined, false, true);
}
}
const chalk: ChalkInstance = new Chalk();
export const chalkStderr: ChalkInstance = new Chalk({
level: stderrColor === false ? 0 : stderrColor.level,
});
export default chalk;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
// The hot-shots 8.x shim for installations with the optional unix-dgram
// addon: the uds protocol sends through a native Unix datagram socket.
import { configureUnixDgram } from "./client.ts";
configureUnixDgram(true);
export * from "./client.ts";
export { default } from "./client.ts";
+8
View File
@@ -0,0 +1,8 @@
// The hot-shots 8.x shim for installations without the optional unix-dgram
// addon: the uds protocol reports it missing, as hot-shots does under Node.
import { configureUnixDgram } from "./client.ts";
configureUnixDgram(false);
export * from "./client.ts";
export { default } from "./client.ts";
@@ -370,7 +370,7 @@ export class Client extends DispatcherBase {
localAddress: this.__localAddress,
};
if (channels.beforeConnect.hasSubscribers) {
channels.beforeConnect.publish({ connectParams });
channels.beforeConnect.publish({ connectParams, connector: this.__connector });
}
let settled = false;
const onConnected = (err: Error | null, socket: Socket | null): void => {
@@ -428,7 +428,7 @@ export class Client extends DispatcherBase {
if (this.__socket === socket) this.__onSocketClose(socket);
});
if (channels.connected.hasSubscribers) {
channels.connected.publish({ connectParams });
channels.connected.publish({ connectParams, connector: this.__connector, socket });
}
this.emit("connect", this.__url, [this as Dispatcher]);
this.__resume(false);
@@ -447,7 +447,7 @@ export class Client extends DispatcherBase {
): void {
this.__connecting = false;
if (channels.connectError.hasSubscribers) {
channels.connectError.publish({ connectParams, error: err });
channels.connectError.publish({ connectParams, connector: this.__connector, error: err });
}
this.__onError(err);
this.emit("connectionError", this.__url, [this as Dispatcher], err);
@@ -658,7 +658,7 @@ export class Client extends DispatcherBase {
}
header += request.headers;
if (channels.sendHeaders.hasSubscribers) {
channels.sendHeaders.publish({ request: request.diagnosticView(), headers: header });
channels.sendHeaders.publish({ request: request.diagnosticView(), headers: header, socket });
}
if (body === null) {
if (contentLength === 0) socket.write(`${header}content-length: 0\r\n\r\n`);
@@ -17,6 +17,7 @@ import {
} from "../ir/ir.js";
import { computeTraced, explainCycle } from "./cycle-analysis.js";
import { computeTraced as llvmTraced } from "./llvm/shapes.js";
import { addRecordResultEntries } from "./llvm/record-results.js";
const loc = { file: "cycles.ts", start: 0, end: 1 };
function module(): IrModule {
@@ -404,6 +405,61 @@ test("a later write through any alias makes the edge mutable", () => {
checkExact(record, ["record:list"]);
});
test("a result-slot entry's stores into its destination are not writes", () => {
const next: IrType = { kind: "union", unionId: "next" };
const listModule = (): IrModule => {
const mod = module();
mod.records = [shape("list", [next, F64])];
mod.unions = [{ id: "next", arms: [ref("list"), { kind: "nullT" }] }];
mod.functions.push({
name: "cell",
params: [{ localId: "rest", name: "rest", type: next }],
locals: [],
returnType: ref("list"),
body: [
{
kind: "return",
value: {
kind: "recordLit",
fields: [
{ name: "field0", value: { kind: "varRef", localId: "rest", type: next, loc } },
{ name: "field1", value: { kind: "numLit", value: 1, type: F64, loc } },
],
type: ref("list"),
loc,
},
loc,
},
],
loc,
});
return addRecordResultEntries(mod);
};
// The entry stores the literal's fields into caller frame storage that
// nothing can point to: the list stays acyclic.
const entries = listModule();
expect(entries.functions.map((fn) => fn.name)).toContain("%into.cell");
checkExact(entries, []);
// The same store into the destination's local id outside an entry, and a
// store into any other binding of the shape inside one, are writes.
const store = (localId: string): IrStmt => ({
kind: "recordSet",
obj: { kind: "varRef", localId, type: ref("list"), loc },
shapeId: "list",
field: "field0",
value: value(next),
loc,
});
for (const [fn, localId] of [
["main", "%into.dst"],
["%into.cell", "v"],
] as const) {
const written = listModule();
written.functions.find((f) => f.name === fn)!.body.unshift(store(localId));
checkExact(written, ["record:list"], ["next"]);
}
});
test("field increments count as writes", () => {
const mod = tree();
mod.functions[0]!.body.push({
@@ -11,7 +11,7 @@ import {
STRING,
VOID,
} from "../ir/ir.js";
import { computeCycleMutability, fieldKey } from "./cycle-mutability.js";
import { type CycleMutability, computeCycleMutability, fieldKey } from "./cycle-mutability.js";
/** Cycle capability for native code generation.
* Nodes are shape units and unions; edges are the references their fields
@@ -38,8 +38,10 @@ import { computeCycleMutability, fieldKey } from "./cycle-mutability.js";
export function computeTraced(
mod: IrModule,
stores?: DynamicFieldStores,
/** computeCycleMutability's answer for this module, when the caller has it. */
precomputed?: CycleMutability,
): { shapes: Set<string>; unions: Set<string> } {
const { shapes, unions } = analyzeCycles(mod, false, stores);
const { shapes, unions } = analyzeCycles(mod, false, stores, precomputed);
return { shapes, unions };
}
@@ -93,6 +95,7 @@ function analyzeCycles(
mod: IrModule,
labels: boolean,
stores?: DynamicFieldStores,
precomputed?: CycleMutability,
): {
shapes: Set<string>;
unions: Set<string>;
@@ -125,7 +128,7 @@ function analyzeCycles(
const lifetimeRoots = new Set(mod.contracts?.programLifetime ?? []);
const programLifetime = (className: string): boolean =>
lifetimeRoots.size > 0 && lifetimeRoots.has(rootOf(className));
const mutability = computeCycleMutability(mod, unitOfClass, stores);
const mutability = precomputed ?? computeCycleMutability(mod, unitOfClass, stores);
const shapeDefs = [
...classes.map((c) => ({
key: `object:${c.name}`,
@@ -2,6 +2,7 @@ import type { IrExpr, IrFunction, IrModule, IrStmt, IrType } from "../ir/ir.js";
import { RUNTIME_EMITTER_CLASS, RUNTIME_STREAM_CLASSES, typeKey } from "../ir/ir.js";
import { dynamicFieldStores, type DynamicFieldStores } from "../ir/int32-slots.js";
import { everyExprChild, everyStmtChild, everyTypeChild } from "../ir/traverse.js";
import { resultSlotStore } from "./result-slots.js";
/** Which shape fields can be written after their object escapes.
*
@@ -17,6 +18,10 @@ import { everyExprChild, everyStmtChild, everyTypeChild } from "../ir/traverse.j
* - every `fieldSet` outside a constructor's own `this`, `fieldIncDec`, and
* `recordSet` makes its (unit, field) mutable; `recordKeySet` on declared
* fields and `recordKeyDelete` make the whole record mutable;
* - a result-slot entry's stores into its destination (result-slots.ts) are
* not writes: the destination is frame storage of a caller that only
* projects the value, so no object can point to it and its fields cannot
* lie on a cycle;
* - a constructor's stores into its own `this` are construction-only up to
* the first top-level statement in which `this` escapes (any use other
* than a field store target, a field read, or the receiver of the direct
@@ -69,6 +74,22 @@ export function fieldKey(unit: string, field: string): string {
return `${unit}\0${field}`;
}
/** The cycle unit of a class: `object:<root>`, the root of its hierarchy,
* as computeImmutableFields and computeTraced group classes. */
export function hierarchyUnitOfClass(mod: IrModule): (className: string) => string {
const baseOf = new Map((mod.classes ?? []).map((c) => [c.name, c.base ?? null] as const));
const roots = new Map<string, string>();
return (name) => {
const known = roots.get(name);
if (known !== undefined) return `object:${known}`;
let cur = name;
for (let base = baseOf.get(cur); base !== null && base !== undefined; base = baseOf.get(cur))
cur = base;
roots.set(name, cur);
return `object:${cur}`;
};
}
export function computeCycleMutability(
mod: IrModule,
unitOfClass: (className: string) => string,
@@ -296,7 +317,8 @@ export function computeCycleMutability(
result.fields.add(fieldKey(unitOfClass(s.className), s.field));
break;
case "recordSet":
result.fields.add(fieldKey(`record:${s.shapeId}`, s.field));
if (!resultSlotStore(fn.name, s))
result.fields.add(fieldKey(`record:${s.shapeId}`, s.field));
break;
case "recordKeySet":
if (s.overflowOnly !== true) result.units.add(`record:${s.shapeId}`);
@@ -56,6 +56,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,
};
@@ -48,6 +48,8 @@ export class ImmutableFields {
export function computeImmutableFields(
mod: IrModule,
stores?: DynamicFieldStores,
/** computeCycleMutability's answer for this module, when the caller has it. */
precomputed?: CycleMutability,
): ImmutableFields | null {
if (mod.lib !== undefined) return null;
const baseOf = new Map((mod.classes ?? []).map((c) => [c.name, c.base ?? null] as const));
@@ -62,7 +64,7 @@ export function computeImmutableFields(
return cur;
};
const unitOfClass = (name: string): string => `object:${rootOf(name)}`;
const mutability = computeCycleMutability(mod, unitOfClass, stores);
const mutability = precomputed ?? computeCycleMutability(mod, unitOfClass, stores);
return mutability.all ? null : new ImmutableFields(mutability, unitOfClass);
}
@@ -213,6 +215,9 @@ export function stableGlobals(mod: IrModule): Set<string> {
}
return everyStmtChild(s, expr, stmt);
};
for (const fn of mod.functions) fn.body.every(stmt);
for (const fn of mod.functions) {
if (candidates.size === 0) break;
fn.body.every(stmt);
}
return candidates;
}
@@ -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,
+7 -2
View File
@@ -244,9 +244,12 @@ export function cycleAppendHelper(): string[] {
}
/** A collector header to install: the object type's constant ScrCycType
* descriptor (its trace and teardown; see cycTypeDescriptor). */
* descriptor (its trace and teardown; see cycTypeDescriptor). `acyclic`:
* the descriptor's trace is scr_cyc_no_children, and `buf` starts out
* SCR_CYC_ACYCLIC (scr_cyc_alloc does the same on its path). */
export interface CycHeader {
type: string;
acyclic?: boolean;
}
/** The first collector statistics index of an emitted ScrCycType
@@ -391,7 +394,9 @@ function allocLines(
` %sa.cgp = getelementptr inbounds i8, ptr %sa.blk, i64 2`,
` store i16 0, ptr %sa.cgp ; ScrCycHdr.color, gen`,
` %sa.bufp = getelementptr inbounds i8, ptr %sa.blk, i64 ${CYC_BUF}`,
` store i32 0, ptr %sa.bufp ; ScrCycHdr.buf`,
cyc.acyclic === true
? ` store i32 -1, ptr %sa.bufp ; ScrCycHdr.buf = SCR_CYC_ACYCLIC`
: ` store i32 0, ptr %sa.bufp ; ScrCycHdr.buf`,
` %sa.live = load i64, ptr @scr_cyc_live`,
` %sa.live1 = add i64 %sa.live, 1`,
` store i64 %sa.live1, ptr @scr_cyc_live`,
@@ -57,10 +57,11 @@ test("only empty literals stored where the module appends construct small", () =
expect(found.has(idle)).toBe(false);
expect(found.has(returned)).toBe(false);
// Only `fill` builds arrays.
// Only `fill` builds arrays. They may hold int32 slots (element kind 7),
// which does not change the choice of constructor.
const llvm = emitLlvmModule(module(body, ["xs", "ys"]));
expect(llvm.match(/call ptr @scr_arr_new_small\(i32 0\)/g)).toHaveLength(2);
expect(llvm.match(/call ptr @scr_arr_new\(i32 0, i64 0\)/g)).toHaveLength(2);
expect(llvm.match(/call ptr @scr_arr_new_small\(i32 [07]\)/g)).toHaveLength(2);
expect(llvm.match(/call ptr @scr_arr_new\(i32 [07], i64 0\)/g)).toHaveLength(2);
});
test("a binding with the same id in another function does not count", () => {
@@ -85,6 +85,7 @@ export function findAppendedArrayLiterals(module: IrModule): ReadonlySet<IrExpr>
for (const field of fieldAliases.get(binding) ?? []) appendedFields.add(field);
const result = new Set<IrExpr>();
if (appendedBindings.size === 0 && appendedFields.size === 0) return result;
const consider = (value: IrExpr | null, appended: boolean): void => {
if (value && appended && isEmptyLiteral(value)) result.add(value);
};
@@ -10,6 +10,8 @@
* SameValueZero are both pointer identity (class instances, records,
* arrays, maps, sets, typed arrays): 64-bit compares, four slots per
* branch.
* - sc_arr_index_of_cref, the same over the 4-byte slots of compact class
* references (SCR_ELEM_CREF), against the needle's 32-bit reference.
* - sc_arr_index_of_str, for strings: pointer identity, then the length,
* then the bytes. Strings of up to 16 bytes compare as two words; a short
* string's word is the eight bytes ending at its last byte, which start
@@ -26,10 +28,12 @@ import type { IrType } from "../../ir/ir.js";
import { SCR_ARR_FIELD } from "./common.js";
import { FN_ATTRS } from "./shapes.js";
export type ArraySearchKind = "ptr" | "str";
export type ArraySearchKind = "ptr" | "cref" | "str";
/** The inline search for arrays of `elem`, or null for the runtime search. */
export function arraySearchKind(elem: IrType): ArraySearchKind | null {
/** The inline search for arrays of `elem`, or null for the runtime search.
* `compact`: the arrays hold compact class references (SCR_ELEM_CREF). */
export function arraySearchKind(elem: IrType, compact: boolean): ArraySearchKind | null {
if (compact) return elem.kind === "object" ? "cref" : null;
switch (elem.kind) {
case "object":
case "record":
@@ -46,7 +50,7 @@ export function arraySearchKind(elem: IrType): ArraySearchKind | null {
}
export function arraySearchHelperName(kind: ArraySearchKind): string {
return kind === "ptr" ? "sc_arr_index_of_ptr" : "sc_arr_index_of_str";
return `sc_arr_index_of_${kind}`;
}
/** The definitions of the helpers in `kinds`; they declare their slow paths
@@ -76,9 +80,18 @@ export function emitArraySearchHelpers(
` %dp = getelementptr inbounds %ScrArr, ptr %a, i32 0, i32 ${SCR_ARR_FIELD.data}`,
` %data = load ptr, ptr %dp`,
);
if (kind === "ptr") {
if (kind === "ptr" || kind === "cref") {
// A compact reference is the address over 8 (classes.ts
// compactRefLoadLines).
const slot = kind === "ptr" ? "i64" : "i32";
defs.push(
` %needle = ptrtoint ptr %v to i64`,
...(kind === "ptr"
? [` %needle = ptrtoint ptr %v to i64`]
: [
` %vaddr = ptrtoint ptr %v to i64`,
` %vref = lshr i64 %vaddr, 3`,
` %needle = trunc i64 %vref to i32`,
]),
// Four slots per step with one branch, then one at a time.
` br label %quad`,
`quad:`,
@@ -89,9 +102,9 @@ export function emitArraySearchHelpers(
`quadbody:`,
...[0, 1, 2, 3].flatMap((k) => [
` %qi${k} = add ${sz} %q, ${k}`,
` %qp${k} = getelementptr inbounds i64, ptr %data, ${sz} %qi${k}`,
` %qs${k} = load i64, ptr %qp${k}`,
` %qh${k} = icmp eq i64 %qs${k}, %needle`,
` %qp${k} = getelementptr inbounds ${slot}, ptr %data, ${sz} %qi${k}`,
` %qs${k} = load ${slot}, ptr %qp${k}`,
` %qh${k} = icmp eq ${slot} %qs${k}, %needle`,
]),
` %qh01 = or i1 %qh0, %qh1`,
` %qh23 = or i1 %qh2, %qh3`,
@@ -107,9 +120,9 @@ export function emitArraySearchHelpers(
` %done = icmp eq ${sz} %i, %len`,
` br i1 %done, label %miss, label %body`,
`body:`,
` %sp = getelementptr inbounds i64, ptr %data, ${sz} %i`,
` %s = load i64, ptr %sp`,
` %hit = icmp eq i64 %s, %needle`,
` %sp = getelementptr inbounds ${slot}, ptr %data, ${sz} %i`,
` %s = load ${slot}, ptr %sp`,
` %hit = icmp eq ${slot} %s, %needle`,
` br i1 %hit, label %found, label %cont`,
);
} else {
+2 -2
View File
@@ -95,8 +95,8 @@ export class BlockBuilder {
this.terminate(`br label %${label}`);
}
condBr(v: string, t: string, f: string): void {
this.terminate(`br i1 ${v}, label %${t}, label %${f}`);
condBr(v: string, t: string, f: string, attachment = ""): void {
this.terminate(`br i1 ${v}, label %${t}, label %${f}${attachment}`);
}
/** Emit a double-indexed `for (i = 0; i < len; i++)` loop. `next` is
@@ -43,6 +43,7 @@ export function findInitializedByteLoopBindings(
if (stmt.init) list([stmt.init], inner);
list(stmt.body, inner);
}
if (stmt.kind === "while") entries.set(stmt, new Set(known));
if (stmt.kind === "while" || stmt.kind === "doWhile") list(stmt.body, new Set(known));
}
}
@@ -99,6 +99,7 @@ function match(
f: IrFunction,
loop: IrStmt & { kind: "for" },
captured: string[] = [],
scalarStrings: string[] = [],
): BytesLoopVersion | null {
const ranges = analyzeIntegerRanges(f);
const locals = new Map(f.locals.map((l) => [l.id, l]));
@@ -109,6 +110,9 @@ function match(
ranges,
initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(),
proven: new Set(),
workers: false,
owned: new Set(),
scalarStrings: new Set(scalarStrings),
});
}
@@ -132,6 +136,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))]);
@@ -142,6 +179,11 @@ test("statically ranged indices compare one constant with the length", () => {
]);
expect(version.affine).toEqual([]);
expect(version.counted).toBeNull();
// A uint32 index (an array counter's range) gets no guard: no typed array
// in practice has 2^31 elements.
const wide = bin(">>>", bin("^", ref("i"), ref("k")), num(0));
const unbounded = counted([set(ref("out", f64s), num(0), get(ref("table", i32s), wide))]);
expect(match(fn([unbounded]), unbounded)!.fixed).toEqual([{ receiver: 0, end: 0 }]);
});
test("scaled counters and body temps resolve to their receiver and index", () => {
@@ -209,6 +251,178 @@ 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("while loop counters below a bound version their accesses", () => {
// while (i < n) { out[i] = src[i + 1]; i = i + 1 }
const step: IrStmt = { kind: "assign", localId: "i", value: bin("+", ref("i"), num(1)), loc };
const whileLoop = (body: IrStmt[]): IrStmt & { kind: "while" } => ({
kind: "while",
cond: { kind: "bin", op: "<", left: ref("i"), right: ref("n"), type: BOOL, loc },
body,
loc,
});
const copy = whileLoop([
set(ref("out", f64s), ref("i"), get(ref("src", f64s), bin("+", ref("i"), num(1)))),
step,
]);
const declared = (loop: IrStmt): IrFunction =>
fn([{ kind: "varDecl", localId: "i", init: ref("k"), loc }, loop]);
const versionOf = (loop: IrStmt & { kind: "while" }): BytesLoopVersion | null => {
const f = declared(loop);
const ranges = analyzeIntegerRanges(f);
return matchBytesLoopVersion(loop, null, {
locals: new Map(f.locals.map((l) => [l.id, l])),
invariantGlobals: new Set(),
captured: new Set(),
ranges,
initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(),
proven: new Set(),
scalarStrings: new Set(),
workers: false,
owned: new Set(),
});
};
const version = versionOf(copy)!;
expect(version.bounds.size).toBe(2);
expect(version.bounded.map((c) => [version.receivers[c.receiver]!.ref, c.slack])).toEqual([
[ref("out", f64s), 0],
[ref("src", f64s), 1],
]);
// `out[i]` needs `i` to start at 0 or above (`src[i + 1]` only at -1).
expect(version.counters.map((c) => [c.ref.kind === "varRef" && c.ref.localId, c.min])).toEqual([
["i", 0],
]);
// An access after the counter advances in the same pass stays checked.
const advanced = whileLoop([step, set(ref("out", f64s), ref("i"), num(0))]);
expect(versionOf(advanced)).toBeNull();
const mod: IrModule = {
irVersion: 17,
sourceFile: loc.file,
entry: "__main",
functions: [
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
declared(copy),
],
};
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(fast).not.toContain("bytes.index");
expect(body.slice(0, body.indexOf(fast))).toContain("bytes.index.invalid");
});
test("Float16Array accesses version in elements and share the element tag", () => {
const f16s: IrType = { kind: "bytes", elem: "f16" };
const withHalves = (body: IrStmt[]): IrFunction => {
@@ -276,3 +490,131 @@ test("bodies compiled without optimization keep one checked loop", () => {
expect(body).not.toContain("bytes.version");
expect(body).toContain("bytes.index.invalid");
});
const STR: IrType = { kind: "string" };
const charCode = (receiver: IrExpr, index: IrExpr): IrExpr => ({
kind: "strIntrinsic",
method: "charCodeAt",
receiver,
args: [index],
type: F64,
loc,
});
/** The kernel with a string parameter `text`. */
function textKernel(body: IrStmt[], mutable = false): IrFunction {
const kernel = fn(body, [local("text", STR, mutable)]);
kernel.params.push({ localId: "text", name: "text", type: STR });
return kernel;
}
test("charCodeAt reads of a string binding version with the typed-array reads", () => {
const read = charCode(ref("text", STR), bin("+", ref("i"), num(1)));
const loop = counted([set(ref("out", f64s), ref("i"), read)]);
const version = match(textKernel([loop]), loop)!;
expect(version.bounds.has(read)).toBe(true);
expect(
version.receivers.map((r) => [r.ref.kind === "varRef" && r.ref.localId, r.string]),
).toEqual([
["out", false],
["text", true],
]);
expect(version.affine.map((c) => [c.receiver, c.minOffset, c.maxEnd])).toEqual([
[0, 0, 0],
[1, 1, 1],
]);
// A string the loop rebinds keeps its checked reads.
const rebinding = counted([
set(ref("out", f64s), ref("i"), charCode(ref("text", STR), ref("i"))),
{ kind: "assign", localId: "text", value: ref("text", STR), loc },
]);
const rebound = match(textKernel([rebinding], true), rebinding)!;
expect(rebound.receivers.map((r) => r.string)).toEqual([false]);
// A string held as a slice of another string has no header to test.
const sliced = match(textKernel([loop]), loop, [], ["text"])!;
expect(sliced.receivers.map((r) => r.string)).toEqual([false]);
expect(sliced.bounds.has(read)).toBe(false);
});
test("charAt reads of a string binding version as one-unit reads", () => {
const read: IrExpr = {
kind: "strIntrinsic",
method: "charAt",
receiver: ref("text", STR),
args: [bin("-", ref("i"), num(1))],
type: STR,
loc,
};
const loop = counted([{ kind: "exprStmt", expr: read, loc }]);
const version = match(textKernel([loop]), loop)!;
expect(version.bounds.has(read)).toBe(true);
expect(version.receivers.map((r) => r.string)).toEqual([true]);
expect(version.affine.map((c) => [c.receiver, c.minOffset, c.maxEnd])).toEqual([[0, -1, -1]]);
});
test("the guard tests a string's ASCII bit and the fast copy reads bytes", () => {
const loop = counted([set(ref("out", f64s), ref("i"), charCode(ref("text", STR), ref("i")))]);
const kernel = textKernel([loop]);
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\.slow\d*:/m));
expect(guard).toMatch(/load i32, ptr %\S+\n\s+%\S+ = icmp slt i32 %\S+, 0/);
const fast = body.slice(body.lastIndexOf("bytes.version.fast"));
expect(fast).toMatch(/load i8, ptr %\S+\n\s+%\S+ = uitofp i8/);
expect(fast).not.toContain("@sc_str_char_code_at_int");
// The checked copy keeps the inline helper and its fallback.
expect(body).toContain("@sc_str_char_code_at_int");
});
test("while loop counters below a string's length version its reads", () => {
// while (i < text.length && text.charCodeAt(i) === 32) i = i + 1
const text = ref("text", STR);
const read = charCode(text, ref("i"));
const length: IrExpr = {
kind: "strIntrinsic",
method: "length",
receiver: text,
args: [],
type: F64,
loc,
};
const loop: IrStmt & { kind: "while" } = {
kind: "while",
cond: {
kind: "logical",
op: "&&",
left: { kind: "bin", op: "<", left: ref("i"), right: length, type: BOOL, loc },
right: { kind: "bin", op: "===", left: read, right: num(32), type: BOOL, loc },
type: BOOL,
loc,
},
body: [{ kind: "assign", localId: "i", value: bin("+", ref("i"), num(1)), loc }],
loc,
};
const f = textKernel([{ kind: "varDecl", localId: "i", init: ref("k"), loc }, loop]);
const version = matchBytesLoopVersion(loop, null, {
locals: new Map(f.locals.map((l) => [l.id, l])),
invariantGlobals: new Set(),
captured: new Set(),
ranges: analyzeIntegerRanges(f),
initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(),
proven: new Set(),
scalarStrings: new Set(),
workers: false,
owned: new Set(),
})!;
// The read is bounded by the counter's test against the length, not by
// a static range the guard could not meet.
expect(version.bounds.has(read)).toBe(true);
expect(version.receivers.map((r) => r.string)).toEqual([true]);
expect(version.bounded.map((c) => c.slack)).toEqual([0]);
expect(version.fixed).toEqual([]);
});
@@ -1,4 +1,4 @@
import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js";
import { F64, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js";
import type { IntegerRange, IntegerRanges } from "../../ir/integer-ranges.js";
import type { IntegerCountedForLoop } from "../../ir/integer-loops.js";
import type { LlvmEmitterContext } from "./expr-context.js";
@@ -6,7 +6,9 @@ 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";
import { emitStringAsciiFlag, emitStringByteLength } from "./string-reads.js";
type Node = IrExpr | IrStmt;
type VarRef = IrExpr & { kind: "varRef" };
@@ -22,6 +24,24 @@ interface KeyedTerm extends Term {
key: string;
}
/** Every access of `receiver` at a while loop counter `c + d` happens while
* `c < bound` (`c <= bound` when `inclusive`) holds and before the counter
* advances, so it is in bounds when `bound + slack <= length`. */
interface BoundedCheck {
receiver: number;
bound: IrExpr;
slack: number;
}
/** A while loop counter whose entry value the guard tests: an exact
* integer of at least `min`. A counter that `| 0` wraps also needs every
* bound of its loops at most `wrapLimit`, so that it never reaches 2^31. */
interface CounterCheck {
ref: VarRef;
min: number;
wrapBounds: { bound: IrExpr; limit: number }[];
}
/** Every access of `receiver` at a static index range ends below `end + 1`. */
interface FixedCheck {
receiver: number;
@@ -41,10 +61,22 @@ interface AffineCheck {
export interface BytesLoopVersion {
/** Typed-array accesses that are in bounds whenever the guard holds. */
bounds: ReadonlySet<Node>;
/** Root receiver bindings, read once before the loop. */
receivers: { ref: VarRef; global: boolean }[];
/** 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; `string` marks a
* string whose `charCodeAt` and `charAt` reads the version covers. */
receivers: { ref: VarRef; global: boolean; string: boolean }[];
/** Receivers the guard requires to have no shared storage. */
ownedReceivers: number[];
fixed: FixedCheck[];
affine: AffineCheck[];
/** While loops: accesses at a counter below a loop-invariant bound. */
bounded: BoundedCheck[];
counters: CounterCheck[];
/** 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,13 +98,24 @@ 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>;
/** String bindings held as a slice of another string (string-slices.ts):
* they have no string header for the guard to read. */
scalarStrings: ReadonlySet<string>;
}
const MAX_COST = 400;
const MAX_FIXED_END = 2 ** 32;
const MAX_FIXED_END = 2 ** 31;
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,14 +128,29 @@ 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.
*
* `s.charCodeAt(index)` and `s.charAt(index)` (so `s[index]`) of a string
* binding qualify the same way. A string is immutable: its bytes, its
* length and the capacity-word bit that proves it ASCII (string-reads.ts)
* are fixed when it is built. The guard also requires that bit, so each
* UTF-16 unit is one byte and the length is the byte length. The fast copy
* reads each unit with one byte load, and each character as the immortal
* one-character string for its byte.
*
* 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
* whenever the guard fails, and keeps every out-of-bounds read, ignored
* write and NaN-index behavior. */
export function matchBytesLoopVersion(
loop: IrStmt & { kind: "for" },
loop: IrStmt & { kind: "for" | "while" },
counted: IntegerCountedForLoop | null,
facts: BytesLoopFacts,
): BytesLoopVersion | null {
@@ -131,10 +189,10 @@ export function matchBytesLoopVersion(
// writes is not invariant either: `for (let i = (a = b, 0); ...)` must
// not keep the bounds the guard proved for the old `a`.
if (
(loop.init !== null && !scanStmt(loop.init)) ||
(loop.kind === "for" && loop.init !== null && !scanStmt(loop.init)) ||
!loop.body.every(scanStmt) ||
(loop.cond !== null && !scanExpr(loop.cond)) ||
(loop.update !== null && !scanStmt(loop.update))
(loop.kind === "for" && loop.update !== null && !scanStmt(loop.update))
)
return null;
// A body temp stands for its initializer only when it is a constant
@@ -172,7 +230,8 @@ export function matchBytesLoopVersion(
};
const resolving = new Set<string>();
const root = (e: IrExpr): VarRef | null => {
if (e.kind !== "varRef" || e.type.kind !== "bytes") return null;
if (e.kind !== "varRef" || (e.type.kind !== "bytes" && e.type.kind !== "string")) return null;
if (facts.scalarStrings.has(e.localId)) return null;
const init = temp(e.localId);
if (init) {
if (resolving.has(e.localId)) return null;
@@ -183,9 +242,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 +253,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 +281,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,11 +368,49 @@ 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);
const receivers: { ref: VarRef; global: boolean }[] = [];
const whileAccesses = loop.kind === "while" ? whileCounterAccesses(loop) : null;
function whileCounterAccesses(loop: IrStmt & { kind: "while" }) {
return findWhileCounterAccesses(loop, {
counterLocal: (id) => {
const local = facts.locals.get(id);
return (
local?.type.kind === "f64" &&
!local.boxed &&
!local.tdz &&
!facts.captured.has(id) &&
facts.initialized.has(id)
);
},
boundKey: (e) => {
if (e.type.kind !== "f64") return null;
// The guard evaluates the bound before the loop: a typed array's
// length needs the binding itself, not a temp the body declares.
if (
e.kind === "bytesIntrinsic" &&
e.method === "length" &&
e.receiver.kind === "varRef" &&
e.receiver.type.kind === "bytes" &&
invariant(e.receiver.localId)
)
return `bytes-length:${e.receiver.localId}`;
if (
e.kind === "strIntrinsic" &&
e.method === "length" &&
e.args.length === 0 &&
e.receiver.kind === "varRef" &&
e.receiver.type.kind === "string" &&
invariant(e.receiver.localId)
)
return `string-length:${e.receiver.localId}`;
return pureKey(e);
},
});
}
const receivers: { ref: VarRef; global: boolean; string: boolean }[] = [];
const receiverIndex = new Map<string, number>();
const fixed = new Map<number, { check: FixedCheck; nodes: Node[] }>();
const affineChecks = new Map<
@@ -310,14 +422,32 @@ export function matchBytesLoopVersion(
if (index === undefined) {
index = receivers.length;
receiverIndex.set(ref.localId, index);
receivers.push({ ref, global: !facts.locals.has(ref.localId) });
receivers.push({
ref,
global: !facts.locals.has(ref.localId),
string: ref.type.kind === "string",
});
}
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[]>();
// While loops: accesses at a counter below a bound, by receiver and bound.
const boundedChecks = new Map<
string,
{ check: BoundedCheck; nodes: { node: Node; access: WhileAccess }[] }
>();
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 && ref.type.kind === "bytes" && !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>();
@@ -342,11 +472,30 @@ export function matchBytesLoopVersion(
});
return;
}
const bounded = whileAccesses?.accesses.get(node);
const loopCounter = bounded && whileAccesses!.counters.get(bounded.counter);
if (bounded && loopCounter && (!loopCounter.wraps || loopCounter.bounds !== null)) {
const receiver = receiverOf(ref);
const key = `${receiver}|${bounded.key}|${bounded.adj}`;
const slack = bounded.offset + width - 1 + bounded.adj;
const group = boundedChecks.get(key);
if (group) {
group.check.slack = Math.max(group.check.slack, slack);
group.nodes.push({ node, access: bounded });
} else
boundedChecks.set(key, {
check: { receiver, bound: bounded.bound, slack },
nodes: [{ node, access: bounded }],
});
return;
}
const range = facts.ranges.get(index);
if (!range) return;
const end = range.max + width - 1;
// Only a length a buffer can have: an outer loop must not pay for a
// guard that bounds an inner loop's unbounded counter.
// Only an end below 2^31: an outer loop must not pay for a guard that
// bounds an inner loop's unbounded counter, and the counter of a loop to
// an array's length reaches 2^32 - 2, a length no typed array in
// practice has.
if (range.min < 0 || end >= MAX_FIXED_END) return;
const receiver = receiverOf(ref);
const group = fixed.get(receiver);
@@ -360,7 +509,12 @@ export function matchBytesLoopVersion(
const numeric = byteNumberAccess(e);
if (numeric) access(e, e.receiver, e.args[numeric.offsetArg]!, numeric.width);
else if (e.method === "get" && e.args[0]) access(e, e.receiver, e.args[0], 1);
}
} else if (
e.kind === "strIntrinsic" &&
(e.method === "charCodeAt" || e.method === "charAt") &&
e.args[0]
)
access(e, e.receiver, e.args[0], 1);
return everyExprChild(e, visitExpr, visitStmt);
};
const visitStmt = (s: IrStmt): boolean => {
@@ -368,6 +522,7 @@ export function matchBytesLoopVersion(
return everyStmtChild(s, visitExpr, visitStmt);
};
loop.body.every(visitStmt);
if (loop.kind === "while") visitExpr(loop.cond);
// Bound the counter so every counter operand stays a safe integer.
let bound = SAFE;
const affineGroups = [...affineChecks.values()].filter((group) => {
@@ -383,9 +538,25 @@ 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 boundedKept = [...boundedChecks.values()].slice(
0,
Math.max(0, MAX_CHECKS - fixedKept.length - affineKept.length),
);
const ownedKept = [...owned.entries()].slice(0, MAX_CHECKS);
if (
fixedKept.length === 0 &&
affineKept.length === 0 &&
boundedKept.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 +568,65 @@ 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);
// While loop counters of the kept checks: each entry value is an exact
// integer that no access offset takes below 0. In the fast copy each
// access's index is below its length, and its counter read at least the
// most negative offset.
const narrow = (e: IrExpr, range: IntegerRange): void => {
const known = ranges.get(e) ?? facts.ranges.get(e);
ranges.set(
e,
known ? { min: Math.max(known.min, range.min), max: Math.min(known.max, range.max) } : range,
);
};
const counterMins = new Map<string, number>();
for (const group of boundedKept) {
used.add(group.check.receiver);
for (const { node, access } of group.nodes) {
bounds.add(node);
const min = 0 - access.offset;
counterMins.set(access.counter, Math.max(counterMins.get(access.counter) ?? -SAFE, min));
narrow(access.ref, { min: -MAX_OFFSET, max: SAFE });
if (access.index !== access.ref) narrow(access.index, { min: 0, max: SAFE });
}
}
const counterChecks: CounterCheck[] = [];
for (const [id, min] of counterMins) {
const info = whileAccesses!.counters.get(id)!;
counterChecks.push({
ref: { kind: "varRef", localId: id, type: F64, loc: loop.loc },
min,
wrapBounds: info.wraps
? info.bounds!.map((b) => ({ bound: b.bound, limit: 2 ** 31 - info.sum - b.adj }))
: [],
});
}
// 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 +636,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,14 +650,330 @@ export function matchBytesLoopVersion(
minOffset: g.check.minOffset,
maxEnd: g.check.maxEnd,
})),
bounded: boundedKept.map((g) => ({
receiver: renumber.get(g.check.receiver)!,
bound: g.check.bound,
slack: g.check.slack,
})),
counters: counterChecks,
guarded,
counted: affineKept.length > 0 ? counted : null,
counterBound: needsBound ? bound : null,
ranges,
};
}
/** A typed-array access at a while loop counter: `counter + offset`, read
* or written while `counter < bound` (`<=` when `adj` is 1) holds. */
interface WhileAccess {
counter: string;
/** The index, and the counter's read in it. */
index: IrExpr;
ref: IrExpr;
bound: IrExpr;
key: string;
adj: number;
offset: number;
}
interface WhileCounter {
/** Some increment is `c = (c + k) | 0`. */
wraps: boolean;
/** The sum of every increment literal in the loop. */
sum: number;
/** One bound from the condition of the loop and of every nested while
* loop that advances the counter; null when one of them has none. */
bounds: { bound: IrExpr; adj: number }[] | null;
}
/** `counter = counter + k`, `counter = (counter + k) | 0` or `counter++`. */
function counterStep(s: IrStmt): { id: string; k: number; wraps: boolean } | null {
if (s.kind === "exprStmt" && s.expr.kind === "incDec" && s.expr.op === "+")
return { id: s.expr.localId, k: 1, wraps: false };
if (s.kind !== "assign") return null;
let value = s.value,
wraps = false;
if (
value.kind === "bin" &&
value.op === "|" &&
value.right.kind === "numLit" &&
value.right.value === 0 &&
value.left.kind === "bin"
) {
value = value.left;
wraps = true;
}
if (
value.kind !== "bin" ||
value.op !== "+" ||
value.left.kind !== "varRef" ||
value.left.localId !== s.localId ||
value.right.kind !== "numLit" ||
!Number.isSafeInteger(value.right.value) ||
value.right.value <= 0 ||
value.right.value > MAX_OFFSET
)
return null;
return { id: s.localId, k: value.right.value, wraps };
}
/** Typed-array and string accesses of a while loop that happen at a
* counter below a loop-invariant bound. A counter is a number local that
* the loop changes only by positive integer increments, written as
* statements of the body, of its blocks and branches, or of nested while
* loops. The condition of the loop, an `if` or a `&&` chain that compares
* the counter below a bound (`while (pos < end && bytes[pos] === 32)
* pos++`, or `s.charCodeAt(pos)` of a string) makes it below that bound
* until the next increment on the path. The counter starts at its entry
* value and only grows (a `| 0` increment never wraps when every bound is
* at most 2^31 minus the increments), so an access at `counter + d` is in
* bounds for the whole loop when the entry value is at least `-d` and
* `bound + d < length` (`+ 1` for `<=`). */
function findWhileCounterAccesses(
loop: IrStmt & { kind: "while" },
host: {
counterLocal: (id: string) => boolean;
/** A key for a bound expression whose value the loop never changes. */
boundKey: (e: IrExpr) => string | null;
},
): { accesses: Map<Node, WhileAccess>; counters: Map<string, WhileCounter> } {
const steps = new Map<string, { sum: number; wraps: boolean }>();
const refused = new Set<string>();
// Increments count only as statements of the loop's straight-line
// structure; any other write refuses the binding.
const writes = (list: readonly IrStmt[], direct: boolean): void => {
for (const s of list) {
const step = counterStep(s);
if (step && direct) {
const prior = steps.get(step.id);
steps.set(step.id, {
sum: (prior?.sum ?? 0) + step.k,
wraps: (prior?.wraps ?? false) || step.wraps,
});
continue;
}
if (step) refused.add(step.id);
const nested =
direct &&
(s.kind === "if" || s.kind === "while" || (s.kind === "block" && !s.labels?.length));
if (s.kind === "assign" || s.kind === "varDecl" || s.kind === "forOf") refused.add(s.localId);
if (s.kind === "tryCatch" && s.catchLocalId) refused.add(s.catchLocalId);
everyStmtChild(
s,
(e) => (exprWrites(e), true),
(c) => (writes([c], nested), true),
);
}
};
const exprWrites = (e: IrExpr): void => {
if (e.kind === "assignExpr" || e.kind === "incDec") refused.add(e.localId);
everyExprChild(
e,
(c) => (exprWrites(c), true),
(c) => (writes([c], false), true),
);
};
exprWrites(loop.cond);
writes(loop.body, true);
const counter = (id: string): boolean =>
steps.has(id) && !refused.has(id) && host.counterLocal(id);
type Fact = { bound: IrExpr; key: string; adj: number };
type Clean = ReadonlyMap<string, Fact>;
// The bounds a condition puts on counters while it holds.
const facts = (cond: IrExpr, into: Map<string, Fact>): void => {
if (cond.kind === "toBool") return facts(cond.operand, into);
if (cond.kind === "logical" && cond.op === "&&") {
facts(cond.left, into);
facts(cond.right, into);
return;
}
if (cond.kind !== "bin") return;
const below = cond.op === "<" || cond.op === "<=",
above = cond.op === ">" || cond.op === ">=";
const c = below ? cond.left : above ? cond.right : null,
bound = below ? cond.right : cond.left;
if (c?.kind !== "varRef" || !counter(c.localId) || into.has(c.localId)) return;
const key = host.boundKey(bound);
if (key === null) return;
into.set(c.localId, { bound, key, adj: cond.op === "<=" || cond.op === ">=" ? 1 : 0 });
};
// The facts that hold after either of two paths.
const meet = (a: Clean | null, b: Clean | null): Clean | null => {
if (a === null) return b;
if (b === null) return a;
const out = new Map<string, Fact>();
for (const [id, fact] of a) if (b.get(id) === fact) out.set(id, fact);
return out;
};
const withFacts = (clean: Clean, cond: IrExpr): Clean => {
const out = new Map(clean);
facts(cond, out);
return out;
};
const accesses = new Map<Node, WhileAccess>();
const record = (node: Node, index: IrExpr, clean: Clean): void => {
let ref: IrExpr = index,
offset = 0;
if (index.kind === "bin" && (index.op === "+" || index.op === "-")) {
const literal =
index.right.kind === "numLit" ? index.right : index.op === "+" ? index.left : null;
const other = literal === index.right ? index.left : index.right;
if (literal?.kind !== "numLit" || !Number.isSafeInteger(literal.value)) return;
if (Math.abs(literal.value) > MAX_OFFSET || Object.is(literal.value, -0)) return;
offset = index.op === "-" ? -literal.value : literal.value;
ref = other;
}
if (ref.kind !== "varRef") return;
const fact = clean.get(ref.localId);
if (fact) accesses.set(node, { counter: ref.localId, index, ref, ...fact, offset });
};
const expr = (e: IrExpr, clean: Clean): void => {
if (e.kind === "bytesIntrinsic") {
const numeric = byteNumberAccess(e);
const index = numeric
? e.args[numeric.offsetArg]
: e.method === "get"
? e.args[0]
: undefined;
if (index) record(e, index, clean);
}
if (
e.kind === "strIntrinsic" &&
(e.method === "charCodeAt" || e.method === "charAt") &&
e.args[0]
)
record(e, e.args[0], clean);
if (e.kind === "logical" && e.op === "&&") {
expr(e.left, clean);
expr(e.right, withFacts(clean, e.left));
return;
}
if (e.kind === "ternary") {
expr(e.cond, clean);
expr(e.then, withFacts(clean, e.cond));
expr(e.else_, clean);
return;
}
everyExprChild(
e,
(c) => (expr(c, clean), true),
(c) => (stmts([c], clean), true),
);
};
// Walks statements in order; the result is the clean state where the
// list falls through, or null when it never does.
const stmts = (list: readonly IrStmt[], clean: Clean): Clean | null => {
let current: Clean | null = clean;
for (const s of list) {
if (current === null) return null;
const step = counterStep(s);
if (step) {
const next: Map<string, Fact> = new Map(current);
next.delete(step.id);
current = next;
continue;
}
switch (s.kind) {
case "if": {
expr(s.cond, current);
const then = stmts(s.then, withFacts(current, s.cond));
const otherwise = stmts(s.else_ ?? [], current);
current = meet(then, otherwise);
break;
}
case "while": {
// A pass of the nested loop may follow its own increments.
const advanced: Map<string, Fact> = new Map(current);
for (const id of writtenCounters(s.body)) advanced.delete(id);
expr(s.cond, advanced);
stmts(s.body, withFacts(advanced, s.cond));
current = advanced;
break;
}
case "block":
if (!s.labels?.length) {
current = stmts(s.body, current);
break;
}
everyStmtChild(
s,
(e) => (expr(e, current!), true),
(c) => (stmts([c], current!), true),
);
break;
case "bytesSet":
record(s, s.index, current);
everyStmtChild(
s,
(e) => (expr(e, current!), true),
(c) => (stmts([c], current!), true),
);
break;
case "break":
case "continue":
case "return":
case "throw":
everyStmtChild(
s,
(e) => (expr(e, current!), true),
(c) => (stmts([c], current!), true),
);
current = null;
break;
default:
everyStmtChild(
s,
(e) => (expr(e, current!), true),
(c) => (stmts([c], current!), true),
);
break;
}
}
return current;
};
const writtenCounters = (list: readonly IrStmt[]): Set<string> => {
const out = new Set<string>();
const visit = (s: IrStmt): boolean => {
const step = counterStep(s);
if (step) out.add(step.id);
return everyStmtChild(s, () => true, visit);
};
list.every(visit);
return out;
};
expr(loop.cond, new Map());
stmts(loop.body, withFacts(new Map(), loop.cond));
// Each counter's bounds: the loop's and those of nested while loops that
// advance it.
const counters = new Map<string, WhileCounter>();
const loops: (IrStmt & { kind: "while" })[] = [loop];
const collectLoops = (s: IrStmt): boolean => {
if (s.kind === "while") loops.push(s);
return everyStmtChild(s, () => true, collectLoops);
};
loop.body.every(collectLoops);
for (const [id, step] of steps) {
if (!counter(id)) continue;
let bounds: WhileCounter["bounds"] = [];
for (const l of loops) {
if (l !== loop && !writtenCounters(l.body).has(id)) continue;
const fact = withFacts(new Map(), l.cond).get(id);
if (!fact) {
bounds = null;
break;
}
bounds.push({ bound: fact.bound, adj: fact.adj });
}
counters.set(id, { ...step, bounds });
}
return { accesses, counters };
}
/** 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,
@@ -465,19 +999,63 @@ export function emitBytesLoopVersionGuard(
}
return value;
});
const lengths = receivers.map((receiver) => {
const len = emitBytesHeaderLength(host, receiver);
const lengths = receivers.map((receiver, index) => {
const len = version.receivers[index]!.string
? emitStringByteLength(host, receiver)
: emitBytesHeaderLength(host, receiver);
if (host.sizeType === "i64") return len;
const wide = B.tmp();
B.line(`${wide} = zext ${host.sizeType} ${len} to i64`);
return wide;
});
const conditions: string[] = [];
// A string's byte length is its UTF-16 length when it is ASCII.
version.receivers.forEach(({ string }, index) => {
if (string) conditions.push(emitStringAsciiFlag(host, receivers[index]!));
});
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}`);
conditions.push(ok);
}
for (const counter of version.counters) {
const value = host.emitExpr(counter.ref);
// A counter held as an integer (an int32 slot) is exact already and
// needs at most its lower bound tested.
const integer = exactInteger(host, value, counter.ref);
if (integer && integer.range.max <= SAFE) {
if (integer.range.min < counter.min) {
const ok = B.tmp();
B.line(`${ok} = icmp sge i64 ${widenInteger(host, integer)}, ${counter.min}`);
conditions.push(ok);
}
} else conditions.push(emitExactIntegerTest(host, value.name, counter.min, SAFE).ok);
for (const wrap of counter.wrapBounds) {
const bound = host.emitExpr(wrap.bound);
const boundInteger = exactInteger(host, bound, wrap.bound);
if (boundInteger && boundInteger.range.max <= wrap.limit) continue;
const ok = B.tmp();
B.line(`${ok} = fcmp ole double ${bound.name}, ${f64Lit(wrap.limit)}`);
conditions.push(ok);
}
}
for (const check of version.bounded) {
// `bound <= length - slack` in doubles: both sides are exact (a length
// is below 2^53 and a slack below 2^32), and NaN fails.
const bound = host.emitExpr(check.bound).name;
const length = B.tmp(),
room = B.tmp(),
ok = B.tmp();
B.line(`${length} = uitofp i64 ${lengths[check.receiver]} to double`);
B.line(`${room} = fsub double ${length}, ${f64Lit(check.slack)}`);
B.line(`${ok} = fcmp ole double ${bound}, ${room}`);
conditions.push(ok);
}
if (counted && version.affine.length > 0) {
// The first and last counter values, in ascending order. The last value
// of a non-unit step is exact: `start + ⌊(limit ∓ 1 - start) / step⌋ *
@@ -523,6 +1101,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 = emitExactIntegerTest(host, value.name, GUARDED_TERM.min, GUARDED_TERM.max);
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 +1156,39 @@ export function emitBytesLoopVersionGuard(
else B.condBr(valid, fast, slow);
}
/** Tests that a double is an exact integer within [min, max] (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 emitExactIntegerTest(
host: LlvmEmitterContext,
value: string,
min: number,
max: number,
): { 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(min)}`);
B.line(`${high} = fcmp ole double ${value}, ${f64Lit(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,24 @@
import { expect, test } from "vitest";
import { DYN, F64, STRING, VOID, type IrModule } from "../../ir/ir.js";
import {
DYN,
F64,
STRING,
VOID,
type IrExpr,
type IrFunction,
type IrModule,
type IrType,
} 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,
@@ -157,12 +168,85 @@ test("ProgramLifetime hierarchies are never freed and take no reference counts",
expect(body(mangleClassRelease("Owner"))).toContain("load i32, ptr %bufp");
});
test("arrays of ProgramLifetime instances tell the runtime their teardown releases nothing", () => {
const kids = { kind: "array" as const, elem: { kind: "object" as const, className: "Node" } };
const owners = { kind: "array" as const, elem: { kind: "object" as const, className: "Owner" } };
const mod: IrModule = {
irVersion: 17,
sourceFile: loc.file,
entry: "main",
functions: [
{
name: "main",
params: [],
returnType: VOID,
locals: [],
body: [
{ kind: "exprStmt", expr: { kind: "arrayLit", elems: [], type: kids, loc }, loc },
{ kind: "exprStmt", expr: { kind: "arrayLit", elems: [], type: owners, loc }, loc },
],
loc,
},
],
classes: [
{ name: "Node", fields: [], methods: [], loc },
{ name: "Owner", fields: [{ name: "nodes", type: kids }], methods: [], loc },
],
contracts: { programLifetime: ["Node"] },
};
const llvm = emitLlvmModule(mod, { pointerBits: 64 });
const ops = (cls: string): string =>
new RegExp(`^@sc_arrops\\.[^\\n]*${mangleClassRelease(cls)}[^\\n]*$`, "m").exec(llvm)?.[0] ??
"";
// SCR_ARR_OPS_RELEASE_NOTHING for Node; Owner's elements are released.
expect(ops("Node")).toMatch(
/= internal global \{ ptr, ptr, ptr, i32, i32 \} .*, i32 0, i32 2 \}$/,
);
expect(ops("Owner")).toMatch(/= internal global %ScrArrOps .*, i32 0 \}$/);
});
test("classes whose trace visits nothing start acyclic in a traced hierarchy", () => {
const mapper = { kind: "object" as const, className: "Mapper" };
const mod: IrModule = {
irVersion: 17,
sourceFile: loc.file,
entry: "main",
functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }],
classes: [
{ name: "Mapper", fields: [], methods: [], loc },
// A dyn field can reach anything: the hierarchy carries collector headers.
{
name: "Linked",
base: "Mapper",
fields: [
{ name: "next", type: mapper },
{ name: "hook", type: DYN },
],
methods: [],
loc,
},
{ name: "Simple", base: "Mapper", fields: [{ name: "value", type: F64 }], methods: [], loc },
],
};
const llvm = emitLlvmModule(mod, { pointerBits: 64 });
const body = (name: string): string =>
new RegExp(`^define internal [a-z]+ @${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(llvm)?.[0] ?? "";
const descriptor = (cls: string): string =>
llvm.split("\n").find((line) => line.startsWith(`@${mangleClassTrace(cls)}.cyctype = `)) ?? "";
expect(descriptor("Simple")).toContain("{ ptr @scr_cyc_no_children, ");
expect(body(mangleClassNew("Simple"))).toContain("store i32 -1, ptr %sa.bufp");
expect(descriptor("Linked")).toContain(`{ ptr @${mangleClassTrace("Linked")}, `);
expect(body(mangleClassNew("Linked"))).toContain("store i32 0, ptr %sa.bufp");
// Releases share the hierarchy's possible-root step, which skips the
// call for SCR_CYC_ACYCLIC, and a death unbuffers only below -1.
expect(body(mangleClassRelease("Simple"))).toContain("icmp slt i32 %buf, 0");
expect(body(mangleClassReleaseDirect("Simple"))).toContain("icmp slt i32 %dbuf, -1");
});
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 },
];
@@ -176,7 +260,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,
},
@@ -184,17 +268,210 @@ test("ProgramLifetime classes pack their fields after a 32-bit header", () => {
contracts: { programLifetime: ["Node"] },
};
const llvm = emitLlvmModule(mod, { pointerBits: 64 });
// Fields go widest first, each at the first offset that fits it. Node's
// weight (an int32 slot: nothing stores a fraction) takes the word the
// header shares (24 bytes, not 32); Leaf keeps Node's offsets and puts its
// bool next to Node's (32 bytes, not 48).
expect(llvm).toContain(`%${mangleClassStruct("Node")} = type { i32, i32, ptr, i8 }`);
expect(llvm).toContain(`%${mangleClassStruct("Leaf")} = type { i32, i32, ptr, i8, i8, ptr }`);
// Scalars narrower than 8 bytes go first, then the rest, widest first,
// each at the first offset that fits it. Node's weight (an int32 slot:
// nothing stores a fraction) takes the word the header shares and its
// flag the next one (24 bytes, not 32); Leaf keeps Node's offsets and puts
// its bool next to Node's (32 bytes, not 48).
expect(llvm).toContain(`%${mangleClassStruct("Node")} = type { i32, i32, i8, ptr }`);
expect(llvm).toContain(`%${mangleClassStruct("Leaf")} = type { i32, i32, i8, i8, ptr, ptr }`);
// Without the contract, the layout is the declared one.
const mortal = emitLlvmModule({ ...mod, contracts: {} }, { pointerBits: 64 });
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("arrays of ProgramLifetime instances keep 32-bit references in 4-byte slots", () => {
const node = { kind: "object" as const, className: "Node" };
const nodes = { kind: "array" as const, elem: node };
const ref = (localId: string, type: IrType): IrExpr => ({ kind: "varRef", localId, type, loc });
const fn = (name: string, body: IrExpr, returnType: IrType): IrFunction => ({
name,
params: [
{ name: "a", localId: "a", type: nodes },
{ name: "v", localId: "v", type: node },
],
returnType,
locals: [
{ id: "a", name: "a", type: nodes, mutable: false },
{ id: "v", name: "v", type: node, mutable: false },
],
body: [{ kind: "return", value: body, loc }],
loc,
});
const mod: IrModule = {
irVersion: 17,
sourceFile: loc.file,
entry: "main",
functions: [
{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc },
fn(
"first",
{
kind: "arrayGet",
arr: ref("a", nodes),
index: { kind: "numLit", value: 0, type: F64, loc },
type: node,
loc,
},
node,
),
fn(
"add",
{
kind: "arrIntrinsic",
method: "push",
receiver: ref("a", nodes),
args: [ref("v", node)],
type: F64,
loc,
},
F64,
),
fn("make", { kind: "arrayLit", elems: [ref("v", node)], type: nodes, loc }, nodes),
fn(
"find",
{
kind: "arrIntrinsic",
method: "indexOf",
receiver: ref("a", nodes),
args: [ref("v", node)],
type: F64,
loc,
},
F64,
),
],
classes: [{ name: "Node", fields: [{ name: "kids", type: nodes }], methods: [], loc }],
contracts: { programLifetime: ["Node"] },
};
const body = (llvm: string, name: string): string =>
llvm.split(/^define /m).find((f) => f.includes(`; ${name}\n`)) ?? "";
const llvm = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false, lowRegion: true });
// The element type's ops say so (SCR_ARR_OPS_COMPACT), so the runtime makes
// the arrays SCR_ELEM_CREF.
expect(llvm).toMatch(
/^@sc_arrops\.\S+\.c = internal global \{ ptr, ptr, ptr, i32, i32 \} \{ [^}]*, i32 0, i32 1 \}$/m,
);
// A read loads the 32-bit reference and shifts it; an append stores one.
const first = body(llvm, "first");
expect(first).toMatch(/getelementptr inbounds i32, ptr %\S+, i64 \S+\n\s+%\S+ = load i32/);
// The states follow 4-byte slots.
expect(first).toContain("getelementptr inbounds [5 x i8]");
expect(first).toMatch(/= inttoptr i64 %\S+ to ptr/);
expect(body(llvm, "add")).toMatch(
/= lshr i64 %\S+, 3\n\s+%\S+ = trunc i64 %\S+ to i32\n\s+store i32/,
);
// indexOf scans the 4-byte slots for the needle's reference.
expect(llvm).toMatch(/call double @sc_arr_index_of_cref\(ptr %\S+, ptr %\S+\)/);
expect(/^define internal double @sc_arr_index_of_cref[^]*?^}/m.exec(llvm)?.[0]).toContain(
"load i32, ptr %qp0",
);
// Elsewhere the arrays keep 8-byte slots.
const elsewhere = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false });
expect(elsewhere).not.toMatch(/sc_arrops\.\S+\.c /);
expect(body(elsewhere, "first")).not.toContain("inttoptr");
expect(elsewhere).not.toContain("sc_arr_index_of_cref");
});
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 = {
+156 -19
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) {
@@ -619,16 +703,27 @@ export function classFieldTypes(host: ShapeHost, cls: IrClassDef, int32?: Int32S
/** Gives the classes `compact` admits (whole hierarchies: the test is the
* root's) the 32-bit header and packed fields. A class keeps its base's
* field offsets, and places each of its own fields, widest first, at the
* first offset after the header that fits it: in a hole another field's
* alignment left, in the base's tail padding, or at the end. So 32-bit and
* smaller fields share 8-byte words. The struct lists the header and the
* fields by offset, which LLVM's layout of the type reproduces (checked). */
* field offsets, and places each of its own fields at the first offset
* after the header that fits it: in a hole another field's alignment left,
* in the base's tail padding, or at the end. Scalars narrower than 8 bytes
* (flags, ids, counts) go first, widest first, so they share the header's
* cache line; then the other fields, widest first. So 32-bit and smaller
* fields share 8-byte words. The struct lists the header and the fields by
* offset, which LLVM's layout of the type reproduces (checked). */
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,11 +737,28 @@ 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 smallScalar = (f: { type: string; field: number; size: number }): number =>
f.size < 8 && f.type !== "ptr" && !compressed[f.field] ? 0 : 1;
const own = types
.map((type, field) => ({ type, field, ...fieldStorage(type) }))
.slice(baseCount)
.sort((a, b) => b.size - a.size || a.field - b.field);
.sort((a, b) => smallScalar(a) - smallScalar(b) || b.size - a.size || a.field - b.field);
for (const f of own) {
const sorted = [...elems].sort((a, b) => a.offset - b.offset);
let at = -1;
@@ -679,6 +791,7 @@ export function assignCompactLayouts(
meta.compact = true;
meta.compactElems = elems;
meta.compactSlots = slots;
meta.compressed = compressed;
}
}
@@ -705,7 +818,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 +864,9 @@ export function emitClassShapes(
nullable?: NullableRefFields,
int32?: Int32Slots,
): { typeDefs: string[]; defs: string[] } {
// Whether compact instances live in the program-lifetime region, where
// fields and arrays can refer to them in 32 bits (LlEmitter.compactRegion).
const regionRefs = host.compactRegion?.() === true;
const typeDefs: string[] = [];
const defs: string[] = [];
const emitted = (mod.classes ?? []).filter((c) => !c.runtime);
@@ -897,15 +1016,28 @@ 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
? indexedFields.filter((f) => traceAdapter(host, f.type) !== null)
: [];
// A traced class whose trace visits nothing carries the collector header
// only because its hierarchy is traced: it is never on a cycle. Its
// descriptor names scr_cyc_no_children and its objects start out
// SCR_CYC_ACYCLIC, so releases never buffer them.
const acyclic =
traced && runtimePrefix === null && !isStreamRooted && tracedFields.length === 0;
const bounded =
runtimePrefix !== null ||
isStreamRooted ||
@@ -1089,11 +1221,12 @@ export function emitClassShapes(
`entry:`,
];
const cycType = `@${mangleClassTrace(cls.name)}.cyctype`;
if (acyclic) host.declare(`declare void @scr_cyc_no_children(ptr, ptr, ptr)`);
if (traced)
defs.push(
cycTypeDescriptor(
cycType,
`@${mangleClassTrace(cls.name)}`,
acyclic ? "@scr_cyc_no_children" : `@${mangleClassTrace(cls.name)}`,
`@${mangleClassGcFree(cls.name)}`,
host.nextCycKind?.() ?? 0,
),
@@ -1108,12 +1241,17 @@ 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(
host,
sizeOf,
traced && !lifetime ? { type: cycType } : null,
traced && !lifetime ? { type: cycType, acyclic } : null,
storedHeaderBytes(meta),
),
);
@@ -1188,7 +1326,6 @@ export function emitClassShapes(
// exactly the complement, then free (the trace/teardown complement
// contract in scr_runtime.h).
const indexed = indexedFields;
const tracedFields = indexed.filter((f) => traceAdapter(host, f.type) !== null);
const untracedRefFields = indexed.filter(
(f) => isRefCounted(f.type) && traceAdapter(host, f.type) === null,
);
+8 -3
View File
@@ -10,9 +10,10 @@ import type {
import type { LlvmEmitterContext } from "./expr-context.js";
/** Field numbers of %ScrArr, which mirrors scr_runtime.h's ScrArr field for
* field (its static assertions pin `len`, `cap`, `cyc`, `filled` and
* `data`). `len` and `cap` are i32 on every target; the dense states end
* the allocation after the `cap` value slots at `data` (scrArrStates). */
* field (its static assertions pin `len`, `cap`, `cyc`, `spare`, `filled`
* and `data`). `len` and `cap` are i32 on every target; the dense states of
* an array with states (SCR_ARR_STATES in `spare`) end the allocation after
* the `cap` value slots at `data` (scrArrStates). */
export const SCR_ARR_FIELD = {
rc: 0,
len: 1,
@@ -24,6 +25,10 @@ export const SCR_ARR_FIELD = {
data: 7,
} as const;
/** The `spare` flag of a %ScrArr whose dense slots carry state bytes
* (SCR_ARR_STATES in scr_array.h). */
export const SCR_ARR_STATES = 16;
/** Loads the i32 `len` or `cap` of a %ScrArr at `ptr` into `dest` as a
* size-type count. */
export function loadArrCount(
@@ -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 &&
@@ -248,11 +252,13 @@ export function findConstantCallbacks(
// entries actually used as indirect callees in each body. A module-wide
// global/function cross product would dominate large-source emission.
const globalTargets = new Map<string, string>();
const result = new Map<string, Map<string, string>>();
// Only indirect callees get targets: without one, nothing resolves.
if (![...indirectCallees.values()].some((callees) => callees.size > 0)) return result;
for (const [id, value] of globals) {
const target = resolve(null, value);
if (target) globalTargets.set(id, target);
}
const result = new Map<string, Map<string, string>>();
for (const fn of mod.functions) {
const needed = indirectCallees.get(fn.name)!;
if (!needed.size) continue;
@@ -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;
}
@@ -316,6 +316,55 @@ test.each([32, 64] as const)(
},
);
test("arrays without state bytes skip the state stores", () => {
const read: IrExpr = { kind: "arrayGet", arr: ref("s", words), index, type: STRING, loc };
const write: IrStmt = {
kind: "arraySet",
arr: ref("s", words),
index,
value: { kind: "strLit", value: "x", type: STRING, loc },
loc,
};
const push: IrStmt = {
kind: "exprStmt",
expr: {
kind: "arrIntrinsic",
method: "push",
receiver: ref("a", nums),
args: [{ kind: "numLit", value: 2.5, type: F64, loc }],
type: F64,
loc,
},
loc,
};
const llvm = emitLlvmModule(
module("plain", STRING, [push, write, { kind: "return", value: read, loc }]),
);
const fn = body(llvm, "plain");
// SCR_ARR_STATES (16) in `spare`, field 5.
const flag = String.raw`getelementptr inbounds %ScrArr, ptr %t\d+, i32 0, i32 5\n %t\d+ = load i16, ptr %t\d+, !tbaa !\d+\n %t\d+ = and i16 %t\d+, 16\n %t\d+ = icmp ne i16 %t\d+, 0\n`;
// A push marks the state only when the array has states.
expect(fn).toMatch(
new RegExp(
`${flag} br i1 %t\\d+, label %arr\\.push\\.state\\d+, label %arr\\.push\\.marked\\d+\\narr\\.push\\.state\\d+:\\n store i8 1, ptr `,
),
);
// Above the prefix, an array without states has no elements (its prefix
// is its length): a store there appends at the length and replaces no
// value.
expect(fn).toMatch(
new RegExp(
`${flag} br i1 %t\\d+, label %arr\\.set\\.states\\d+, label %arr\\.set\\.plain\\d+`,
),
);
expect(fn).toMatch(
/^arr\.set\.plain\d+:\n %t\d+ = icmp eq i64 .*\n br i1 %t\d+, label %arr\.set\.values\d+, label %arr\.set\.slow\d+$/m,
);
expect(fn).toMatch(/phi i1 \[ %t\d+, %arr\.set\.states\d+ \], \[ false, %arr\.set\.values\d+ \]/);
// Reads test no flag: they reach the state check only above the prefix.
expect(fn).not.toMatch(/label %arr\.get\.state\d+/);
});
test("state and presence queries answer dense slots inline", () => {
const state: IrExpr = { kind: "arrayState", arr: ref("b", flags), index, type: F64, loc };
const has: IrExpr = { kind: "arrayHas", arr: ref("s", words), index, type: BOOL, loc };
@@ -14,23 +14,48 @@
* state bytes altogether: the runtime keeps every state below it a present
* value, so reads load the slot directly and writes replace it without
* touching its state. Other dense indices take the state-checked path,
* whose writes extend the bound when they continue it. */
* whose writes extend the bound when they continue it. An array without
* state bytes (SCR_ARR_STATES clear in `spare`) keeps its bound at its
* length, so reads never reach the state check for it; a write past the
* bound tests the flag and, without states, appends at the length. */
import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js";
import {
f64Lit,
loadArrCount,
SCR_ARR_FIELD,
SCR_ARR_STATES,
scrArrStatePtr,
scrArrStates,
sizeToDouble,
storeArrCount,
} from "./common.js";
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import { exactInteger, widenInteger } from "./integer-values.js";
import { compactRefLoadLines, compactRefStoreLines } from "./classes.js";
import { elemAccess } from "./shapes.js";
import { indexInteger } from "./integer-values.js";
import { integerOrNaNIndex } from "./wrapping-sums.js";
const SCR_ARR_VALUE = 1;
/** Whether the array at `array` has state bytes, as an i1. `spare` changes
* only in runtime calls, so its loads take their own alias tag. */
export function emitArrayHasStates(host: LlvmEmitterContext, array: string): string {
const B = host.B;
const sparePtr = B.tmp(),
spare = B.tmp(),
bit = B.tmp(),
has = B.tmp();
B.line(
`${sparePtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.spare}`,
);
host.markMemoryPointer(sparePtr, "array:spare");
B.line(`${spare} = load i16, ptr ${sparePtr}${host.fieldAliasAttachment(sparePtr)}`);
B.line(`${bit} = and i16 ${spare}, ${SCR_ARR_STATES}`);
B.line(`${has} = icmp ne i16 ${bit}, 0`);
return has;
}
/** Call-site effects of the non-retaining read fallbacks: they only read
* module-visible memory, or trap without returning to the caller (the trap
* writes runtime-private stderr/TLS state). Declaring this lets LLVM keep
@@ -74,13 +99,21 @@ function emitDenseGuard(
): DenseGuard {
const B = host.B;
const size = host.sizeType;
const integer = exactInteger(host, index, indexExpr);
const integer = indexInteger(host, index, indexExpr);
const nanIndex = integer === null ? integerOrNaNIndex(host, index, indexExpr) : null;
let wide: string;
let integral: string | null = null;
if (integer) {
// A proven integer needs no double round-trip. Negative signed values
// compare above any length or capacity as unsigned 64-bit integers.
wide = widenInteger(host, integer);
if (integer !== null) {
// A proven integer, or a wide view that equals the double at every
// index below a length, needs no double round-trip. Negative signed
// values compare above any length or capacity as unsigned 64-bit
// integers, and every index that fails the comparisons takes the
// double's path.
wide = integer;
} else if (nanIndex) {
// An exact integer or NaN is an index exactly when it is ordered.
wide = nanIndex.wide;
integral = nanIndex.ordered;
} else {
// freeze keeps NaN/out-of-range conversions from producing poison; the
// exact round-trip comparison then rejects every value that is not an
@@ -209,26 +242,52 @@ function statePointer(
return statePtr;
}
/** The dense slot type: boolean arrays store one byte per element, every
* other element kind an 8-byte slot. */
function slotType(acc: ArrayAccess): "i8" | "i64" {
return acc === "bool" ? "i8" : "i64";
/** How an array's dense slots hold its elements: the access kind (`i32`
* for int32 arrays, ir/int32-arrays.ts), or `cref` for references to compact
* class instances (SCR_ELEM_CREF: a 4-byte slot holding the address over 8;
* LlEmitter.compactArrayElem). */
type SlotKind = ArrayAccess | "cref";
function slotKind(host: LlvmEmitterContext, acc: ArrayAccess, element: IrType): SlotKind {
return acc === "ref" && host.compactArrayElem(element) ? "cref" : acc;
}
/** The dense slot type: boolean arrays store one byte per element, compact
* references and int32 arrays four, every other element kind an 8-byte slot. */
function slotType(kind: SlotKind): "i8" | "i32" | "i64" {
return kind === "bool" ? "i8" : kind === "cref" || kind === "i32" ? "i32" : "i64";
}
/** Bytes per dense slot (scr_arr_slot_width). */
export function slotWidth(acc: ArrayAccess): number {
return acc === "bool" ? 1 : 8;
export function slotWidth(kind: SlotKind): number {
return kind === "bool" ? 1 : kind === "cref" || kind === "i32" ? 4 : 8;
}
/** Bytes per dense slot of an array of `element` (scr_arr_slot_width). */
export function denseSlotWidth(host: LlvmEmitterContext, element: IrType): number {
return slotWidth(slotKind(host, elemAccess(element), element));
}
/** The runtime accessor family of an access: int32 arrays answer through
* the double-valued entry points, which convert their slots. */
export function runtimeAccess(acc: ArrayAccess): "f64" | "bool" | "ref" {
return acc === "i32" ? "f64" : acc;
}
/** The LLVM type of an access's values at the emitter's interface. */
function valueType(acc: ArrayAccess): "double" | "i1" | "ptr" {
return acc === "f64" || acc === "i32" ? "double" : acc === "bool" ? "i1" : "ptr";
}
function valuePointer(
host: LlvmEmitterContext,
guard: Pick<DenseGuard, "offset" | "data">,
acc: ArrayAccess,
kind: SlotKind,
): string {
const B = host.B;
const valuePtr = B.tmp();
B.line(
`${valuePtr} = getelementptr inbounds ${slotType(acc)}, ptr ${guard.data}, ${host.sizeType} ${guard.offset}`,
`${valuePtr} = getelementptr inbounds ${slotType(kind)}, ptr ${guard.data}, ${host.sizeType} ${guard.offset}`,
);
host.markMemoryPointer(valuePtr, "array:elements");
return valuePtr;
@@ -241,7 +300,13 @@ function loadState(host: LlvmEmitterContext, guard: DenseGuard): string {
return state;
}
export type ArrayAccess = "f64" | "bool" | "ref";
/** The ScrElemKind of int32 arrays (scr_array.h). */
export const SCR_ELEM_I32 = 7;
/** How a dense array's slots are read and written: doubles, booleans,
* references, or the int32 slots of SCR_ELEM_I32 arrays (ir/int32-arrays.ts),
* whose values are doubles at the emitter's interface. */
export type ArrayAccess = "f64" | "bool" | "ref" | "i32";
/** Before a self-append (`a[i] = a[i] + suffix`) replaces a reference
* element: when `index` lies in the hole-free prefix and the slot still
@@ -297,14 +362,33 @@ export function emitDenseArrayGet(
retain: boolean,
proven = false,
): string {
return emitDenseArrayRead(host, array, index, indexExpr, acc, element, getter, retain, proven)
.value;
}
/** emitDenseArrayGet, with the int32 of an int32 array's element: the
* slot itself on the dense path, the runtime getter's double converted
* (exactly: the element is an int32) on the other. */
export function emitDenseArrayRead(
host: LlvmEmitterContext,
array: string,
index: LlValue,
indexExpr: IrExpr | undefined,
acc: ArrayAccess,
element: IrType,
getter: string,
retain: boolean,
proven = false,
): { value: string; int32: string | null } {
const B = host.B;
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
const ty = valueType(acc);
const ret = acc === "bool" ? "zeroext i1" : ty;
const kind = slotKind(host, acc, element);
host.declare(`declare ${ret} @${getter}(ptr, double)`);
if (acc === "ref" && retain && !isRefCounted(element)) {
const t = B.tmp();
B.line(`${t} = call ${ty} @${getter}(ptr ${array}, double ${index.name})`);
return t;
return { value: t, int32: null };
}
const slow = B.newLabel("arr.get.slow"),
join = B.newLabel("arr.get.join"),
@@ -322,7 +406,7 @@ export function emitDenseArrayGet(
slow,
true,
valueLabel,
slotWidth(acc),
slotWidth(kind),
proven,
induction,
);
@@ -333,12 +417,18 @@ export function emitDenseArrayGet(
B.condBr(present, valueLabel, slow);
}
B.startBlock(valueLabel);
const ptr = valuePointer(host, guard, acc);
const ptr = valuePointer(host, guard, kind);
let fast: string;
let fastInt: string | null = null;
let fastLabel = valueLabel;
if (acc === "f64") {
fast = B.tmp();
B.line(`${fast} = load double, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
} else if (acc === "i32") {
fastInt = B.tmp();
fast = B.tmp();
B.line(`${fastInt} = load i32, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
B.line(`${fast} = sitofp i32 ${fastInt} to double`);
} else if (acc === "bool") {
const raw = B.tmp();
fast = B.tmp();
@@ -347,7 +437,13 @@ export function emitDenseArrayGet(
} else {
const raw = B.tmp(),
nonnull = B.tmp();
B.line(`${raw} = load ptr, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
if (kind === "cref") {
const ref = B.tmp();
B.line(`${ref} = load i32, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
for (const line of compactRefLoadLines(ref, raw, `${raw}.r`)) B.line(line.trimStart());
} else {
B.line(`${raw} = load ptr, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
}
B.line(`${nonnull} = icmp ne ptr ${raw}, null`);
fastLabel = B.newLabel("arr.get.ref");
B.condBr(nonnull, fastLabel, slow);
@@ -359,11 +455,21 @@ export function emitDenseArrayGet(
const slowValue = B.tmp();
const attrs = acc === "ref" && retain ? " cold" : READ_FALLBACK;
B.line(`${slowValue} = call ${ty} @${getter}(ptr ${array}, double ${index.name})${attrs}`);
let slowInt: string | null = null;
if (fastInt !== null) {
slowInt = B.tmp();
B.line(`${slowInt} = fptosi double ${slowValue} to i32`);
}
B.br(join);
B.startBlock(join);
const result = B.tmp();
B.line(`${result} = phi ${ty} [ ${fast}, %${fastLabel} ], [ ${slowValue}, %${slow} ]`);
return result;
let int32: string | null = null;
if (fastInt !== null && slowInt !== null) {
int32 = B.tmp();
B.line(`${int32} = phi i32 [ ${fastInt}, %${fastLabel} ], [ ${slowInt}, %${slow} ]`);
}
return { value: result, int32 };
}
/** `scr_arr_get_number`: a numeric element, or NaN for every missing,
@@ -374,13 +480,14 @@ export function emitDenseArrayNumber(
index: LlValue,
indexExpr: IrExpr | undefined,
proven = false,
acc: "f64" | "i32" = "f64",
): string {
return emitDenseArrayGet(
host,
array,
index,
indexExpr,
"f64",
acc,
{ kind: "f64" },
"scr_arr_get_number",
false,
@@ -435,7 +542,9 @@ export function emitDenseArrayState(
* value's reference. A dense index stores the slot, marks it present, and
* extends the length when it lands at or past it; a replaced present
* reference is released after the new edge is published (a release can
* collect cycles). Every other index takes the unchanged runtime call. */
* collect cycles). Every other index takes the unchanged runtime call.
* `int32`, for an int32 array, is the value as an i32 when the caller has
* it at hand. */
export function emitDenseArraySet(
host: LlvmEmitterContext,
array: string,
@@ -445,10 +554,12 @@ export function emitDenseArraySet(
element: IrType,
value: string,
proven = false,
int32: string | null = null,
): void {
const B = host.B;
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
const setter = `scr_arr_set_${acc}`;
const ty = valueType(acc);
const kind = slotKind(host, acc, element);
const setter = `scr_arr_set_${runtimeAccess(acc)}`;
host.declare(`declare void @${setter}(ptr, double, ${acc === "bool" ? "i1 zeroext" : ty})`);
const call = (attrs: string): void =>
B.line(`call void @${setter}(ptr ${array}, double ${index.name}, ${ty} ${value})${attrs}`);
@@ -456,6 +567,13 @@ export function emitDenseArraySet(
call("");
return;
}
// An int32 array's stores are proven int32s (ir/int32-arrays.ts); both
// dense paths below store the converted value.
let int32Value: string | null = int32;
if (acc === "i32" && int32Value === null) {
int32Value = B.tmp();
B.line(`${int32Value} = fptosi double ${value} to i32`);
}
const slow = B.newLabel("arr.set.slow"),
join = B.newLabel("arr.set.join"),
filledLabel = B.newLabel("arr.set.filled");
@@ -467,33 +585,70 @@ export function emitDenseArraySet(
slow,
false,
filledLabel,
slotWidth(acc),
slotWidth(kind),
proven,
);
const { offset, len } = guard;
const storeValue = (valuePtr: string): void => {
if (acc === "f64") {
B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
} else if (acc === "i32") {
B.line(`store i32 ${int32Value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
} else if (acc === "bool") {
const bits = B.tmp();
B.line(`${bits} = zext i1 ${value} to i8`);
B.line(`store i8 ${bits}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
} else if (kind === "cref") {
const ref = B.tmp();
for (const line of compactRefStoreLines(value, ref, `${ref}.r`)) B.line(line.trimStart());
B.line(`store i32 ${ref}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
} else {
B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
}
};
const stated = B.newLabel("arr.set.states"),
plain = B.newLabel("arr.set.plain"),
plainStore = B.newLabel("arr.set.values"),
stored = B.newLabel("arr.set.stored.slot");
B.condBr(emitArrayHasStates(host, array), stated, plain);
B.startBlock(stated);
const statePtr = statePointer(host, guard);
const valuePtr = valuePointer(host, guard, acc);
let oldState: string | null = null;
let old: string | null = null;
if (acc === "ref") {
oldState = B.tmp();
old = B.tmp();
const valuePtr = valuePointer(host, guard, kind);
let statedOld: string | null = null;
let statedHad: string | null = null;
// Compact instances are never released: a replaced one needs nothing.
if (kind === "ref") {
const oldState = B.tmp();
statedOld = B.tmp();
statedHad = B.tmp();
B.line(`${oldState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
B.line(`${old} = load ptr, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
B.line(`${statedOld} = load ptr, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
B.line(`${statedHad} = icmp eq i8 ${oldState}, ${SCR_ARR_VALUE}`);
}
storeValue(valuePtr);
B.line(`store i8 ${SCR_ARR_VALUE}, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
const statedEnd = B.currentLabel();
B.br(stored);
// Without states the bound is the length, so a store here appends at
// the length (it replaces no value) or leaves holes, which the runtime
// gives states.
B.startBlock(plain);
const appends = B.tmp();
B.line(`${appends} = icmp eq ${host.sizeType} ${offset}, ${len}`);
B.condBr(appends, plainStore, slow);
B.startBlock(plainStore);
storeValue(valuePointer(host, guard, kind));
const plainEnd = B.currentLabel();
B.br(stored);
B.startBlock(stored);
let old: string | null = null;
let hadValue: string | null = null;
if (kind === "ref") {
old = B.tmp();
hadValue = B.tmp();
B.line(`${old} = phi ptr [ ${statedOld}, %${statedEnd} ], [ null, %${plainEnd} ]`);
B.line(`${hadValue} = phi i1 [ ${statedHad}, %${statedEnd} ], [ false, %${plainEnd} ]`);
}
// A store at the bound continues the hole-free prefix.
const filledWide = sizedFilled(host, guard.filled),
continues = B.tmp();
@@ -537,9 +692,7 @@ export function emitDenseArraySet(
storeArrCount(host, nextLen, lenPtr, host.fieldAliasAttachment(lenPtr));
B.br(storedLabel);
B.startBlock(storedLabel);
if (acc === "ref" && oldState && old) {
const hadValue = B.tmp();
B.line(`${hadValue} = icmp eq i8 ${oldState}, ${SCR_ARR_VALUE}`);
if (kind === "ref" && hadValue && old) {
const releaseLabel = B.newLabel("arr.set.release");
B.condBr(hadValue, releaseLabel, join);
B.startBlock(releaseLabel);
@@ -549,9 +702,9 @@ export function emitDenseArraySet(
// Below the bound the slot holds a present value: replace it, leaving
// the state alone, and release a replaced reference after the store.
B.startBlock(filledLabel);
const filledValuePtr = valuePointer(host, guard, acc);
const filledValuePtr = valuePointer(host, guard, kind);
let filledOld: string | null = null;
if (acc === "ref") {
if (kind === "ref") {
filledOld = B.tmp();
B.line(
`${filledOld} = load ptr, ptr ${filledValuePtr}${host.fieldAliasAttachment(filledValuePtr)}`,
@@ -578,10 +731,12 @@ export function emitDenseArrayPop(
host: LlvmEmitterContext,
array: string,
acc: ArrayAccess,
element: IrType,
empty: string,
slow: string,
): string {
const B = host.B;
const kind = slotKind(host, acc, element);
const size = host.sizeType;
const header = (field: number): string => {
const ptr = B.tmp();
@@ -615,19 +770,42 @@ export function emitDenseArrayPop(
states = B.tmp();
B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
loadArrCount(host, cap, capPtr, host.fieldAliasAttachment(capPtr));
scrArrStates(host, states, data, cap, slotWidth(acc));
const valuePtr = valuePointer(host, { offset: last, data }, acc);
scrArrStates(host, states, data, cap, slotWidth(kind));
const valuePtr = valuePointer(host, { offset: last, data }, kind);
const statePtr = statePointer(host, { offset: last, states });
const slotTy = slotType(acc);
const slotTy = slotType(kind);
let raw = B.tmp();
B.line(`${raw} = load ${slotTy}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
if (slotTy === "i8") {
const wide = B.tmp();
B.line(`${wide} = zext i8 ${raw} to i64`);
raw = wide;
} else if (slotTy === "i32") {
if (kind === "cref") {
// The 8-byte slot of a compact reference is its address.
const wide = B.tmp(),
address = B.tmp();
B.line(`${wide} = zext i32 ${raw} to i64`);
B.line(`${address} = shl nuw nsw i64 ${wide}, 3`);
raw = address;
} else {
// The slot bits of an int32 element's double, as the f64 path returns them.
const number = B.tmp(),
bits = B.tmp();
B.line(`${number} = sitofp i32 ${raw} to double`);
B.line(`${bits} = bitcast double ${number} to i64`);
raw = bits;
}
}
B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
// Only an array with states has a state byte to clear.
const clear = B.newLabel("arr.pop.state"),
cleared = B.newLabel("arr.pop.cleared");
B.condBr(emitArrayHasStates(host, array), clear, cleared);
B.startBlock(clear);
B.line(`store i8 0, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
B.br(cleared);
B.startBlock(cleared);
let last32 = last;
if (size === "i64") {
last32 = B.tmp();
@@ -648,12 +826,14 @@ export function emitDenseArrayPush(
host: LlvmEmitterContext,
array: string,
acc: ArrayAccess,
element: IrType | null,
value: string,
): string {
const B = host.B;
const size = host.sizeType;
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
const pusher = `scr_arr_push_${acc}`;
const kind = element === null ? acc : slotKind(host, acc, element);
const ty = valueType(acc);
const pusher = `scr_arr_push_${runtimeAccess(acc)}`;
host.declare(`declare double @${pusher}(ptr, ${acc === "bool" ? "i1 zeroext" : ty})`);
const header = (field: number): string => {
const ptr = B.tmp();
@@ -685,20 +865,35 @@ export function emitDenseArrayPush(
B.line(`${spare} = sub nuw ${size} ${cap}, ${len}`);
B.line(`${near} = getelementptr inbounds i8, ptr ${data}, ${size} ${spare}`);
B.line(
`${statePtr} = getelementptr inbounds [${slotWidth(acc)} x i8], ptr ${near}, ${size} ${cap}, ${size} -1`,
`${statePtr} = getelementptr inbounds [${slotWidth(kind)} x i8], ptr ${near}, ${size} ${cap}, ${size} -1`,
);
host.markMemoryPointer(statePtr, "array:present");
const valuePtr = valuePointer(host, { offset: len, data }, acc);
const valuePtr = valuePointer(host, { offset: len, data }, kind);
if (acc === "f64") {
B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
} else if (acc === "i32") {
const int32 = B.tmp();
B.line(`${int32} = fptosi double ${value} to i32`);
B.line(`store i32 ${int32}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
} else if (acc === "bool") {
const bits = B.tmp();
B.line(`${bits} = zext i1 ${value} to i8`);
B.line(`store i8 ${bits}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
} else if (kind === "cref") {
const ref = B.tmp();
for (const line of compactRefStoreLines(value, ref, `${ref}.r`)) B.line(line.trimStart());
B.line(`store i32 ${ref}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
} else {
B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
}
// Only an array with states has a state byte to mark.
const mark = B.newLabel("arr.push.state"),
marked = B.newLabel("arr.push.marked");
B.condBr(emitArrayHasStates(host, array), mark, marked);
B.startBlock(mark);
B.line(`store i8 ${SCR_ARR_VALUE}, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
B.br(marked);
B.startBlock(marked);
const next = B.tmp(),
fastValue = B.tmp();
B.line(`${next} = add nuw ${size} ${len}, 1`);
@@ -716,6 +911,7 @@ export function emitDenseArrayPush(
B.line(`${bumped} = select i1 ${packed}, i32 ${nextFilled}, i32 ${filled}`);
B.line(`store i32 ${bumped}, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
B.line(`${fastValue} = ${sizeToDouble(size)} ${size} ${next} to double`);
const fastEnd = B.currentLabel();
B.br(join);
B.startBlock(slow);
const slowValue = B.tmp();
@@ -723,6 +919,6 @@ export function emitDenseArrayPush(
B.br(join);
B.startBlock(join);
const result = B.tmp();
B.line(`${result} = phi double [ ${fastValue}, %${fast} ], [ ${slowValue}, %${slow} ]`);
B.line(`${result} = phi double [ ${fastValue}, %${fastEnd} ], [ ${slowValue}, %${slow} ]`);
return result;
}
@@ -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(),
+420 -1
View File
@@ -1,6 +1,6 @@
import { typedRefConstructor } from "./shapes.js";
import { bindLiveArrayOps } from "./expr-stream-bridges.js";
import { BYTES_ELEM_NUM, SCR_ARR_FIELD, f64Lit, sizeToDouble } from "./common.js";
import { BYTES_ELEM_NUM, SCR_ARR_FIELD, SCR_ARR_STATES, f64Lit, sizeToDouble } from "./common.js";
import { emitUnionPeek, emitUnionTag } from "./union-repr.js";
import {
INLINE_UNION_FIELD,
@@ -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;
@@ -185,6 +196,55 @@ export class LlDyn {
constructor(private readonly host: DynHost) {}
/** Journal replay (emit-journal.ts): builds or finds the helper a chunk
* emitter recorded, and answers its name. */
replayHelper(op: string, args: readonly unknown[]): string {
switch (op) {
case "dyn.collectionDynOps":
return this.collectionDynOps(args[0] as IrType & { kind: "map" | "set" });
case "dyn.dynMatchHelper":
return this.dynMatchHelper(args[0] as IrType, args[1] as boolean);
case "dyn.jsonSchema":
return this.jsonSchema(args[0] as IrType);
case "dyn.recordNameTable": {
const shapeId = args[0] as string;
return this.recordNameTable(shapeId, this.host.recordsById.get(shapeId)!).names;
}
case "dyn.jsonParseHelper":
return this.jsonParseHelper(args[0] as IrType)!;
case "dyn.dynCheckHelper":
return this.dynCheckHelper(args[0] as IrType, args[1] as boolean);
case "dyn.toDynHelper":
return this.toDynHelper(args[0] as IrType);
case "dyn.promiseDynAdapterHelper":
return this.promiseDynAdapterHelper(args[0] as IrType);
case "dyn.dynFuncThunkHelper":
return this.dynFuncThunkHelper(args[0] as IrType & { kind: "func" });
case "dyn.dynFuncBoxHelper":
return this.dynFuncBoxHelper(args[0] as IrType & { kind: "func" });
case "dyn.dynFuncAdapterHelper":
return this.dynFuncAdapterHelper(args[0] as IrType & { kind: "func" });
case "dyn.dynToStrHelper":
return this.dynToStrHelper();
case "dyn.dynIterNHelper":
return this.dynIterNHelper();
case "dyn.dynDestrCheckHelper":
return this.dynDestrCheckHelper();
case "dyn.dynKeyGetHelper":
return this.dynKeyGetHelper();
case "dyn.dynComputedKeyGetHelper":
return this.dynComputedKeyGetHelper();
case "dyn.caughtToDynHelper":
return this.caughtToDynHelper();
case "dyn.messageListenerHelper":
return this.messageListenerHelper(args[0] as IrType & { kind: "func" }) ?? "";
case "dyn.messageReaderHelper":
return this.messageReaderHelper(args[0] as IrType);
default:
throw new InternalCompilerError(`llvm emitter bug: no dyn helper replay ${op}`);
}
}
/** The types a dyn matcher or builder for `t` checks structurally: record
* fields and index values, array elements and union arms. */
private structuralChildren(t: IrType): readonly IrType[] {
@@ -374,6 +434,7 @@ export class LlDyn {
if (prior) return prior;
const name = `sc_map_dyn_${this.mapDynOps.size}`;
this.mapDynOps.set(key, name);
this.host.journalAllocated?.(name, "dyn.collectionDynOps", [t]);
const keyType = t.kind === "map" ? t.key : t.elem;
const valueType: IrType = t.kind === "map" ? t.value : { kind: "f64" };
const emitSlot = (type: IrType, role: string): void => {
@@ -431,6 +492,7 @@ export class LlDyn {
`@${name} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr } { ptr ${this.host.cstr(key)}, ptr @${name}_key_box, ptr @${name}_key_unbox, ptr @${this.dynMatchHelper(keyType)}, ptr @${name}_value_box, ptr @${name}_value_unbox }`,
"",
);
this.host.journalBuilt?.();
return name;
}
@@ -829,6 +891,7 @@ export class LlDyn {
if (existing) return existing;
const name = `sc_dm_${this.dynMatchers.size}`;
this.dynMatchers.set(matcherKey, name);
this.host.journalAllocated?.(name, "dyn.dynMatchHelper", [t, preserveRefs]);
const recursive = this.isRecursiveShape(t);
const wrapped = recursive || this.holdsRecursiveShape(t);
const B = new BlockBuilder();
@@ -1189,6 +1252,7 @@ export class LlDyn {
``,
);
if (wrapped) this.emitVisitWrapper(name, t, recursive ? "match" : "visit");
this.host.journalBuilt?.();
return name;
}
@@ -1282,6 +1346,7 @@ export class LlDyn {
const S = host.sizeType;
const name = `sc_jsch_${this.jsonSchemas.size}`;
this.jsonSchemas.set(key, name);
this.host.journalAllocated?.(name, "dyn.jsonSchema", [t]);
let body: string;
switch (t.kind) {
case "f64":
@@ -1353,6 +1418,7 @@ export class LlDyn {
`@${name} = internal constant { ${S}, ${S}, ptr, ptr, ptr, ptr, ptr, ptr, ${S} } { ${body} }`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -1442,6 +1508,7 @@ export class LlDyn {
const base = `sc_rnames_${this.recordNameTables.size}`;
const table = { names: `@${base}`, lens: `@${base}_lens` };
this.recordNameTables.set(shapeId, table);
this.host.journalAllocated?.(base, "dyn.recordNameTable", [shapeId]);
const n = shape.fields.length;
const names = recordSourceFieldOrder(shape);
this.defs.push(
@@ -1449,6 +1516,7 @@ export class LlDyn {
`@${base}_lens = internal constant [${n} x ${S}] [${names.map((name) => `${S} ${Buffer.byteLength(name, "utf8")}`).join(", ")}]`,
``,
);
this.host.journalBuilt?.();
return table;
}
@@ -1467,6 +1535,7 @@ export class LlDyn {
const host = this.host;
const name = `sc_jp_${this.jsonParsers.size}`;
this.jsonParsers.set(key, name);
this.host.journalAllocated?.(name, "dyn.jsonParseHelper", [t]);
const schema = this.jsonSchema(t);
const check = this.dynCheckHelper(t);
host.declare(`declare ptr @scr_json_parse_schema(ptr, ptr)`);
@@ -1493,6 +1562,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -1508,6 +1578,7 @@ export class LlDyn {
if (existing) return existing;
const name = `sc_dc_${this.dynBuilders.size}`;
this.dynBuilders.set(builderKey, name);
this.host.journalAllocated?.(name, "dyn.dynCheckHelper", [t, preserveRefs]);
const recursive = this.isRecursiveShape(t);
const memo = !recursive && this.holdsRecursiveShape(t) && this.valTy(t) === "ptr";
const host = this.host;
@@ -1530,6 +1601,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
if (isRefCounted(t) && t.kind !== "dyn") {
@@ -1708,6 +1780,32 @@ export class LlDyn {
B.line(`store ${member.type} ${oldValue}, ptr ${checkedPtr}`);
});
if (t.kind !== "record") {
// Whether the dense slots carry states (SCR_ARR_STATES in
// `spare`) moves with them; the rest of `spare` stays.
const cachedSpare = B.tmp(),
checkedSpare = B.tmp();
const sparePtr = (array: string): string => {
const ptr = B.tmp();
B.line(
`${ptr} = getelementptr inbounds %ScrArr, ptr ${array}, i64 0, i32 ${SCR_ARR_FIELD.spare}`,
);
return ptr;
};
const cachedSparePtr = sparePtr(cached),
checkedSparePtr = sparePtr(checked);
B.line(`${cachedSpare} = load i16, ptr ${cachedSparePtr}`);
B.line(`${checkedSpare} = load i16, ptr ${checkedSparePtr}`);
const exchange = (own: string, other: string, ptr: string): void => {
const kept = B.tmp(),
moved = B.tmp(),
next = B.tmp();
B.line(`${kept} = and i16 ${own}, ${~SCR_ARR_STATES}`);
B.line(`${moved} = and i16 ${other}, ${SCR_ARR_STATES}`);
B.line(`${next} = or i16 ${kept}, ${moved}`);
B.line(`store i16 ${next}, ptr ${ptr}`);
};
exchange(cachedSpare, checkedSpare, cachedSparePtr);
exchange(checkedSpare, cachedSpare, checkedSparePtr);
// Sparse slots and numeric properties live out of line; each
// array keeps its own match metadata.
host.declare("declare void @scr_arr_swap_indexed_ext(ptr, ptr)");
@@ -2526,6 +2624,7 @@ export class LlDyn {
``,
);
if (recursive || memo) this.emitVisitWrapper(name, t, recursive ? "build" : "memo");
this.host.journalBuilt?.();
return name;
}
@@ -2540,6 +2639,7 @@ export class LlDyn {
if (existing) return existing;
const name = `sc_td_${this.toDynFns.size}`;
this.toDynFns.set(key, name);
this.host.journalAllocated?.(name, "dyn.toDynHelper", [t]);
const host = this.host;
const B = new BlockBuilder();
let sourceAccessor: { name: string; release: string } | null = null;
@@ -3146,6 +3246,7 @@ export class LlDyn {
``,
);
}
this.host.journalBuilt?.();
return name;
}
@@ -3162,6 +3263,7 @@ export class LlDyn {
if (existing) return existing;
const name = `sc_pda_${this.promiseDynAdapters.size}`;
this.promiseDynAdapters.set(key, name);
this.host.journalAllocated?.(name, "dyn.promiseDynAdapterHelper", [inner]);
const host = this.host;
const B = new BlockBuilder();
host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`);
@@ -3239,6 +3341,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -3251,6 +3354,7 @@ export class LlDyn {
if (this.dynToStrFn) return this.dynToStrFn;
const name = "sc_ds";
this.dynToStrFn = name;
this.host.journalAllocated?.(name, "dyn.dynToStrHelper", []);
const host = this.host;
host.declare(`declare void @scr_jb_init(ptr)`);
host.declare(`declare ptr @scr_jb_finish(ptr)`);
@@ -3555,6 +3659,7 @@ export class LlDyn {
``,
);
}
this.host.journalBuilt?.();
return name;
}
@@ -3566,6 +3671,7 @@ export class LlDyn {
if (this.caughtToDynFn) return this.caughtToDynFn;
const name = "sc_cd";
this.caughtToDynFn = name;
this.host.journalAllocated?.(name, "dyn.caughtToDynHelper", []);
const host = this.host;
const B = new BlockBuilder();
const kp = B.tmp();
@@ -3671,6 +3777,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -3684,6 +3791,7 @@ export class LlDyn {
this.helperMemo.set(name, name);
const B = new BlockBuilder();
const host = this.host;
this.host.journalAllocated?.(name, "dyn.dynComputedKeyGetHelper", []);
const stringHelper = this.dynKeyGetHelper();
host.declare(`declare ptr @scr_dyn_property_key_value(ptr)`);
host.declare(`declare ptr @scr_dyn_symbol_key_get(ptr, ptr, i1 zeroext)`);
@@ -3721,6 +3829,7 @@ export class LlDyn {
"}",
"",
);
this.host.journalBuilt?.();
return name;
}
@@ -3730,6 +3839,7 @@ export class LlDyn {
if (existing) return existing;
const name = "sc_dyn_key_get";
this.dynBuilders.set(memoKey, name);
this.host.journalAllocated?.(name, "dyn.dynKeyGetHelper", []);
const host = this.host;
const B = new BlockBuilder();
const kd = this.kindOf(B, "%d");
@@ -4123,6 +4233,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -4134,6 +4245,7 @@ export class LlDyn {
if (existing) return existing;
const name = "sc_dyn_destr_check";
this.dynBuilders.set(memoKey, name);
this.host.journalAllocated?.(name, "dyn.dynDestrCheckHelper", []);
const host = this.host;
host.declare(`declare void @scr_jb_init(ptr)`);
host.declare(`declare ptr @scr_jb_finish(ptr)`);
@@ -4188,6 +4300,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -4199,6 +4312,7 @@ export class LlDyn {
if (existing) return existing;
const name = "sc_dyn_iter_n";
this.dynBuilders.set(memoKey, name);
this.host.journalAllocated?.(name, "dyn.dynIterNHelper", []);
const host = this.host;
host.declare(`declare ptr @scr_dyn_new_arr()`);
host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
@@ -4403,6 +4517,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -4434,6 +4549,7 @@ export class LlDyn {
if (existing) return existing;
const name = `sc_dfk_${this.dynFuncThunks.size}`;
this.dynFuncThunks.set(key, name);
this.host.journalAllocated?.(name, "dyn.dynFuncThunkHelper", [t]);
const host = this.host;
const B = new BlockBuilder();
const argNames: string[] = [];
@@ -4583,6 +4699,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -4593,6 +4710,7 @@ export class LlDyn {
if (existing) return existing;
const name = `sc_dfb_${this.dynFuncBoxes.size}`;
this.dynFuncBoxes.set(key, name);
this.host.journalAllocated?.(name, "dyn.dynFuncBoxHelper", [t]);
const host = this.host;
const thunk = this.dynFuncThunkHelper(t);
host.declare(`declare ptr @scr_closure_retain_v(ptr)`);
@@ -4607,6 +4725,305 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
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) {
// A signature without an entry still takes a number.
this.messageListeners.set(key, null);
this.host.journalAllocated?.("", "dyn.messageListenerHelper", [t]);
this.host.journalBuilt?.();
return null;
}
const name = `sc_dml_${this.messageListeners.size}`;
this.messageListeners.set(key, name);
this.host.journalAllocated?.(name, "dyn.messageListenerHelper", [t]);
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(),
`}`,
``,
);
this.host.journalBuilt?.();
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);
this.host.journalAllocated?.(name, "dyn.messageReaderHelper", [t]);
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(),
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -4618,6 +5035,7 @@ export class LlDyn {
if (existing) return existing;
const name = `sc_dfa_${this.dynFuncAdapters.size}`;
this.dynFuncAdapters.set(key, name);
this.host.journalAllocated?.(name, "dyn.dynFuncAdapterHelper", [t]);
const host = this.host;
const B = new BlockBuilder();
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
@@ -4720,6 +5138,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
}
@@ -16,9 +16,7 @@ import {
withInitializerBindings,
withInitializerLocals,
} from "../../ir/initializer-bindings.js";
import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js";
import { constructorClass } from "../../ir/final-fields.js";
import { findInitializedByteLoopBindings } from "./byte-windows.js";
import { findLoopArrayBorrows } from "./loop-array-borrows.js";
import { findCallArrayReads, findLocalArrayReads } from "./local-array-reads.js";
import { findScalarStringSlices, findScratchStringSlices } from "./string-slices.js";
@@ -35,6 +33,7 @@ import {
mangleArgPack,
mangleAsyncAdoptCall,
mangleAsyncAwaitCall,
mangleAsyncDirectSpawn,
mangleAsyncSpawn,
mangleBorrowedFunction,
mangleClassStruct,
@@ -54,11 +53,15 @@ import { INT32_ABI_SUFFIX, SCALAR_UNION_RESULT } from "./int32-abi.js";
import { f64Lit } from "./common.js";
import { RESULT_ENTRY_PREFIX } from "./record-results.js";
import { emitIntegerClone, emitIntegerDispatcher } from "./integer-clones.js";
import { beginFunctionLoops, LOOPS } from "./loop-state.js";
import { runningExtremumUpdates } from "./running-extrema.js";
import { findIntegerOrNaNLocals } from "./wrapping-sums.js";
export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
const B = new BlockBuilder();
emitter.B = B;
emitter.currentFunctionName = fn.name;
emitter.findInt32Allocations(fn);
emitter.currentTier = fn.tier;
// Fiber switches restore the active exception cell before control
// returns to a synchronous frame (emitPendingCheck).
@@ -80,8 +83,11 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
emitter.currentLocals.set(id, plain);
emitter.lazyCaptureBoxes.set(id, "");
}
emitter.runningExtrema = emitter.debug === null ? runningExtremumUpdates(fn) : new Set();
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;
@@ -96,7 +102,6 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
const initializerBindings = emitter.initializerBindings.get(fn.name) ?? [];
const numericFn = withInitializerBindings(fn, initializerBindings);
emitter.numericLocals = new Map(numericFn.locals.map((l) => [l.id, l]));
emitter.unaliasedArrays = findUnaliasedLocalArrays(fn);
emitter.borrowedParameters.clear();
emitter.referenceWrites?.enter(fn);
emitter.reboundOwnerSlots.clear();
@@ -127,32 +132,23 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
emitter.stableCaptureIds =
fn.captures !== undefined ? emitter.lazyCaptures.stableCaptures(fn.name) : new Set();
emitter.integerLoopBindings.clear();
emitter.countedLoopsEnabled = emitter.debug === null && !fn.async && !fn.generator;
emitter.speculativeLoopsEnabled = emitter.countedLoopsEnabled;
emitter.integerTemps.clear();
emitter.integerTempsEnabled = emitter.debug === null && !fn.async && !fn.generator;
beginFunctionLoops(emitter, fn, numericFn);
emitter.speculativeLoopsEnabled = LOOPS.of(emitter).countedLoopsEnabled;
emitter.speculativeFunction = fn.name;
emitter.speculation = null;
emitter.speculationResume = null;
emitter.int32Kinds = null;
emitter.int32KindResume = null;
emitter.finalFieldBorrows =
!fn.async &&
!fn.generator &&
constructorClass(fn.name) === null &&
emitter.finalFields.size > 0;
emitter.byteWindowsEnabled = emitter.countedLoopsEnabled;
emitter.bytesLoopVersionsEnabled = emitter.countedLoopsEnabled;
emitter.bytesLoopEntries = null;
emitter.bytesLoopFunction = fn;
emitter.denseLoopsEnabled = emitter.countedLoopsEnabled;
emitter.denseLoop = null;
emitter.denseLoopEntries =
emitter.denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === "array")
? findInitializedByteLoopBindings(fn)
: new Map();
emitter.byteWindowEntries =
emitter.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes")
? findInitializedByteLoopBindings(fn)
: new Map();
emitter.integerViews.clear();
emitter.fieldPointerTags.clear();
emitter.compressedFieldPtrs.clear();
emitter.integerArrayBindings.clear();
emitter.loopArrayBorrows =
emitter.debug === null
@@ -225,7 +221,9 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
emitter.integerRanges = rangeFacts.ranges;
emitter.integerLoopRanges = rangeFacts.loopRanges;
emitter.int32Elements = slotFacts.elements ?? null;
emitter.integerOrNaNLocals = findIntegerOrNaNLocals(emitter, numericFn);
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
@@ -406,7 +404,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
if (local.boxed || emitter.captureIds.has(local.id)) slots.add(`%${mangleLocal(local.id)}`);
B.volatileSlots = slots;
// Without optimization a versioned copy only adds code.
emitter.bytesLoopVersionsEnabled = false;
LOOPS.of(emitter).bytesLoopVersionsEnabled = false;
}
// Captured bindings come in through the environment — borrowed for the
// whole call (the closure owns them): bound here, never released here.
@@ -910,6 +908,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)`);
@@ -1117,8 +1129,11 @@ export function emitAsyncAwaitCall(
``,
];
}
emitter.declare(`declare void @scr_async_inline_enter(ptr)`);
emitter.declare(`declare void @scr_async_inline_exit(ptr)`);
const framed = emitter.fiberlessFrames;
if (framed) {
emitter.declare(`declare void @scr_async_inline_enter(ptr)`);
emitter.declare(`declare void @scr_async_inline_exit(ptr)`);
}
emitter.declare(`declare void @scr_await_hop()`);
emitter.declare(`declare ptr @scr_promise_new()`);
emitter.declare(`declare void @scr_promise_reject_pending(ptr)`);
@@ -1127,11 +1142,12 @@ export function emitAsyncAwaitCall(
const out = [
head,
`entry:`,
` %frame = alloca { ptr, ptr }`,
` call void @scr_async_inline_enter(ptr %frame)`,
...(framed
? [` %frame = alloca { ptr, ptr }`, ` call void @scr_async_inline_enter(ptr %frame)`]
: []),
retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`,
...emitter.pendingTestLines("%pend"),
` call void @scr_async_inline_exit(ptr %frame)`,
...(framed ? [` call void @scr_async_inline_exit(ptr %frame)`] : []),
// Adopting: the then job's turn comes before the settled promise's
// own reaction, whichever way the body completed.
...(adopt ? [` call void @scr_await_hop()`] : []),
@@ -0,0 +1,237 @@
/* Chunked function emission (emit-module.ts). An emitter that emits one
* contiguous range of a module's functions records here the helpers and
* sites it numbers from a counter (`sc_td_${size}` and the like). The
* emitter that merges the ranges replays each range's records in order:
* replay calls the same entry point with the same arguments, so every
* helper gets the number and the definition the serial emission gives it,
* and the range's text is renamed to those numbers. */
import type { LlEmitter } from "./emitter.js";
/** One event of a range's emission, in order. */
export type JournalEntry =
/** A helper or site the range's emitter numbered: the name it gave it
* (a counter prefix and a number), and the replay entry point
* (registerReplay) with its arguments, which are plain IR values. */
| {
readonly kind: "op";
readonly name: string;
readonly op: string;
readonly args: readonly unknown[];
}
/** The first request of a key of a module-level table made outside any
* replayable definition, with the value the range's emitter stored. */
| {
readonly kind: "entry";
readonly table: string;
readonly key: string;
readonly value: unknown;
};
export class EmitJournal {
readonly entries: JournalEntry[] = [];
/** The replayable definitions being built, innermost last. */
private readonly open: string[] = [];
/** The definitions still open (empty between functions). */
get unclosed(): readonly string[] {
return this.open;
}
/** Keys already recorded per table. */
private readonly recorded = new Map<string, Set<string>>();
/** A keyed helper, recorded when its number is taken, before anything its
* definition creates in turn: that keeps the serial numbering order.
* Replaying a helper that exists answers its name, so helpers created
* inside another helper's definition are recorded too. Opens the
* definition, which the creating entry point closes with `built`. */
allocated(name: string, op: string, args: readonly unknown[]): void {
this.entries.push({ kind: "op", name, op, args });
this.open.push(op);
}
/** Opens a definition whose helper is not replayed itself (its
* entry point records the request with `used`). */
opened(op: string): void {
this.open.push(op);
}
/** A request, made outside any definition, of the helper `name` that an
* entry point answered; recorded once per name. Replay makes the same
* request. */
used(name: string, op: string, args: readonly unknown[]): void {
if (this.open.length !== 0) return;
if (this.usedNames.has(name)) return;
this.usedNames.add(name);
this.entries.push({ kind: "op", name, op, args });
}
private readonly usedNames = new Set<string>();
/** Closes the definition `allocated` or `opened` opened. */
built(): void {
if (this.open.pop() === undefined) this.overclosed = true;
}
/** A `built` closed a definition no `allocated` opened. */
overclosed = false;
/** An unkeyed site (one per use). Sites taken while a definition is built
* are taken again when the merging emitter replays that definition. */
site(name: string, op: string, args: readonly unknown[] = []): void {
if (this.open.length === 0) this.entries.push({ kind: "op", name, op, args });
}
/** A request of `key` in a module-level table. Only the first request
* made outside any definition is recorded: requests made while a
* definition is built happen again when it is replayed. */
entry(table: string, key: string, value: unknown): void {
if (this.open.length !== 0) return;
let keys = this.recorded.get(table);
if (keys === undefined) this.recorded.set(table, (keys = new Set()));
if (keys.has(key)) return;
keys.add(key);
this.entries.push({ kind: "entry", table, key, value });
}
}
/** A range emitter's result (LlEmitter.chunkResult): plain data, so a
* worker process can write it. */
export interface ChunkResult {
readonly entries: readonly JournalEntry[];
readonly flags: {
readonly usesCoroutines: boolean;
readonly usesTimers: boolean;
readonly usesGenSlots: boolean;
readonly needsUnionNone: boolean;
readonly needsNullableAbsent: boolean;
readonly needsOom: boolean;
readonly needsBadTag: boolean;
readonly needsBadKey: boolean;
readonly needsRetainBox: boolean;
};
readonly publishedNamesSym: string | null;
readonly rcHelpers: readonly string[];
readonly immortalValues: readonly string[];
readonly fnValues: readonly string[];
readonly arraySearchKinds: readonly string[];
readonly recordCloneShapes: readonly string[];
readonly awaitCallees: readonly string[];
readonly adoptCallees: readonly string[];
readonly directAwaitCallees: readonly string[];
readonly inlineAwaitCallees: readonly string[];
}
type Replay = (emitter: LlEmitter, args: readonly unknown[]) => string;
const replays = new Map<string, Replay>();
/** Registers the entry point that creates (or finds) a numbered helper or
* site from its recorded arguments and answers its name. */
export function registerReplay(op: string, replay: Replay): void {
replays.set(op, replay);
}
/** Replays a range's entries on the merging emitter, in record order, and
* records the renames from the range's names to its own in `names`. */
export function replayJournal(
emitter: LlEmitter,
entries: readonly JournalEntry[],
names: Map<string, string>,
tags: Map<number, number>,
): void {
for (const entry of entries) {
if (entry.kind === "entry") {
emitter.applyJournalEntry(entry.table, entry.key, entry.value, names, tags);
continue;
}
const replay = replays.get(entry.op);
if (replay === undefined)
throw new Error(`llvm emitter bug: no replay for journal entry ${entry.op}`);
const own = stripSigil(replay(emitter, entry.args));
const local = stripSigil(entry.name);
if (own !== local) names.set(local, own);
}
}
/** The rename of a range's text: numbered names (`names`, without the
* sigil) and field alias tags (`tags`). One scan for each kind of name:
* a numbered name starts `sc_` after a character that cannot be part of a
* name, and ends at the last digit after its counter prefix. */
export function renameChunk(
names: ReadonlyMap<string, string>,
tags: ReadonlyMap<number, number>,
): (text: string) => string {
const prefixes = new Set<string>();
for (const name of names.keys()) prefixes.add(name.replace(/\d+$/, ""));
if (prefixes.size === 0 && tags.size === 0) return (text) => text;
return (text) => {
let out = prefixes.size === 0 ? text : renameNames(text, names, prefixes);
if (tags.size > 0) out = renameTags(out, tags);
return out;
};
}
function renameNames(
text: string,
names: ReadonlyMap<string, string>,
prefixes: ReadonlySet<string>,
): string {
const parts: string[] = [];
let copied = 0;
for (let at = text.indexOf("sc_"); at >= 0; at = text.indexOf("sc_", at + 3)) {
if (at > 0 && isNameChar(text.charCodeAt(at - 1))) continue;
// The counter prefix ends where the digits start; the number ends at
// the last digit.
let end = at + 3;
while (end < text.length && isNameChar(text.charCodeAt(end)) && !isDigit(text.charCodeAt(end)))
end++;
if (end === text.length || !isDigit(text.charCodeAt(end))) continue;
if (!prefixes.has(text.slice(at, end))) continue;
let last = end;
while (last < text.length && isDigit(text.charCodeAt(last))) last++;
const own = names.get(text.slice(at, last));
if (own === undefined) continue;
parts.push(text.slice(copied, at), own);
copied = last;
}
if (copied === 0) return text;
parts.push(text.slice(copied));
return parts.join("");
}
function renameTags(text: string, tags: ReadonlyMap<number, number>): string {
const marker = "!tbaa !";
const parts: string[] = [];
let copied = 0;
for (let at = text.indexOf(marker); at >= 0; at = text.indexOf(marker, at + marker.length)) {
const start = at + marker.length;
let end = start;
while (end < text.length && isDigit(text.charCodeAt(end))) end++;
const own = tags.get(Number(text.slice(start, end)));
if (own === undefined) continue;
parts.push(text.slice(copied, start), String(own));
copied = end;
}
if (copied === 0) return text;
parts.push(text.slice(copied));
return parts.join("");
}
/** `[-$._A-Za-z0-9]`: a character of an LLVM identifier. */
function isNameChar(code: number): boolean {
return (
(code >= 48 && code <= 57) ||
(code >= 65 && code <= 90) ||
(code >= 97 && code <= 122) ||
code === 95 ||
code === 36 ||
code === 45 ||
code === 46
);
}
function isDigit(code: number): boolean {
return code >= 48 && code <= 57;
}
function stripSigil(name: string): string {
return name.startsWith("@") ? name.slice(1) : name;
}
+244 -49
View File
@@ -2,6 +2,10 @@
import { relieveHeapPressure } from "../../heap-pressure.js";
import { emitWasmModules } from "./wasm-modules.js";
import { type LlEmitter, ffiCallbackDummyLl, llStrBytes } from "./emitter.js";
import { type ChunkResult, EmitJournal } from "./emit-journal.js";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { JS_IC_WORDS } from "./js-ic.js";
import {
mangleAsyncSpawn,
@@ -24,7 +28,7 @@ import {
} from "../../ir/ir.js";
import {
FN_ATTRS,
limitInlineDynReleases,
limitInlineReleases,
releaseSym,
typedRefConstructor,
vAdapters,
@@ -35,9 +39,11 @@ import { emitClassMembershipHelper } from "./classes.js";
import { type RegexMatcherCode, compileRegexMatcher } from "./regex-matchers.js";
import {
MAP_ITER_DECL,
MAP_SIZE_DECL,
MAP_ITER_NUM_DECL,
SCR_MAP_NUM_DEAD,
NUMBER_MAP_ENTRY_DECL,
STRING_MAP_ENTRY_C_DECL,
STRING_MAP_ENTRY_DECL,
} from "./map-number-lookup.js";
import { RUNTIME_ABI_MARKER } from "../runtime-abi.js";
@@ -59,16 +65,150 @@ import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js";
import { emitLibraryIdentityLines } from "../library-identity-markers.js";
import { InternalCompilerError } from "../../errors.js";
/** The function bodies emitted in `chunks` contiguous ranges, each by its
* own emitter (LlEmitter.chunkEmitter), then merged in function order: the
* text one emitter gives. */
function emitFunctionsInChunks(emitter: LlEmitter, chunks: number): string[] {
const functions = emitter.mod.functions;
const size = Math.ceil(functions.length / chunks);
const ranges: { chunk: LlEmitter; texts: string[] }[] = [];
for (let start = 0; start < functions.length; start += size) {
const chunk = emitter.chunkEmitter();
const texts = functions.slice(start, start + size).map((fn) => {
const text = chunk.emitFunction(fn);
if (chunk.journal!.overclosed)
throw new Error(`llvm emitter bug: journal definition closed twice in ${fn.name}`);
if (chunk.journal!.unclosed.length > 0)
throw new Error(
`llvm emitter bug: unclosed journal definitions after ${fn.name}: ${chunk.journal!.unclosed.join(", ")}`,
);
return text;
});
ranges.push({ chunk, texts });
}
const fnDefs: string[] = [];
for (const { chunk, texts } of ranges) {
// The plain-data result a worker process would write, round-tripped.
const result = JSON.parse(JSON.stringify(chunk.chunkResult())) as ChunkResult;
const rename = emitter.mergeChunk(result);
for (const text of texts) {
const def = rename(text);
emitter.fnDefText.set(functions[fnDefs.length]!.name, def);
fnDefs.push(def);
}
}
return fnDefs;
}
/** Modules with fewer functions emit in one process: forking and merging
* would cost more than the workers save (SCRIPTC_EMIT_WORKER_FUNCTIONS
* lowers it, for tests). */
function workerMinFunctions(): number {
const value = Number(process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] ?? "2000");
return Number.isFinite(value) && value > 0 ? value : 2000;
}
/** The function bodies emitted on forked worker processes, `workers` at a
* time: the function list splits into contiguous ranges, each emitted in
* its own process, which inherits this emitter as it is at the fork and
* records a journal (emit-journal.ts). Ranges merge in function order, so
* the text is the serial text. A range whose worker fails is emitted here
* when its turn comes. Null without fork primitives (Node, Windows, inside
* a worker) or when the module is small. */
function emitFunctionsOnWorkers(emitter: LlEmitter, workers: number): string[] | null {
const fork = emitter.targetOptions.forkPrimitives;
const functions = emitter.mod.functions;
if (fork === undefined || workers < 2) return null;
if (functions.length < workerMinFunctions()) return null;
// More ranges than workers: a range's merge overlaps the later ranges'
// emission, and a slow range holds back less.
const count = Math.min(functions.length, workers * 3);
const size = Math.ceil(functions.length / count);
const dir = mkdtempSync(join(tmpdir(), "scriptc-emit-"));
const pids: number[] = [];
const start = (range: number): void => {
const pid = fork.fork();
if (pid === 0) {
let code = 1;
try {
emitter.journal = new EmitJournal();
const texts: string[] = [];
for (const fn of functions.slice(range * size, (range + 1) * size)) {
texts.push(emitter.emitFunction(fn));
relieveHeapPressure();
}
writeFileSync(join(dir, `${range}.json`), JSON.stringify(emitter.chunkResult()));
// Function text never holds NUL, so it separates the functions.
writeFileSync(join(dir, `${range}.ll`), texts.join("\0"));
code = 0;
} catch {
// The parent emits the range itself.
}
fork.exit(code);
}
pids.push(pid);
};
const fnDefs: string[] = [];
try {
for (let range = 0; range < Math.min(workers, count); range++) start(range);
for (let range = 0; range < count; range++) {
const pid = pids[range]!;
const status = pid > 0 ? fork.wait(pid) : -1;
// The next worker starts from this emitter as merged so far, which
// the journal allows: it records requests, not table contents.
const merged = status === 0 ? readRange(dir, range) : null;
const slice = functions.slice(range * size, (range + 1) * size);
if (merged !== null && merged.texts.length === slice.length) {
const rename = emitter.mergeChunk(merged.result);
slice.forEach((fn, index) => {
const def = rename(merged.texts[index]!);
emitter.fnDefText.set(fn.name, def);
fnDefs.push(def);
});
} else
for (const fn of slice) {
const def = emitter.emitFunction(fn);
emitter.fnDefText.set(fn.name, def);
fnDefs.push(def);
relieveHeapPressure();
}
if (pids.length < count) start(pids.length);
}
} finally {
rmSync(dir, { recursive: true, force: true });
}
return fnDefs;
}
/** A worker's range result, or null when its files are missing. */
function readRange(dir: string, range: number): { result: ChunkResult; texts: string[] } | null {
try {
const result = JSON.parse(readFileSync(join(dir, `${range}.json`), "utf8")) as ChunkResult;
const texts = readFileSync(join(dir, `${range}.ll`), "utf8").split("\0");
return { result, texts };
} catch {
return null;
}
}
export function emitModuleParts(emitter: LlEmitter): string[] {
// Function bodies first (the literal/unit/fn-value tables fill as they
// emit), then the file assembles around them — the runtime ABI’s order.
const fnDefs: string[] = [];
for (const fn of emitter.mod.functions) {
const def = emitter.emitFunction(fn);
fnDefs.push(def);
emitter.fnDefText.set(fn.name, def);
relieveHeapPressure();
}
// Debug metadata is numbered per emitter and not journaled, so a debug
// build emits every body here.
const ranges = emitter.debug === null;
const chunks = emitter.targetOptions.functionChunks ?? 1;
const fnDefs =
(ranges && chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ??
(ranges ? emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) : null) ??
[];
if (fnDefs.length === 0)
for (const fn of emitter.mod.functions) {
const def = emitter.emitFunction(fn);
fnDefs.push(def);
emitter.fnDefText.set(fn.name, def);
relieveHeapPressure();
}
emitter.selectStacklessAsync(fnDefs);
const errorViews: string[] = [];
for (const property of ["name", "message"])
@@ -456,15 +596,18 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
// RC fast paths test the traced byte (shapes.ts).
...(emitter.rcHelpers === null ? [] : [`%ScrMapRc = type { ${emitter.sizeType}, i8, i8, i8 }`]),
// The complete ScrMap and its entries, read by the inline number-key
// lookup (sc_map_entry_f64) and the inline iteration steps
// (sc_map_iter_entry, sc_map_iter_entry_num): field 8 is the i32 entry
// lookup (sc_map_entry_f64), the inline iteration steps
// (sc_map_iter_entry, sc_map_iter_entry_num) and Map.size
// (sc_map_size): field 8 is the i32 entry count, field 9 the i32 live
// count, field 10 the i64 absent-key filter, entries is field 11,
// nbuckets and buckets fields 13 and 14, the direct index and its i32
// length fields 15 and 16.
...(emitter.decls.has(NUMBER_MAP_ENTRY_DECL) ||
emitter.decls.has(STRING_MAP_ENTRY_DECL) ||
emitter.decls.has(STRING_MAP_ENTRY_C_DECL) ||
emitter.decls.has(MAP_ITER_DECL) ||
emitter.decls.has(MAP_ITER_NUM_DECL)
emitter.decls.has(MAP_ITER_NUM_DECL) ||
emitter.decls.has(MAP_SIZE_DECL)
? [
`%ScrMapIx = type { ${emitter.sizeType}, i8, i8, i8, i8, i32, ptr, ptr, i32, i32, i64, ptr, i32, i32, ptr, ptr, i32, i32 }`,
]
@@ -479,6 +622,11 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
emitter.decls.has(MAP_ITER_NUM_DECL)
? [`%ScrMapEntry = type { i64, i64, i64 }`, `%ScrMapNumEntry = type { i64, i64 }`]
: []),
// A compact-value map's { key, hash, val } entry (scr_map.h ScrMapCEntry):
// a 32-bit hash and the value as an address over 8.
...(emitter.decls.has(STRING_MAP_ENTRY_C_DECL)
? [`%ScrMapCEntry = type { i64, i32, i32 }`]
: []),
// The runtime error prefix { rc, vt, name, message, code, cause } and the
// class-object shape { rc, pre, post, ctor, name } — field reads on
// builtin errors and classval loads GEP through these.
@@ -545,6 +693,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
``,
`@scr_error_vts = external ${tl}global [${RUNTIME_ERROR_CLASSES.size} x %ScrVt]`,
`declare void @scr_init()`,
...(emitter.compactRegion ? [`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()`] : []),
@@ -698,21 +847,19 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
for (const code of regexMatchers.values()) out.push(...code.lines, ``);
for (const [, inst] of emitter.templateStringsInstances) {
// One immortal ScrArr per tagged-template site: a data global of
// interned cooked-string literals followed by their states (the
// runtime finds them before data + cap * 9), and the ScrArr header over
// it (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2, no REF
// entry points). Every dense slot is present; reads retain immortal
// strings — a no-op. Slots are 8 bytes on every target: on 32-bit
// targets each holds the pointer in its low half and zero in its high
// half, so the states start where the runtime looks for them.
// interned cooked-string literals, and the ScrArr header over it
// (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2, no REF entry
// points, no state bytes: every dense slot is present). Reads retain
// immortal strings — a no-op. Slots are 8 bytes on every target: on
// 32-bit targets each holds the pointer in its low half and zero in its
// high half.
const n = inst.slots.length;
const present = n === 0 ? "zeroinitializer" : `[ ${inst.slots.map(() => "i8 1").join(", ")} ]`;
const wide = emitter.sizeType === "i64";
const slot = wide ? "ptr" : "<{ ptr, i32 }>";
const slotValue = (s: string): string =>
wide ? `ptr ${s}` : `<{ ptr, i32 }> <{ ptr ${s}, i32 0 }>`;
out.push(
`@${inst.sym}_data = internal constant <{ [${n} x ${slot}], [${n} x i8] }> <{ [${n} x ${slot}] [ ${inst.slots.map(slotValue).join(", ")} ], [${n} x i8] ${present} }>, align 8`,
`@${inst.sym}_data = internal constant [${n} x ${slot}] [ ${inst.slots.map(slotValue).join(", ")} ], align 8`,
`@${inst.sym} = internal global %ScrArr { ${emitter.sizeType} -1, i32 ${n}, i32 ${n}, i8 2, i8 0, i16 0, i32 ${n}, ptr @${inst.sym}_data }`,
);
}
@@ -837,6 +984,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.compactRegion ? [` 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()`] : []),
@@ -1289,6 +1439,18 @@ export function helperDefs(emitter: LlEmitter): string[] {
``,
);
}
// Map.size and Set.size read the live entry count (scr_map_size).
if (emitter.decls.has(MAP_SIZE_DECL))
defs.push(
`define internal double @sc_map_size(ptr %m) ${FN_ATTRS} {`,
`entry:`,
` %np = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 9`,
` %n = load i32, ptr %np`,
` %nf = uitofp i32 %n to double`,
` ret double %nf`,
`}`,
``,
);
// String-key Map/Set lookups test the map's absent-key filter inline
// (scr_map.c scr_map_filter_bits): a key whose two filter bits are not
// both set is absent, so most misses on small and medium maps return
@@ -1297,36 +1459,69 @@ export function helperDefs(emitter: LlEmitter): string[] {
// and the filter reads its bits 44..55, which are the capacity word's
// own. A key without a cached hash, and every possible hit, take the
// runtime lookup.
if (emitter.decls.has(STRING_MAP_ENTRY_DECL)) {
// A compact-value map's lookup (sc_map_entry_str_c) tests the same filter
// and calls scr_map_entry_str_c; its iteration step (sc_map_iter_entry_c)
// reads 16-byte entries and their 32-bit hash.
const stringMapEntry = (helper: string, runtime: string): string[] => [
`define internal ptr @${helper}(ptr %m, ptr %k) ${FN_ATTRS} {`,
`entry:`,
...(emitter.sizeType === "i64"
? [
` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`,
` %filter = load i64, ptr %fp`,
` %capp = getelementptr inbounds %ScrStr, ptr %k, i32 0, i32 2`,
` %cap = load i64, ptr %capp`,
` %cached = icmp uge i64 %cap, 4294967296`,
` br i1 %cached, label %test, label %slow`,
`test:`,
` %s1 = lshr i64 %cap, 44`,
` %i1 = and i64 %s1, 63`,
` %b1 = shl i64 1, %i1`,
` %s2 = lshr i64 %cap, 50`,
` %i2 = and i64 %s2, 63`,
` %b2 = shl i64 1, %i2`,
` %bits = or i64 %b1, %b2`,
` %have = and i64 %filter, %bits`,
` %maybe = icmp eq i64 %have, %bits`,
` br i1 %maybe, label %slow, label %absent`,
`absent:`,
` ret ptr null`,
]
: [` br label %slow`]),
`slow:`,
` %r = call ptr @${runtime}(ptr %m, ptr %k)`,
` ret ptr %r`,
`}`,
``,
];
if (emitter.decls.has(STRING_MAP_ENTRY_DECL))
defs.push(...stringMapEntry("sc_map_entry_str", "scr_map_entry_str"));
if (emitter.decls.has(STRING_MAP_ENTRY_C_DECL)) {
const sz = emitter.sizeType;
defs.push(
`define internal ptr @sc_map_entry_str(ptr %m, ptr %k) ${FN_ATTRS} {`,
...stringMapEntry("sc_map_entry_str_c", "scr_map_entry_str_c"),
`define internal ptr @sc_map_iter_entry_c(ptr %m, double %i) ${FN_ATTRS} {`,
`entry:`,
...(emitter.sizeType === "i64"
? [
` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`,
` %filter = load i64, ptr %fp`,
` %capp = getelementptr inbounds %ScrStr, ptr %k, i32 0, i32 2`,
` %cap = load i64, ptr %capp`,
` %cached = icmp uge i64 %cap, 4294967296`,
` br i1 %cached, label %test, label %slow`,
`test:`,
` %s1 = lshr i64 %cap, 44`,
` %i1 = and i64 %s1, 63`,
` %b1 = shl i64 1, %i1`,
` %s2 = lshr i64 %cap, 50`,
` %i2 = and i64 %s2, 63`,
` %b2 = shl i64 1, %i2`,
` %bits = or i64 %b1, %b2`,
` %have = and i64 %filter, %bits`,
` %maybe = icmp eq i64 %have, %bits`,
` br i1 %maybe, label %slow, label %absent`,
`absent:`,
` ret ptr null`,
]
: [` br label %slow`]),
`slow:`,
` %r = call ptr @scr_map_entry_str(ptr %m, ptr %k)`,
` ret ptr %r`,
` %np = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`,
` %n = load i32, ptr %np`,
` %nf = uitofp i32 %n to double`,
` %lo = fcmp oge double %i, 0.0`,
` %hi = fcmp olt double %i, %nf`,
` %in = and i1 %lo, %hi`,
` br i1 %in, label %load, label %none`,
`load:`,
` %ix = fptoui double %i to ${sz}`,
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
` %entries = load ptr, ptr %ep`,
` %e = getelementptr inbounds %ScrMapCEntry, ptr %entries, ${sz} %ix`,
` %hp = getelementptr inbounds %ScrMapCEntry, ptr %e, i32 0, i32 1`,
` %h = load i32, ptr %hp`,
` %live = icmp ne i32 %h, 0`,
` br i1 %live, label %yes, label %none`,
`yes:`,
` ret ptr %e`,
`none:`,
` ret ptr null`,
`}`,
``,
);
@@ -2274,7 +2469,7 @@ export function emitFfiCallbackDefs(emitter: LlEmitter): { globals: string[]; de
export function emitParts(emitter: LlEmitter): string[] {
const parts = [
...limitInlineDynReleases(emitter.emitModuleParts()),
...limitInlineReleases(emitter.emitModuleParts(), emitter.shapeHost.rcMode),
...emitter.disposeObserverGates(),
];
if (!emitter.executableTls) return parts;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,24 @@
/* Per-feature emitter state. A feature defines its state in its own file
* with defineModuleState and reads it through the returned key, so adding
* or extending a feature does not edit LlEmitter's field list or
* constructor. */
import type { LlEmitter } from "./emitter.js";
/** A feature's state on an emitter, read with `key.of(emitter)`. */
export interface EmitterStateKey<T> {
of(emitter: LlEmitter): T;
}
let moduleSlots = 0;
/** State that lives for the whole module, created on first read. */
export function defineModuleState<T>(create: (emitter: LlEmitter) => T): EmitterStateKey<T> {
const slot = moduleSlots++;
return {
of(emitter) {
const states = emitter.moduleStates;
if (slot >= states.length || states[slot] === undefined) states[slot] = create(emitter);
return states[slot] as T;
},
};
}
@@ -13,6 +13,7 @@ import {
type IrType,
} from "../../ir/ir.js";
import { emitLlvmModule, emitLlvmModuleSource, LlEmitter } from "./emitter.js";
import { INLINE_STR_RELEASES_PER_FUNCTION } from "./shapes.js";
/** A pending-exception check: the inline active-cell test or a runtime call. */
const PENDING_CHECK = /@scr_exc_(?:active|pending)\b/;
@@ -201,17 +202,24 @@ test("detached exit defines the runtime opt-in symbol only when requested", () =
expect(detached.replace("@scr_detached_exit = constant i8 1\n", "")).toBe(plain);
});
test("inline RC inlines every fast path at speed and keeps string and box releases as calls in release", () => {
test("inline RC inlines every fast path at speed and keeps box releases as calls in release", () => {
for (const boxed of [false, true]) {
const module = moduleFor(Array.from({ length: 4 }, call), { boxed });
const release = emitLlvmModule(module);
expect(release).not.toContain("@sc_rc_");
expect(release).not.toContain("%ScrMapRc");
// The release subset inlines string retains and dyn retains and releases
// only; these string and box releases stay runtime calls.
// The release subset inlines string and dyn retains and releases and Map
// retains only: box releases stay runtime calls, and so do the string
// releases of a function with more than INLINE_STR_RELEASES_PER_FUNCTION.
const subset = emitLlvmModule(module, { inlineRc: "release" });
expect(subset).not.toContain("@sc_rc_release_str");
expect(subset).not.toContain("@sc_rc_release_box");
expect(subset).not.toContain("call void @sc_rc_release_box(");
const sites = (ir: string, fn: string): number => ir.split(`call void @${fn}(ptr`).length - 1;
if (!boxed) {
expect(sites(subset, "sc_rc_release_str")).toBe(0);
// Every site calls the runtime, and so does the unused helper's slow path.
expect(sites(subset, "scr_str_release")).toBe(sites(release, "scr_str_release") + 1);
expect(sites(release, "scr_str_release")).toBeGreaterThan(INLINE_STR_RELEASES_PER_FUNCTION);
}
const speed = emitLlvmModule(module, { inlineRc: "speed" });
const helper = boxed ? "sc_rc_release_box" : "sc_rc_release_str";
const runtime = boxed ? "scr_box_release" : "scr_str_release";
File diff suppressed because it is too large Load Diff
@@ -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 (
+159 -51
View File
@@ -7,7 +7,7 @@ import { F64_INF, exactIndexLines, f64Lit, sizeToDouble } from "./common.js";
import { exactInteger, widenInteger } from "./integer-values.js";
import { byteNumberAccess } from "../../ir/byte-numbers.js";
import { emitByteNumber } from "./byte-numbers.js";
import { integerOrNaNRange } from "./wrapping-sums.js";
import { integerOrNaNIndex, integerOrNaNRange } from "./wrapping-sums.js";
import { bytesElementAlias, bytesHeaderAlias } from "./bytes-alias.js";
const BYTES_NUM_KIND: Record<string, { kind: number; le: boolean } | undefined> = {
@@ -119,12 +119,28 @@ export function emitBytesIndex(
B.line(`${narrow} = trunc i64 ${wideIndex} to ${host.sizeType}`);
return narrow;
}
const exact = exactIndexLines(index, len, host.sizeType, () => B.tmp());
for (const line of exact.lines) B.line(line);
let ok: string;
let wide: string;
const nanIndex = integerOrNaNIndex(host, value, expr);
if (nanIndex) {
// An exact integer or NaN: in range when ordered and below the length.
const wideLength = host.sizeType === "i64" ? len : B.tmp();
if (host.sizeType !== "i64") B.line(`${wideLength} = zext ${host.sizeType} ${len} to i64`);
const below = B.tmp();
ok = B.tmp();
B.line(`${below} = icmp ult i64 ${nanIndex.wide}, ${wideLength}`);
B.line(`${ok} = and i1 ${nanIndex.ordered}, ${below}`);
wide = nanIndex.wide;
} else {
const exact = exactIndexLines(index, len, host.sizeType, () => B.tmp());
for (const line of exact.lines) B.line(line);
ok = exact.ok;
wide = exact.wide;
}
const invalid = B.newLabel("bytes.index.invalid");
const valid = B.newLabel("bytes.index.valid");
B.condBr(exact.ok, valid, invalid);
let idx = exact.wide;
B.condBr(ok, valid, invalid);
let idx = wide;
B.startBlock(invalid);
if (skipInvalid) B.br(skipInvalid);
@@ -138,7 +154,7 @@ export function emitBytesIndex(
B.startBlock(valid);
if (host.sizeType !== "i64") {
idx = B.tmp();
B.line(`${idx} = trunc i64 ${exact.wide} to ${host.sizeType}`);
B.line(`${idx} = trunc i64 ${wide} to ${host.sizeType}`);
}
return idx;
}
@@ -176,22 +192,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 +233,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 +286,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 +304,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 +315,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 +351,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(
@@ -399,11 +471,13 @@ export function emitToUint32(
B.line(`${out} = select i1 ${ordered}, i32 ${low}, i32 0`);
return out;
}
// Below 2^63 in magnitude, the i64 conversion truncates exactly (every
// double above 2^53 is an integer), and its low 32 bits are ToUint32.
const aboveMin = B.tmp();
const belowMax = B.tmp();
const fast = B.tmp();
B.line(`${aboveMin} = fcmp oge double ${value}, ${f64Lit(-9007199254740992)}`);
B.line(`${belowMax} = fcmp ole double ${value}, ${f64Lit(9007199254740992)}`);
B.line(`${aboveMin} = fcmp oge double ${value}, ${f64Lit(-9223372036854775808)}`);
B.line(`${belowMax} = fcmp olt double ${value}, ${f64Lit(9223372036854775808)}`);
B.line(`${fast} = and i1 ${aboveMin}, ${belowMax}`);
const fastLabel = B.newLabel("uint32.coerce.fast");
@@ -419,8 +493,8 @@ export function emitToUint32(
B.br(done);
B.startBlock(slowLabel);
// Only NaN, the infinities and magnitudes above 2^53 reach this block.
// Such a finite double is the integer mantissa * 2^shift with shift >= 1,
// Only NaN, the infinities and magnitudes from 2^63 reach this block.
// Such a finite double is the integer mantissa * 2^shift with shift >= 11,
// so its low 32 bits are the shifted mantissa's (none once shift >= 64),
// negated for a negative value. NaN and the infinities have the maximum
// exponent, a shift of 972, and convert to 0 like ToUint32. Integer
@@ -471,13 +545,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 +563,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 +766,7 @@ export function emitBytesIntrinsic(
e.args[0],
host.bytesBounds.has(e),
e.invalidNaN === true,
host.bytesOwned.has(e),
);
case "slice":
return call(
@@ -1012,7 +1118,9 @@ export function emitInlineAtomicsOp(
if (op !== 0 && op !== 1 && op !== 3 && ATOMIC_RMW[op] === undefined) return null;
const elem = array.type.elem;
const B = host.B;
const receiver = host.emitExpr(array).name;
// The instruction and scr_atomics_op borrow the array: an unchanged local
// owns it across the later operands and the operation.
const receiver = emitBorrowedInput(host, array).name;
const idxValue = host.emitExpr(index);
const operand = host.emitExpr(value);
const swap = host.emitExpr(replacement);
@@ -14,6 +14,7 @@ import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext } from "./expr-context.js";
import { unionPeekLines, unionTagLines } from "./union-repr.js";
import { inlineUnionCopyLines } from "./inline-unions.js";
import { registerReplay } from "./emit-journal.js";
/** Text-template union construction around an owned (+1) reference payload:
* a fresh box, or the payload itself for a nullable union. */
@@ -55,6 +56,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
if (sym) return sym;
sym = `sc_race_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "raceAdapterFor", [from, to]);
const toArms = to.kind === "union" ? (host.unionsById.get(to.unionId)?.arms ?? []) : [];
if (from.kind === "void" && !toArms.some((a) => a.kind === "undefinedT")) {
// A Promise<never> entry (the frontend admits void entries without an
@@ -70,6 +72,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
`}`,
``,
);
host.journal?.built();
return sym;
}
if (to.kind !== "union")
@@ -98,6 +101,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
const tag = tagOf(UNDEFINED_T);
d.push(` ${fulfill(host.unitInstanceRef(to.unionId, tag))}`, ` ret void`, `}`, ``);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
if (from.kind !== "union") {
@@ -138,6 +142,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
}
d.push(` ${fulfill("%u")}`, ` ret void`, `}`, ``);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
// Sub-union re-tag: switch over the entry's arms, rebuild under the
@@ -200,6 +205,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
``,
);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
@@ -213,6 +219,7 @@ export function genResultThunkFor(
if (sym) return sym;
sym = mangleGenResThunk(host.resolveThunks.size);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "genResultThunkFor", [genT, recT]);
const shape = host.recordsById.get(recT.shapeId);
const base = host.nullableUnions.fieldBase(recT.shapeId);
const valueT = shape?.fields.find((f) => f.name === "value")?.type;
@@ -299,6 +306,7 @@ export function genResultThunkFor(
``,
];
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
if (valueT.kind !== "union") throw new LlvmUnsupportedError(`genResume:${valueT.kind}`);
@@ -456,6 +464,7 @@ export function genResultThunkFor(
``,
);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
@@ -467,6 +476,7 @@ export function childExitThunkFor(host: LlvmEmitterContext, param: IrType): stri
if (sym) return sym;
sym = `sc_cx_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "childExitThunkFor", [param]);
const def = host.unionsById.get(param.unionId);
const f64Tag = def ? def.arms.findIndex((a) => a.kind === "f64") : -1;
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
@@ -494,6 +504,7 @@ export function childExitThunkFor(host: LlvmEmitterContext, param: IrType): stri
`}`,
``,
);
host.journal?.built();
return sym;
}
@@ -510,6 +521,7 @@ export function childExitSignalThunkFor(
if (sym) return sym;
sym = `sc_cx_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "childExitSignalThunkFor", [codeParam, sigParam]);
const codeDef = host.unionsById.get(codeParam.unionId);
const f64Tag = codeDef ? codeDef.arms.findIndex((a) => a.kind === "f64") : -1;
const codeNullTag = codeDef ? codeDef.arms.findIndex((a) => a.kind === "nullT") : -1;
@@ -560,6 +572,7 @@ export function childExitSignalThunkFor(
`}`,
``,
);
host.journal?.built();
return sym;
}
@@ -569,6 +582,7 @@ export function execFileThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
if (sym) return sym;
sym = `sc_exec_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "execFileThunkFor", [cbT]);
const errorParam = cbT.params[0];
let errorTag = -1;
let nullTag = -1;
@@ -627,6 +641,7 @@ export function execFileThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
``,
);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
@@ -639,6 +654,7 @@ export function ipcMessageThunkFor(
if (sym) return sym;
sym = `sc_ipc_msg_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "ipcMessageThunkFor", [cbT]);
const param = cbT.params[0];
const d: string[] = [
`define internal void @${sym}(ptr %cb, ptr %message) ${FN_ATTRS} { ; IPC message ${typeKey(cbT)}`,
@@ -679,6 +695,7 @@ export function ipcMessageThunkFor(
}
d.push(` ret void`, `}`, ``);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
@@ -688,6 +705,7 @@ export function ipcSendThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
if (sym) return sym;
sym = `sc_ipc_send_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "ipcSendThunkFor", [cbT]);
const param = cbT.params[0];
const d: string[] = [
`define internal void @${sym}(ptr %cb, ptr %error) ${FN_ATTRS} { ; IPC send callback ${typeKey(cbT)}`,
@@ -736,6 +754,7 @@ export function ipcSendThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
}
d.push(` ret void`, `}`, ``);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
@@ -747,6 +766,7 @@ export function childDataThunkFor(host: LlvmEmitterContext, param: IrType): stri
if (sym) return sym;
sym = `sc_cd_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "childDataThunkFor", [param]);
const def = host.unionsById.get(param.unionId);
const bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes" && a.elem === "u8") : -1;
if (bytesTag < 0)
@@ -768,6 +788,7 @@ export function childDataThunkFor(host: LlvmEmitterContext, param: IrType): stri
`}`,
``,
);
host.journal?.built();
return sym;
}
@@ -780,6 +801,7 @@ export function childDataDynThunkFor(host: LlvmEmitterContext, chunk: "bytes" |
if (sym) return sym;
sym = `sc_cdd_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "childDataDynThunkFor", [chunk]);
const box = chunk === "bytes" ? "scr_dyn_new_buffer" : "scr_dyn_new_str";
host.declare(`declare ptr @${box}(ptr)`);
host.resolveThunkDefs.push(
@@ -793,6 +815,7 @@ export function childDataDynThunkFor(host: LlvmEmitterContext, chunk: "bytes" |
`}`,
``,
);
host.journal?.built();
return sym;
}
@@ -813,6 +836,7 @@ export function emitterFixedAdapter(
if (sym) return { fn: sym, shim };
sym = `sc_ee_ad_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "emitterFixedAdapter", [cbT]);
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
host.declare(`declare void @scr_closure_release(ptr)`);
const params = cursor ? "ptr %args" : cbT.params.map((_, i) => `ptr %a${i}`).join(", ");
@@ -857,6 +881,7 @@ export function emitterFixedAdapter(
}
d.push(` call void @scr_closure_release(ptr %orig)`, ` ret void`, `}`, ``);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return { fn: sym, shim };
}
@@ -882,6 +907,7 @@ export function emitterOnceAdapter(
if (sym) return { fn: sym, shim };
sym = `sc_ee_once_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "emitterOnceAdapter", [boxerT, fire]);
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
host.declare(`declare void @scr_closure_release(ptr)`);
host.declare(`declare void @${fire}(ptr, ptr)`);
@@ -921,6 +947,7 @@ export function emitterOnceAdapter(
``,
);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return { fn: sym, shim };
}
@@ -990,6 +1017,7 @@ export function closeBindThunkFor(
if (sym) return sym;
sym = `sc_ncb_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "closeBindThunkFor", [cbUnion, retServer]);
const def = host.unionsById.get(cbUnion.unionId);
const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1;
const funcArm = funcTag >= 0 ? (def!.arms[funcTag] as IrType & { kind: "func" }) : null;
@@ -1079,6 +1107,7 @@ export function closeBindThunkFor(
}
d.push(`}`, ``);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
@@ -1094,6 +1123,7 @@ export function closeOverrideWrapFor(
if (sym) return sym;
sym = `sc_ncw_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "closeOverrideWrapFor", [cbUnion, retServer]);
const def = host.unionsById.get(cbUnion.unionId);
const undefTag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1;
if (undefTag < 0)
@@ -1122,6 +1152,7 @@ export function closeOverrideWrapFor(
`}`,
``,
);
host.journal?.built();
return sym;
}
@@ -1140,6 +1171,7 @@ export function settledStoreFor(
if (sym) return sym;
sym = `sc_settled_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "settledStoreFor", [from, to]);
const def = host.unionsById.get(to.unionId);
if (!def) throw new InternalCompilerError("llvm emitter bug: settled store to an unknown union");
const armOf = (field: string): { tag: number; arm: IrType & { kind: "record" } } => {
@@ -1230,5 +1262,52 @@ export function settledStoreFor(
`}`,
``,
);
host.journal?.built();
return sym;
}
registerReplay("raceAdapterFor", (host, args) =>
raceAdapterFor(host, args[0] as IrType, args[1] as IrType),
);
registerReplay("genResultThunkFor", (host, args) =>
genResultThunkFor(
host,
args[0] as IrType & { kind: "generator" },
args[1] as IrType & { kind: "record" },
),
);
registerReplay("childExitThunkFor", (host, args) => childExitThunkFor(host, args[0] as IrType));
registerReplay("childExitSignalThunkFor", (host, args) =>
childExitSignalThunkFor(host, args[0] as IrType, args[1] as IrType),
);
registerReplay("execFileThunkFor", (host, args) =>
execFileThunkFor(host, args[0] as IrType & { kind: "func" }),
);
registerReplay("ipcMessageThunkFor", (host, args) =>
ipcMessageThunkFor(host, args[0] as IrType & { kind: "func" }),
);
registerReplay("ipcSendThunkFor", (host, args) =>
ipcSendThunkFor(host, args[0] as IrType & { kind: "func" }),
);
registerReplay("childDataThunkFor", (host, args) => childDataThunkFor(host, args[0] as IrType));
registerReplay("childDataDynThunkFor", (host, args) =>
childDataDynThunkFor(host, args[0] as "bytes" | "str"),
);
registerReplay("closeBindThunkFor", (host, args) =>
closeBindThunkFor(host, args[0] as IrType, args[1] as boolean),
);
registerReplay("closeOverrideWrapFor", (host, args) =>
closeOverrideWrapFor(host, args[0] as IrType, args[1] as boolean),
);
registerReplay("settledStoreFor", (host, args) =>
settledStoreFor(host, args[0] as IrType, args[1] as IrType & { kind: "union" }),
);
registerReplay(
"emitterFixedAdapter",
(host, args) => emitterFixedAdapter(host, args[0] as IrType & { kind: "func" }).fn,
);
registerReplay(
"emitterOnceAdapter",
(host, args) =>
emitterOnceAdapter(host, args[0] as IrType & { kind: "func" }, args[1] as string).fn,
);
@@ -11,6 +11,7 @@ import {
type IrExpr,
type IrFunction,
classValueConstructor,
type IrType,
} from "../../ir/ir.js";
import { liveRefView } from "./expr-stream-bridges.js";
import { typedRefConstructor, vAdapters } from "./shapes.js";
@@ -29,8 +30,8 @@ import {
mangleClassStruct,
mangleFnClosure,
mangleFunction,
mangleWrapper,
mangleVtStruct,
mangleAsyncDirectSpawn,
} from "../mangle.js";
import {
classEnvironmentIndex,
@@ -55,6 +56,7 @@ import {
import type { Writes } from "./reference-writes.js";
import { emitJsIcCall, jsIcSiteOf } from "./js-ic.js";
import { emitClassMembership, emitDowncastGuard } from "./narrow-guards.js";
import { registerReplay } from "./emit-journal.js";
export function emitCallExpr(
host: LlvmEmitterContext,
@@ -74,7 +76,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 +186,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
@@ -807,22 +817,7 @@ export function emitCallExpr(
const pushReceiver = (): void => {
if (lazyReceiver) {
const t = lazyReceiver.type;
const key = typeKey(t);
let recipe = host.typedRefRecipes.get(key);
if (recipe === undefined) {
const adapter = host.liveDynRefAdapter(t);
const rc = vAdapters(host.shapeHost, t);
const ctor = typedRefConstructor(host.shapeHost, t);
const kind = ctor.endsWith("_traced") ? 2 : ctor.endsWith("_observed") ? 1 : 0;
// The box binds the same live view eager boxing attaches, so
// `in`, for-in and Object.keys see the class members.
const view = liveRefView(adapter);
recipe = `@sc_trr_${host.typedRefRecipes.size}`;
host.typedRefRecipes.set(key, recipe);
host.resolveThunkDefs.push(
`${recipe} = internal constant { ptr, ptr, ptr, ${host.sizeType}, ptr, ptr, i32, i32, ptr } { ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit}, i32 ${kind}, i32 ${view?.code ?? 0}, ptr ${view?.ops ?? "null"} }`,
);
}
const recipe = typedRefRecipe(host, t);
host.declare("declare void @scr_dyn_this_push_typed(ptr, ptr)");
B.line(`call void @scr_dyn_this_push_typed(ptr ${lazyReceiver.name}, ptr ${recipe})`);
return;
@@ -830,21 +825,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
@@ -921,9 +904,24 @@ export function emitCallExpr(
? `@${mangleBorrowedFunction(known!.name)}`
: `@${mangleBorrowingWrapper(known!.name)}`
: undefined;
// A known target outside the may-throw set leaves nothing pending, so
// only the entry through the closure's own pointer needs the test.
// Worker programs keep it on every path: there it also observes
// termination, which loops count on (callTestsPending).
const quiet =
known !== undefined && !host.mayThrow.has(known.name) && host.mod.workers !== true;
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 {
const fnp = B.tmp(),
fn = B.tmp();
@@ -983,17 +981,21 @@ export function emitCallExpr(
B.line(
`${t} = phi ${host.llType(e.type)} [ ${specialized}, %${fastEnd} ], [ ${indirect}, %${fallbackEnd} ]`,
);
if (quiet && pending) {
pendingGuard = B.tmp();
B.line(`${pendingGuard} = phi i1 [ false, %${fastEnd} ], [ true, %${fallbackEnd} ]`);
}
}
}
if (e.type.kind === "void") {
if (host.indirectMayThrow) host.emitPendingCheck();
if (pending) host.emitPendingCheck(undefined, false, pendingGuard);
if (lent) host.releaseTemps(host.frames.pop()!);
return { name: "", type: e.type };
}
// The check runs AFTER the result temp joins the frame: an unwind
// releases it (the dummy is NULL for refcounted returns).
const out = host.own({ name: t, type: e.type });
if (host.indirectMayThrow) host.emitPendingCheck();
if (pending) host.emitPendingCheck(undefined, false, pendingGuard);
if (lent) {
host.toFrame(out);
host.releaseTemps(host.frames.pop()!);
@@ -1478,3 +1480,28 @@ function emitNumericIndexRead(
B.line(`${result} = phi ptr [ ${fast}, %${fastLabel} ], [ ${read.name}, %${slowLabel} ]`);
return host.own({ name: result, type: e.type });
}
/** The recipe a lazily boxed receiver of type `t` is built from: its RC
* adapters, type key and live view, one per type. */
function typedRefRecipe(host: LlvmEmitterContext, t: IrType): string {
const key = typeKey(t);
let recipe = host.typedRefRecipes.get(key);
if (recipe !== undefined) return recipe;
const adapter = host.liveDynRefAdapter(t);
const rc = vAdapters(host.shapeHost, t);
const ctor = typedRefConstructor(host.shapeHost, t);
const kind = ctor.endsWith("_traced") ? 2 : ctor.endsWith("_observed") ? 1 : 0;
// The box binds the same live view eager boxing attaches, so `in`,
// for-in and Object.keys see the class members.
const view = liveRefView(adapter);
recipe = `@sc_trr_${host.typedRefRecipes.size}`;
host.typedRefRecipes.set(key, recipe);
host.journal?.allocated(recipe, "typedRefRecipe", [t]);
host.resolveThunkDefs.push(
`${recipe} = internal constant { ptr, ptr, ptr, ${host.sizeType}, ptr, ptr, i32, i32, ptr } { ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit}, i32 ${kind}, i32 ${view?.code ?? 0}, ptr ${view?.ops ?? "null"} }`,
);
host.journal?.built();
return recipe;
}
registerReplay("typedRefRecipe", (host, args) => typedRefRecipe(host, args[0] as IrType));
@@ -2,7 +2,7 @@
import { arraySearchHelperName, arraySearchKind } from "./array-search.js";
import { InternalCompilerError } from "../../errors.js";
import { isStableReceiverOperand, undefinedArmTag } from "../../ir/analysis.js";
import { type IrExpr, type IrType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js";
import { F64, type IrExpr, type IrType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js";
import { mangleResolveThunk } from "../mangle.js";
import {
arrOps,
@@ -31,8 +31,10 @@ import {
emitMapIterCount,
emitMapIterEntry,
emitMapIterSlot,
compactMapValues,
mapEntryLayout,
emitMapIterStart,
emitMapSize,
emitNumberMapGetRef,
emitNumberMapGetScalar,
emitNumberMapHas,
@@ -49,6 +51,8 @@ import {
STRING_CHAR_AT_INT_MARKER,
STRING_CHAR_CODE_AT_DECL,
STRING_CHAR_CODE_AT_INT_MARKER,
emitAsciiChar,
emitAsciiCharCode,
STRING_UTF16_LEN_DECL,
} from "./string-reads.js";
import { exactInteger, widenInteger } from "./integer-values.js";
@@ -56,8 +60,12 @@ import {
emitDenseArrayNumber,
emitDenseArrayPop,
emitDenseArrayPush,
runtimeAccess,
type ArrayAccess,
} from "./dense-array-access.js";
import { INT32_ARRAY_METHODS } from "../../ir/int32-arrays.js";
import { emitUnionWiden } from "./expr-records.js";
import { registerReplay } from "./emit-journal.js";
export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string {
const key = typeKey(inner);
@@ -65,6 +73,7 @@ export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string
if (!sym) {
sym = mangleResolveThunk(host.resolveThunks.size);
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "resolveThunkFor", [inner]);
const v = vAdapters(host.shapeHost, inner);
host.declare(`declare void @scr_resolve_ref_impl(ptr, ptr, ptr, ptr, ptr)`);
host.resolveThunkDefs.push(
@@ -75,6 +84,7 @@ export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string
`}`,
``,
);
host.journal?.built();
}
return sym;
}
@@ -102,20 +112,25 @@ export function tagInSet(
return acc;
}
/** `element`: the array's element type, or null for arrays whose
* elements are not compact class references (dense-array-access.ts
* slotKind). */
export function arrPush(
host: LlvmEmitterContext,
arr: string,
acc: "f64" | "bool" | "ref",
acc: ArrayAccess,
value: string,
element: IrType | null,
): string {
return emitDenseArrayPush(host, arr, acc, value);
return emitDenseArrayPush(host, arr, acc, element, value);
}
/** Move evaluated values through one bounded slot buffer per call site. */
export function emitArrayValues(
host: LlvmEmitterContext,
arr: string,
acc: "f64" | "bool" | "ref",
acc: ArrayAccess,
element: IrType,
values: LlValue[],
prepend = false,
sharedBuffer?: string,
@@ -129,13 +144,16 @@ export function emitArrayValues(
return result;
}
if (values.length === 1) {
if (!prepend) return arrPush(host, arr, acc, values[0]!.name);
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
if (!prepend) return arrPush(host, arr, acc, values[0]!.name, element);
const flat = runtimeAccess(acc);
const ty = flat === "f64" ? "double" : flat === "bool" ? "i1" : "ptr";
host.declare(
`declare double @scr_arr_unshift_${acc}(ptr, ${ty}${acc === "bool" ? " zeroext" : ""})`,
`declare double @scr_arr_unshift_${flat}(ptr, ${ty}${flat === "bool" ? " zeroext" : ""})`,
);
const result = B.tmp();
B.line(`${result} = call double @scr_arr_unshift_${acc}(ptr ${arr}, ${ty} ${values[0]!.name})`);
B.line(
`${result} = call double @scr_arr_unshift_${flat}(ptr ${arr}, ${ty} ${values[0]!.name})`,
);
return result;
}
const capacity = Math.min(values.length, 64);
@@ -153,8 +171,9 @@ export function emitArrayValues(
const slot = B.tmp(),
pointer = B.tmp();
const value = values[start + i]!.name;
// The runtime takes double bits for int32 arrays too, and converts.
const pack =
acc === "f64"
acc === "f64" || acc === "i32"
? `bitcast double ${value} to i64`
: acc === "bool"
? `zext i1 ${value} to i64`
@@ -274,6 +293,33 @@ export function emitArrayCopyLoop(
host.B.line(`call double @scr_arr_push_spread(ptr ${dst}, ptr ${src})`);
}
/** `s.charCodeAt(i)` in place of `s.charAt(i)`, the same index read as a
* code unit. The emitter records the charAt it stands for
* (charAtUnitReads), so a loop version's proof for the charAt covers it. */
export function charAtUnitRead(
host: LlvmEmitterContext,
charAt: IrExpr & { kind: "strIntrinsic" },
): IrExpr & { kind: "strIntrinsic" } {
const read: IrExpr & { kind: "strIntrinsic" } = {
kind: "strIntrinsic",
method: "charCodeAt",
receiver: charAt.receiver,
args: charAt.args,
type: F64,
loc: charAt.loc,
};
host.charAtUnitReads.set(read, charAt);
return read;
}
/** An enclosing loop version proved the string of `e` ASCII and its index
* below the length (bytes-loop-versions.ts). */
function provenStringRead(host: LlvmEmitterContext, e: IrExpr): boolean {
if (host.bytesBounds.has(e)) return true;
const origin = host.charAtUnitReads.get(e);
return origin !== undefined && host.bytesBounds.has(origin);
}
export function emitStrIntrinsic(
host: LlvmEmitterContext,
e: IrExpr & { kind: "strIntrinsic" },
@@ -319,8 +365,10 @@ export function emitStrIntrinsic(
const t = B.tmp();
const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null;
if (index) {
host.declare(STRING_CHAR_CODE_AT_INT_MARKER);
const wide = widenInteger(host, index);
if (provenStringRead(host, e))
return { name: emitAsciiCharCode(host, r.name, wide), type: e.type };
host.declare(STRING_CHAR_CODE_AT_INT_MARKER);
B.line(`${t} = call double @sc_str_char_code_at_int(ptr ${r.name}, i64 ${wide})`);
return { name: t, type: e.type };
}
@@ -333,8 +381,11 @@ export function emitStrIntrinsic(
const t = B.tmp();
const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null;
if (index) {
host.declare(STRING_CHAR_AT_INT_MARKER);
const wide = widenInteger(host, index);
// The table entry is immortal: nothing owns it.
if (provenStringRead(host, e))
return host.markImmortal({ name: emitAsciiChar(host, r.name, wide), type: e.type });
host.declare(STRING_CHAR_AT_INT_MARKER);
B.line(`${t} = call ptr @sc_str_char_at_int(ptr ${r.name}, i64 ${wide})`);
} else {
const at = args[0]?.name ?? f64Lit(0);
@@ -563,7 +614,15 @@ export function emitArrIntrinsic(
if (e.receiver.type.kind !== "array")
throw new InternalCompilerError("llvm emitter bug: arrIntrinsic on non-array");
const elem = e.receiver.type.elem;
const acc = elemAccess(elem);
// Int32 arrays (ir/int32-arrays.ts) reach only these methods; the others
// take the runtime family's flat access. One with checked stores tests
// its kind at each inline access (emitByElementKind) and otherwise takes
// the double entry points, which serve both kinds.
const checked = host.checkedInt32Array(e.receiver);
const access = checked ? "f64" : host.arrayAccess(e.receiver, elem);
if ((access === "i32" || checked) && !INT32_ARRAY_METHODS.has(e.method))
throw new InternalCompilerError(`llvm emitter bug: ${e.method} on an int32 array`);
const acc = runtimeAccess(access);
const accTy = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
const accArg = acc === "bool" ? "i1 zeroext" : accTy;
const method = e.method;
@@ -584,6 +643,13 @@ export function emitArrIntrinsic(
e.receiver.kind === "varRef"
? host.constantNumericTables.get(e.receiver.localId)
: undefined;
if (!table && elem.kind === "f64" && checked) {
const proven = host.provenDenseIndex(e.receiver, e.args[0]!);
const value = host.emitByElementKind(r.name, "double", (kind) =>
emitDenseArrayNumber(host, r.name, index, e.args[0], proven, kind),
)!;
return { name: value, type: e.type };
}
if (!table && elem.kind === "f64")
return {
name: emitDenseArrayNumber(
@@ -592,6 +658,7 @@ export function emitArrIntrinsic(
index,
e.args[0],
host.provenDenseIndex(e.receiver, e.args[0]!),
access === "i32" ? "i32" : "f64",
),
type: e.type,
};
@@ -628,7 +695,22 @@ export function emitArrIntrinsic(
// no-op zero-argument call.
const vs = e.args.map((a) => host.emitExpr(a));
host.emitPublishedArrayGuard(r.name, e.receiver.type, 1, f64Lit(0), f64Lit(vs.length));
return { name: emitArrayValues(host, r.name, acc, vs), type: e.type };
if (checked && vs.length > 0) {
// Each value appends in its own kind test: one that is not an int32
// widens the array before it is pushed.
let length = "";
vs.forEach((v, i) => {
length = host.emitByElementKind(r.name, "double", (kind, widened) => {
if (kind === "i32") host.emitCheckedInt32Store(r.name, v.name, e.args[i]!, widened);
return arrPush(host, r.name, kind, v.name, elem);
})!;
});
return { name: length, type: e.type };
}
// In an int32 kind loop's fast copy, every value is checked before
// the first appends.
vs.forEach((v, i) => host.emitInt32KindStoreCheck(e.receiver, v, e.args[i]!));
return { name: emitArrayValues(host, r.name, access, elem, vs), type: e.type };
}
case "pushSpread": {
// `a.push(...src)`: append src's elements in order (borrowed src,
@@ -660,7 +742,7 @@ export function emitArrIntrinsic(
// from right to left so the final front order is source order.
const vs = e.args.map((a) => host.emitExpr(a));
host.emitPublishedArrayGuard(r.name, e.receiver.type, 4, f64Lit(0), f64Lit(vs.length));
return { name: emitArrayValues(host, r.name, acc, vs, true), type: e.type };
return { name: emitArrayValues(host, r.name, acc, elem, vs, true), type: e.type };
}
case "unshiftSpread": {
// The runtime snapshots the borrowed source and handles self-spread.
@@ -702,7 +784,13 @@ export function emitArrIntrinsic(
let slowLabel = "";
if (method === "pop") {
slowLabel = B.newLabel("remove.slow");
denseRaw = emitDenseArrayPop(host, r.name, acc, la, slowLabel);
const empty = la,
slow = slowLabel;
denseRaw = checked
? host.emitByElementKind(r.name, "i64", (kind) =>
emitDenseArrayPop(host, r.name, kind, elem, empty, slow),
)!
: emitDenseArrayPop(host, r.name, access, elem, la, slowLabel);
denseLabel = B.currentLabel();
B.br(lp);
B.startBlock(slowLabel);
@@ -771,7 +859,7 @@ export function emitArrIntrinsic(
// by content, everything else by pointer). Strict equality. Arrays of
// objects and strings scan inline (array-search.ts).
const v = host.emitExpr(e.args[0]!);
const search = acc === "ref" ? arraySearchKind(elem) : null;
const search = acc === "ref" ? arraySearchKind(elem, host.compactArrayElem(elem)) : null;
if (search !== null) {
host.arraySearchKinds.add(search);
const t = B.tmp();
@@ -789,7 +877,7 @@ export function emitArrIntrinsic(
// Borrowed needle, SameValueZero (NaN matches NaN). For the inline
// searches SameValueZero is strict equality.
const v = host.emitExpr(e.args[0]!);
const search = acc === "ref" ? arraySearchKind(elem) : null;
const search = acc === "ref" ? arraySearchKind(elem, host.compactArrayElem(elem)) : null;
if (search !== null) {
host.arraySearchKinds.add(search);
const at = B.tmp(),
@@ -891,6 +979,14 @@ export function emitArrIntrinsic(
case "fillUndefined": {
const args = e.args.map((arg) => host.emitExpr(arg));
host.emitPublishedArrayGuard(r.name, e.receiver.type, 7);
if (host.checkedFills.has(e)) {
// A new int32 array with checked stores, filled with a value that
// may not be an int32: widen it first then.
const filled = B.newLabel("arr.fill.kind");
host.emitCheckedInt32Store(r.name, args[0]!.name, e.args[0]!, filled);
B.br(filled);
B.startBlock(filled);
}
const helper =
e.method === "copyWithin"
? "scr_arr_copy_within"
@@ -1060,8 +1156,10 @@ function newCollection(host: LlvmEmitterContext, key: IrType, value: IrType): st
const kind = mapKeyKindNum(key, arms);
const m = B.tmp();
host.declare(`declare ptr @scr_map_new(i32, i32, ptr)`);
// SCR_MAP_VAL_CREF (3): string keys and compact values (compactMapValues).
const valKind = compactMapValues(host, key, value) ? 3 : mapValKindNum(value);
B.line(
`${m} = call ptr @scr_map_new(i32 ${kind}, i32 ${mapValKindNum(value)}, ptr ${collectionOps(host, key, value)})`,
`${m} = call ptr @scr_map_new(i32 ${kind}, i32 ${valKind}, ptr ${collectionOps(host, key, value)})`,
);
return m;
}
@@ -1124,6 +1222,9 @@ export function emitMapLikeIntrinsic(
const key = receiverType.kind === "map" ? receiverType.key : receiverType.elem;
const kAcc = mapKeyAccess(key);
const kTy = mapKeyParamType(kAcc);
// The map keeps compact values in 16-byte entries (SCR_MAP_VAL_CREF).
const compact = receiverType.kind === "map" && compactMapValues(host, key, receiverType.value);
const layout = mapEntryLayout(key, compact);
const method = e.method;
// A published Map or Set rejects mutation (the guard precedes the
// operands, which each method evaluates itself).
@@ -1287,7 +1388,7 @@ export function emitMapLikeIntrinsic(
if (numberKey !== null) {
raw = emitNumberMapGetRef(host, r.name, numberKey, value);
} else if (stringKey !== null) {
raw = emitStringMapGetRef(host, r.name, stringKey, value);
raw = emitStringMapGetRef(host, r.name, stringKey, value, compact);
} else {
host.declare(`declare ptr @scr_map_get_${kAcc}_ref(ptr, ${kTy})`);
raw = B.tmp();
@@ -1365,7 +1466,7 @@ export function emitMapLikeIntrinsic(
return host.wrapNullable(raw, raw, value, valueTag, e.type, undefTag);
}
if (stringKey !== null && isRefCounted(value)) {
const raw = emitStringMapGetRef(host, r.name, stringKey, value);
const raw = emitStringMapGetRef(host, r.name, stringKey, value, compact);
return host.wrapNullable(raw, raw, value, valueTag, e.type, undefTag);
}
host.declare(`declare ptr @scr_map_get_${kAcc}_ref(ptr, ${kTy})`);
@@ -1413,7 +1514,8 @@ export function emitMapLikeIntrinsic(
case "has": {
const k = borrowInputs ? host.emitReadReceiver(e.args[0]!) : host.emitExpr(e.args[0]!);
if (kAcc === "f64") return { name: emitNumberMapHas(host, r.name, k.name), type: e.type };
if (kAcc === "str") return { name: emitStringMapHas(host, r.name, k.name), type: e.type };
if (kAcc === "str")
return { name: emitStringMapHas(host, r.name, k.name, compact), type: e.type };
host.declare(`declare zeroext i1 @scr_map_has_${kAcc}(ptr, ${kTy})`);
const t = B.tmp();
B.line(`${t} = call zeroext i1 @scr_map_has_${kAcc}(ptr ${r.name}, ${kTy} ${k.name})`);
@@ -1426,12 +1528,8 @@ export function emitMapLikeIntrinsic(
B.line(`${t} = call zeroext i1 @scr_map_delete_${kAcc}(ptr ${r.name}, ${kTy} ${k.name})`);
return { name: t, type: e.type };
}
case "size": {
host.declare(`declare double @scr_map_size(ptr)`);
const t = B.tmp();
B.line(`${t} = call double @scr_map_size(ptr ${r.name})`);
return { name: t, type: e.type };
}
case "size":
return { name: emitMapSize(host, r.name), type: e.type };
case "clear":
host.declare(`declare void @scr_map_clear(ptr)`);
B.line(`call void @scr_map_clear(ptr ${r.name})`);
@@ -1442,7 +1540,7 @@ export function emitMapLikeIntrinsic(
return { name: emitMapIterStart(host, r.name), type: e.type };
case "iterLive": {
const i = host.emitExpr(e.args[0]!);
const entry = emitMapIterEntry(host, r.name, i.name, mapEntryLayout(key));
const entry = emitMapIterEntry(host, r.name, i.name, layout);
const t = B.tmp();
B.line(`${t} = icmp ne ptr ${entry}, null`);
return { name: t, type: e.type };
@@ -1461,7 +1559,7 @@ export function emitMapLikeIntrinsic(
kAcc === "str" ? "ref" : kAcc,
key,
{ decl: `declare ${retTy} @${symbol}(ptr, double)`, symbol },
mapEntryLayout(key),
layout,
);
return host.own({ name: t, type: e.type });
}
@@ -1488,7 +1586,7 @@ export function emitMapLikeIntrinsic(
decl: `declare ${vAcc === "bool" ? "zeroext i1" : retTy} @${symbol}(ptr, double)`,
symbol,
},
mapEntryLayout(key),
layout,
);
return host.own({ name: t, type: e.type });
}
@@ -1539,3 +1637,5 @@ export function emitSetNew(host: LlvmEmitterContext, e: IrExpr & { kind: "setNew
}
return out;
}
registerReplay("resolveThunkFor", (host, args) => resolveThunkFor(host, args[0] as IrType));
@@ -15,11 +15,23 @@ export interface LlValue {
/** An exact integer snapshot, excluding negative zero, with no discarded
* high bits and the proven range of its original value. */
integer?: { name: string; type: "i32" | "i64"; signed: boolean; range: IntegerRange };
/** An i64 holding the exact integer that the arithmetic producing this
* number computes, when that integer has no proven safe range: its
* absolute value is at most `bound`, and the double equals it whenever
* its absolute value is below `below` (a zero may be -0). Index consumers
* use it in place of a round trip (integer-values.ts `indexInteger`). */
wideInteger?: WideInteger;
slot?: boolean;
/** A sequence local held in a capture box; release the box itself. */
boxed?: boolean;
}
export interface WideInteger {
name: string;
below: number;
bound: number;
}
export type ExprOf<K extends IrExpr["kind"]> = Extract<IrExpr, { kind: K }>;
export type LibCallExpr = ExprOf<"libCall">;
type LibCallPrefixOf<T extends string> = T extends `${infer Prefix}.${string}` ? Prefix : never;
@@ -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;
@@ -1282,9 +1296,7 @@ function emitUnionEqWrappedScalar(
B.startBlock(same);
const eq = B.tmp();
if (arm.kind === "f64") {
host.declare(`declare double @scr_union_get_f64(ptr)`);
const v = B.tmp();
B.line(`${v} = call double @scr_union_get_f64(ptr ${union.name})`);
const v = host.unionGetF64(union.name);
if (e.sameValue) {
host.declare(`declare zeroext i1 @scr_num_same_value(double, double)`);
B.line(`${eq} = call zeroext i1 @scr_num_same_value(double ${v}, double ${plain.name})`);
@@ -1292,9 +1304,7 @@ function emitUnionEqWrappedScalar(
B.line(`${eq} = fcmp oeq double ${v}, ${plain.name}`);
}
} else {
host.declare(`declare zeroext i1 @scr_union_get_bool(ptr)`);
const v = B.tmp();
B.line(`${v} = call zeroext i1 @scr_union_get_bool(ptr ${union.name})`);
const v = host.unionGetBool(union.name);
B.line(`${eq} = icmp eq i1 ${v}, ${plain.name}`);
}
B.line(`store i1 ${eq}, ptr ${slot}`);
@@ -1323,3 +1333,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;
}
@@ -200,6 +200,10 @@ test("identity bitwise forms over a proven int32 return the operand's number", (
}
// Unproven operands keep the full ToInt32; other operators still compute.
expect(body("unknown_or")).toContain("uint32.coerce.slow");
// The i64 conversion covers every magnitude below 2^63; only NaN, the
// infinities and larger magnitudes take the slow block.
expect(body("unknown_or")).toMatch(/ = fcmp oge double %\S+, 0xC3E0000000000000$/m);
expect(body("unknown_or")).toMatch(/ = fcmp olt double %\S+, 0x43E0000000000000$/m);
expect(body("length_and")).toMatch(/ = and i32 .*, 255$/m);
});
@@ -308,7 +312,7 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits"
});
test.each([32, 64] as const)(
"tagged-template string arrays keep 8-byte slots before their states on %i-bit targets",
"tagged-template string arrays keep 8-byte slots without states on %i-bit targets",
(bits) => {
const strings: IrExpr = {
kind: "templateStrings",
@@ -321,14 +325,17 @@ test.each([32, 64] as const)(
mod.functions[0]!.body = [{ kind: "exprStmt", expr: strings, loc }];
const llvm = emitLlvmModule(mod, { pointerBits: bits });
const data = /^@sc_tsa_0_data = internal constant .*$/m.exec(llvm)![0];
// The runtime reads each slot as 8 bytes and finds the states after
// cap * 8 bytes of slots (scr_arr_states_end).
if (bits === 64) expect(data).toContain("<{ [3 x ptr], [3 x i8] }>");
// The runtime reads each slot as 8 bytes; an array without states
// (`spare` 0 in its header) has no state bytes after them.
if (bits === 64) expect(data).toMatch(/^\S+ = internal constant \[3 x ptr\] /);
else {
expect(data).toContain("<{ [3 x <{ ptr, i32 }>], [3 x i8] }>");
expect(data).toMatch(/^\S+ = internal constant \[3 x <\{ ptr, i32 \}>\] /);
expect(data.match(/<\{ ptr @\S+, i32 0 \}>/g)).toHaveLength(3);
}
expect(data).toContain("[3 x i8] [ i8 1, i8 1, i8 1 ]");
expect(data).not.toContain("x i8]");
expect(llvm).toMatch(
/^@sc_tsa_0 = internal global %ScrArr \{ i\d+ -1, i32 3, i32 3, i8 2, i8 0, i16 0, i32 3, /m,
);
},
);
@@ -1,13 +1,14 @@
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js";
import { F64, isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js";
import { isRefCounted, type IrExpr, type IrStrCmpOp, type IrType } from "../../ir/ir.js";
import { mangleRecordClone, mangleRecordNew } from "../mangle.js";
import { arrNewCall, elemAccess } from "./shapes.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
import { BYTES_ELEM_NUM, closureIdentityEqual, f64Lit } from "./common.js";
import { CHECKED_INT32_MIN_LENGTH } from "../../ir/int32-arrays.js";
import { emitStringInputs } from "./string-lifetimes.js";
import { emitStringParts, numberPart, stringParts } from "./string-construction.js";
import { emitBorrowedInputs } from "./borrowed-inputs.js";
@@ -16,23 +17,33 @@ import {
int32Number,
integerNumber,
narrowInteger,
wideArithmetic,
widenInteger,
} from "./integer-values.js";
import {
bitwiseIdentityOperand,
INT32_RANGE,
integerArithmeticRange,
isBitwiseOp,
withinInt32,
withinUint32,
} from "../../ir/integer-ranges.js";
import { emitArrayValues } from "./expr-containers.js";
import { charAtUnitRead, emitArrayValues } from "./expr-containers.js";
import {
constantDivisor,
emitCheckedRemainder,
emitPowerOfTwoRemainder,
emitSignedIntegerRemainder,
} from "./integer-remainder.js";
import { emitDenseArrayGet, emitDenseArrayState, slotWidth } from "./dense-array-access.js";
import {
denseSlotWidth,
emitDenseArrayRead,
emitDenseArrayState,
runtimeAccess,
SCR_ELEM_I32,
slotWidth,
type ArrayAccess,
} from "./dense-array-access.js";
import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js";
export function emitLiteralExpr(
@@ -59,7 +70,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();
@@ -336,7 +348,14 @@ export function emitOperatorExpr(
if (arith[e.op] !== undefined) {
B.line(`${t} = ${arith[e.op]} double ${l.name}, ${r.name}`);
const uint32 = emitWrappingSum(host, e, l, r, li, ri);
if (uint32 !== undefined) return { name: t, type: e.type, uint32 };
// The exact integer beside the double, for index consumers.
const wideInteger = wideArithmetic(host, e.op, l, li, r, ri);
if (uint32 !== undefined || wideInteger) {
const value: LlValue = { name: t, type: e.type };
if (uint32 !== undefined) value.uint32 = uint32;
if (wideInteger) value.wideInteger = wideInteger;
return value;
}
} else B.line(`${t} = fcmp ${cmp[e.op]} double ${l.name}, ${r.name}`);
} else if (bit[e.op] !== undefined) {
if (e.left.type.kind !== "f64" || e.right.type.kind !== "f64") {
@@ -562,14 +581,16 @@ export function regexLiteralTemplate(
};
host.regexInstances.set(key, re);
}
host.journal?.entry("regex", key, re);
return `@${re.sym}`;
}
/** A string equality between `s.charAt(i)` and a literal of exactly one
* UTF-16 code unit: the charAt call and the unit. */
/** A string equality or comparison between `s.charAt(i)` and a literal of
* exactly one UTF-16 code unit: the charAt call, the unit, and whether the
* charAt is the left operand. */
function charAtUnitTest(
e: ExprOf<"strEq">,
): { charAt: ExprOf<"strIntrinsic">; code: number } | null {
e: ExprOf<"strEq" | "strCmp">,
): { charAt: ExprOf<"strIntrinsic">; code: number; left: boolean } | null {
let text: string;
let other: IrExpr;
if (e.right.kind === "strLit") {
@@ -584,7 +605,17 @@ function charAtUnitTest(
if (text.length !== 1) return null;
if (other.kind !== "strIntrinsic" || other.method !== "charAt" || other.args.length !== 1)
return null;
return { charAt: other, code: text.charCodeAt(0) };
return { charAt: other, code: text.charCodeAt(0), left: other === e.left };
}
/** The fcmp predicate of `s.charAt(i) op "c"` on the code unit (or of
* `"c" op s.charAt(i)` when `charAtLeft` is false). A one-unit string
* compares as its unit; an index out of range reads "", which is below
* every one-unit literal, and its unit read is NaN, which the unordered
* predicates count as below. */
function charAtUnitPredicate(op: IrStrCmpOp, charAtLeft: boolean): string {
const flipped = charAtLeft ? op : ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as const)[op];
return { "<": "ult", "<=": "ule", ">": "ogt", ">=": "oge" }[flipped];
}
export function emitStringExpr(
@@ -635,14 +666,7 @@ export function emitStringExpr(
// equals no unit, as "" equals no one-unit literal.
const unit = charAtUnitTest(e);
if (unit !== null) {
const code = host.emitExpr({
kind: "strIntrinsic",
method: "charCodeAt",
receiver: unit.charAt.receiver,
args: unit.charAt.args,
type: F64,
loc: unit.charAt.loc,
});
const code = host.emitExpr(charAtUnitRead(host, unit.charAt));
const t = B.tmp();
B.line(
`${t} = fcmp ${e.negated ? "une" : "oeq"} double ${code.name}, ${f64Lit(unit.code)}`,
@@ -672,6 +696,17 @@ export function emitStringExpr(
return { name: t, type: e.type };
}
case "strCmp": {
// `s.charAt(i) < "c"` (and `s[i]`) for a one-unit literal compares
// the code unit, as the equality above does.
const unit = charAtUnitTest(e);
if (unit !== null) {
const code = host.emitExpr(charAtUnitRead(host, unit.charAt));
const t = B.tmp();
B.line(
`${t} = fcmp ${charAtUnitPredicate(e.op, unit.left)} double ${code.name}, ${f64Lit(unit.code)}`,
);
return { name: t, type: e.type };
}
const [l, r] = emitStringInputs(host, [e.left, e.right]);
const fn = "scr_str_cmp_u16";
host.declare(`declare i32 @${fn}(ptr, ptr)`);
@@ -822,6 +857,7 @@ export function emitStringExpr(
};
host.templateStringsInstances.set(e.key, inst);
}
host.journal?.entry("templateStrings", e.key, inst);
return host.own({ name: host.retainFrame(`@${inst.sym}`, e.type), type: e.type });
}
case "regexIntrinsic":
@@ -868,9 +904,46 @@ export function emitContainerExpr(
// the plain elements up to the next one (scr_arr_reserve_spread).
const firstSpread = e.elems.findIndex((_, i) => spreadSet.has(i));
const capacity = small ? 0 : (e.capacity ?? (firstSpread < 0 ? e.elems.length : firstSpread));
B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
// An int32 array's allocation (ir/int32-arrays.ts) constructs
// SCR_ELEM_I32 storage; its elements are proven int32s. The empty
// temporary of a fill that allocates an int32 array with checked
// stores takes SCR_ELEM_I32 storage when the fill's length (which
// evaluates without effects) is at least CHECKED_INT32_MIN_LENGTH, and
// SCR_ELEM_F64 storage otherwise.
const fillLength = host.checkedAllocations.get(e);
if (fillLength !== undefined && e.elems.length === 0) {
const length = host.emitExpr(fillLength);
const long = B.tmp(),
kind = B.tmp();
B.line(`${long} = fcmp oge double ${length.name}, ${f64Lit(CHECKED_INT32_MIN_LENGTH)}`);
B.line(`${kind} = select i1 ${long}, i32 ${SCR_ELEM_I32}, i32 0`);
host.declare(
small
? `declare ptr @scr_arr_new_small(i32)`
: `declare ptr @scr_arr_new(i32, ${host.sizeType})`,
);
B.line(
small
? `${arr} = call ptr @scr_arr_new_small(i32 ${kind})`
: `${arr} = call ptr @scr_arr_new(i32 ${kind}, ${host.sizeType} ${capacity})`,
);
return host.own({ name: arr, type: e.type });
}
const int32 = host.int32Allocations.has(e);
if (int32) {
host.declare(
small
? `declare ptr @scr_arr_new_small(i32)`
: `declare ptr @scr_arr_new(i32, ${host.sizeType})`,
);
B.line(
small
? `${arr} = call ptr @scr_arr_new_small(i32 ${SCR_ELEM_I32})`
: `${arr} = call ptr @scr_arr_new(i32 ${SCR_ELEM_I32}, ${host.sizeType} ${capacity})`,
);
} else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
const out = host.own({ name: arr, type: e.type });
const acc = elemAccess(elem);
const acc: ArrayAccess = int32 ? "i32" : elemAccess(elem);
let pending: LlValue[] = [];
let buffer: string | undefined;
const flush = (): void => {
@@ -878,7 +951,7 @@ export function emitContainerExpr(
buffer = B.tmp();
B.entryAllocas.push(`${buffer} = alloca [${Math.min(e.elems.length, 64)} x i64]`);
}
if (pending.length > 0) emitArrayValues(host, arr, acc, pending, false, buffer);
if (pending.length > 0) emitArrayValues(host, arr, acc, elem, pending, false, buffer);
pending = [];
};
e.elems.forEach((el, i) => {
@@ -891,7 +964,8 @@ export function emitContainerExpr(
B.line(
`call void @scr_arr_reserve_spread(ptr ${arr}, ptr ${v.name}, ${host.sizeType} ${plain})`,
);
host.emitArrayCopyLoop(arr, v.name, acc);
// A literal with spreads is never an int32 allocation.
host.emitArrayCopyLoop(arr, v.name, runtimeAccess(acc));
return;
}
pending.push(host.emitExpr(el));
@@ -923,7 +997,7 @@ export function emitContainerExpr(
}
const bound = B.tmp();
B.line(`${bound} = fsub double ${n.name}, ${f64Lit(1)}`);
B.countedLoop(bound, () => host.arrPush(arr, acc, fill), "ole");
B.countedLoop(bound, () => host.arrPush(arr, acc, fill, elem), "ole");
return out;
}
case "arrayGet": {
@@ -936,33 +1010,65 @@ export function emitContainerExpr(
// Ref-element reads return +1 (the inline dense path retains like
// the runtime getter); own registers the owned temp in the frame.
const elem = e.arr.type.elem;
const acc = elemAccess(elem);
const t = emitDenseArrayGet(
if (host.checkedInt32Array(e.arr)) {
const proven = host.provenDenseIndex(e.arr, e.index);
const value = host.emitByElementKind(
arr.name,
"double",
(kind) =>
emitDenseArrayRead(
host,
arr.name,
idx,
e.index,
kind,
elem,
"scr_arr_get_f64",
true,
proven,
).value,
)!;
return { name: value, type: e.type };
}
const acc = host.arrayAccess(e.arr, elem);
const read = emitDenseArrayRead(
host,
arr.name,
idx,
e.index,
acc,
elem,
`scr_arr_get_${acc}`,
`scr_arr_get_${runtimeAccess(acc)}`,
true,
host.provenDenseIndex(e.arr, e.index),
);
return host.own({ name: t, type: e.type });
if (read.int32 !== null)
// An int32 array's element: its i32 is exact for index consumers.
return {
name: read.value,
type: e.type,
uint32: read.int32,
integer: { name: read.int32, type: "i32", signed: true, range: INT32_RANGE },
};
return host.own({ name: read.value, type: e.type });
}
case "arrayHas": {
const arr = host.emitStableReceiver(e.arr, [e.index]);
const idx = host.emitExpr(e.index);
if (e.arr.type.kind !== "array")
throw new InternalCompilerError("llvm emitter bug: arrayHas on non-array");
const t = emitDenseArrayState(
host,
arr.name,
idx,
e.index,
"has",
slotWidth(elemAccess(e.arr.type.elem)),
);
const t = host.checkedInt32Array(e.arr)
? host.emitByElementKind(arr.name, "i1", (kind) =>
emitDenseArrayState(host, arr.name, idx, e.index, "has", slotWidth(kind)),
)!
: emitDenseArrayState(
host,
arr.name,
idx,
e.index,
"has",
host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem),
);
return { name: t, type: e.type };
}
case "arrayState": {
@@ -970,14 +1076,18 @@ export function emitContainerExpr(
const idx = host.emitExpr(e.index);
if (e.arr.type.kind !== "array")
throw new InternalCompilerError("llvm emitter bug: arrayState on non-array");
const t = emitDenseArrayState(
host,
arr.name,
idx,
e.index,
"state",
slotWidth(elemAccess(e.arr.type.elem)),
);
const t = host.checkedInt32Array(e.arr)
? host.emitByElementKind(arr.name, "double", (kind) =>
emitDenseArrayState(host, arr.name, idx, e.index, "state", slotWidth(kind)),
)!
: emitDenseArrayState(
host,
arr.name,
idx,
e.index,
"state",
host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem),
);
return { name: t, type: e.type };
}
case "arrIntrinsic":
@@ -1064,13 +1174,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,
@@ -36,6 +43,8 @@ import type { LlvmEmitterContext, LlStreamTypedRefAdapter } from "./expr-context
import type { NullableRefField } from "./nullable-fields.js";
import { emitUnionPeek, emitUnionTag } from "./union-repr.js";
import { inlineUnionCopyLines } from "./inline-unions.js";
import { recordMayKeepSource } from "./record-sources.js";
import { registerReplay } from "./emit-journal.js";
/** An owned (+1) union value for a nullable-pointer class field slot `p`
* (the unit arm's immortal instance, or a fresh box around a retained
@@ -83,6 +92,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri
if (existing) return existing;
const sym = `sc_dpa_${host.dynPromiseAdapters.size}`;
host.dynPromiseAdapters.set(key, sym);
host.journal?.allocated(sym, "dynPromiseAdapter", [inner]);
host.declare(`declare ptr @scr_promise_payload_ref(ptr)`);
host.declare(`declare void @scr_dyn_release_v(ptr)`);
host.declare(`declare void @scr_promise_reject_pending(ptr)`);
@@ -138,6 +148,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri
`}`,
``,
);
host.journal?.built();
return sym;
}
@@ -176,6 +187,7 @@ export function streamTypedRefCommitAdapter(
const check = host.dyn.dynCheckHelper(t);
host.declare(`declare void @scr_arr_release(ptr)`);
host.declare(`declare void @scr_arr_outline(ptr)`);
host.declare(`declare void @scr_arr_adopt_view_properties(ptr, ptr, ptr)`);
const lines = [
`define internal void @${commit}(ptr %target, ptr %d) ${FN_ATTRS} { ; commit live stream element ${typeKey(t)}`,
`entry:`,
@@ -183,6 +195,8 @@ export function streamTypedRefCommitAdapter(
` %missing = icmp eq ptr %next, null`,
` br i1 %missing, label %done, label %swap`,
`swap:`,
// Properties move with the storage (in its out-of-line part).
` call void @scr_arr_adopt_view_properties(ptr %next, ptr %d, ptr %target)`,
// Inline slots live in each header's block; move them out first.
` call void @scr_arr_outline(ptr %target)`,
` call void @scr_arr_outline(ptr %next)`,
@@ -276,7 +290,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);
@@ -331,7 +345,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
@@ -371,7 +390,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}`,
@@ -394,6 +418,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`);
}
@@ -536,6 +565,12 @@ export function streamTypedRefCommitAdapter(
` store ${member.type} %old${index}, ptr %np${index}`,
);
});
// The conversion kept the view's undeclared properties for %next. Shapes
// with accessor slots keep their accessors in the view instead.
if (recordMayKeepSource(shape) && !shapeHasAccessorSlots(shape)) {
host.declare(`declare void @scr_record_source_move(ptr, ptr)`);
lines.push(` call void @scr_record_source_move(ptr %next, ptr %target)`);
}
lines.push(
` call void ${releaseSym(host.shapeHost, t)}(ptr %next)`,
` br label %done`,
@@ -570,6 +605,7 @@ export function liveDynUnionRefAdapter(
const sym = `sc_ldu_${host.liveDynUnionRefAdapters.size}`;
host.liveDynUnionRefAdapters.set(key, sym);
host.journal?.allocated(sym, "liveDynUnionRefAdapter", [t]);
const adapters = new Map<number, LlStreamTypedRefAdapter>();
for (const { arm, tag } of mutableArms) {
@@ -652,6 +688,7 @@ export function liveDynUnionRefAdapter(
`}`,
``,
);
host.journal?.built();
return sym;
}
@@ -700,6 +737,11 @@ export function streamTypedRefMaterializeAdapter(
const snapshot = `sc_ldr_${host.liveDynRefAdapters.size}_materialize`;
const adapter: LlStreamTypedRefAdapter = { snapshot, commit: "null" };
host.liveDynRefAdapters.set(key, adapter);
host.journal?.allocated(
snapshot.replace(/_materialize$/, ""),
"streamTypedRefMaterializeAdapter",
[t],
);
if (isDynTypedRefType(t)) {
const fields = host.classMeta.get(t.className)?.def.fields;
if (
@@ -712,6 +754,7 @@ export function streamTypedRefMaterializeAdapter(
) {
host.declare(`declare ptr @scr_dyn_class_view_unavailable(ptr)`);
adapter.snapshot = "scr_dyn_class_view_unavailable";
host.journal?.built();
return adapter;
}
}
@@ -822,7 +865,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(
@@ -834,7 +877,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`);
@@ -1148,6 +1196,7 @@ export function streamTypedRefMaterializeAdapter(
`}`,
``,
);
host.journal?.built();
return adapter;
}
@@ -1432,6 +1481,7 @@ export function streamFromArrayAdapter(
if (existing) return existing;
const sym = `sc_sfa_${host.streamFromArrayAdapters.size}`;
host.streamFromArrayAdapters.set(key, sym);
host.journal?.allocated(sym, "streamFromArrayAdapter", [t]);
const B = new BlockBuilder();
const unionDef = elem.kind === "union" ? host.unionsById.get(elem.unionId) : undefined;
if (elem.kind === "union" && !unionDef) {
@@ -1548,5 +1598,17 @@ export function streamFromArrayAdapter(
`}`,
``,
);
host.journal?.built();
return sym;
}
registerReplay("dynPromiseAdapter", (host, args) => dynPromiseAdapter(host, args[0] as IrType));
registerReplay("liveDynUnionRefAdapter", (host, args) =>
liveDynUnionRefAdapter(host, args[0] as IrType & { kind: "union" }),
);
registerReplay("streamTypedRefMaterializeAdapter", (host, args) =>
streamTypedRefMaterializeAdapter(host, args[0] as IrType).snapshot.replace(/_materialize$/, ""),
);
registerReplay("streamFromArrayAdapter", (host, args) =>
streamFromArrayAdapter(host, args[0] as IrType & { kind: "array" }),
);
@@ -4,6 +4,7 @@ import { type IrType, isRefCounted, typeKey } from "../../ir/ir.js";
import { FN_ATTRS, releaseSym, retainSym, traceArg } from "./shapes.js";
import type { LlvmEmitterContext } from "./expr-context.js";
import { unionPeekLines, unionTagLines } from "./union-repr.js";
import { registerReplay } from "./emit-journal.js";
export function streamDataAdapter(
host: LlvmEmitterContext,
@@ -14,24 +15,57 @@ export function streamDataAdapter(
if (sym) return sym;
sym = `sc_ee_dad_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "streamDataAdapter", [cbT]);
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 +91,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 +107,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 +128,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 +141,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 +160,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,16 +187,35 @@ 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);
host.journal?.built();
return sym;
}
export function streamDoneFnFor(
host: LlvmEmitterContext,
kind: "w" | "f" | "d" | "t" | "l",
kind: "w" | "f" | "d" | "t" | "l" | "c",
doneT: IrType & { kind: "func" },
): string {
const key = `sd:${kind}:${typeKey(doneT)}`;
@@ -149,6 +223,7 @@ export function streamDoneFnFor(
if (sym) return sym;
sym = `sc_sd_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "streamDoneFnFor", [kind, doneT]);
const errT = doneT.params[0];
let errTag = -1;
if (errT !== undefined) {
@@ -164,25 +239,46 @@ 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"
? "scr_stream_write_done"
: kind === "f"
? "scr_stream_final_done"
: 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" : ""})`);
kind === "c"
? "scr_stream_construct_done"
: kind === "w"
? "scr_stream_write_done"
: kind === "f"
? "scr_stream_final_done"
: kind === "d"
? "scr_stream_destroy_done"
: kind === "t"
? 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 +286,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 +330,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,9 +349,11 @@ 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);
host.journal?.built();
return sym;
}
@@ -261,6 +367,7 @@ export function fsRenameThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
if (sym) return sym;
sym = `sc_fsren_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "fsRenameThunkFor", [cbT]);
const param = cbT.params[0]!;
const d: string[] = [
`define internal void @${sym}(ptr %cb, ptr %err) ${FN_ATTRS} { ; fs.rename callback ${typeKey(cbT)}`,
@@ -316,6 +423,7 @@ export function fsRenameThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
``,
);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
@@ -348,6 +456,7 @@ export function cryptoIntThunkFor(
if (sym) return sym;
sym = `sc_cryptoint_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "cryptoIntThunkFor", [cbT]);
const d: string[] = [
`define internal void @${sym}(ptr %cb, double %value) ${FN_ATTRS} { ; randomInt callback ${typeKey(cbT)}`,
`entry:`,
@@ -390,6 +499,7 @@ export function cryptoIntThunkFor(
``,
);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
@@ -408,6 +518,7 @@ export function zlibBytesThunkFor(
if (sym) return sym;
sym = `sc_${family}bytes_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "zlibBytesThunkFor", [cbT, arrayBuffer, family, valueMode]);
const d: string[] = [
`define internal void @${sym}(ptr %cb, ptr %err, ptr %value) ${FN_ATTRS} { ; zlib bytes callback ${typeKey(cbT)}`,
`entry:`,
@@ -519,12 +630,13 @@ export function zlibBytesThunkFor(
``,
);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
export function streamCbThunkFor(
host: LlvmEmitterContext,
kind: "r" | "w" | "f" | "d" | "t" | "l" | "e",
kind: "r" | "w" | "f" | "d" | "t" | "l" | "e" | "c" | "v",
cbT: IrType,
): string {
if (cbT.kind !== "func")
@@ -534,14 +646,17 @@ export function streamCbThunkFor(
if (sym) return sym;
sym = `sc_scb_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.journal?.allocated(sym, "streamCbThunkFor", [kind, cbT]);
const runtimeParams =
kind === "r"
? ["ptr %s", "double %size"]
: kind === "w" || kind === "t"
? ["ptr %s", "ptr %chunk"]
: kind === "d" || kind === "e"
? ["ptr %s", "ptr %err"]
: ["ptr %s"];
: kind === "v"
? ["ptr %s", "ptr %chunks"]
: kind === "d" || kind === "e"
? ["ptr %s", "ptr %err"]
: ["ptr %s"];
const declared = cbT.params;
const hasThis = declared[0] !== undefined && declared[0].kind === "object";
if (declared.length === 0) {
@@ -550,7 +665,9 @@ export function streamCbThunkFor(
);
}
const off = hasThis ? 1 : 0;
const full = (kind === "r" ? 1 : kind === "w" || kind === "t" ? 3 : kind === "d" ? 2 : 1) + off;
const full =
(kind === "r" ? 1 : kind === "w" || kind === "t" ? 3 : kind === "d" || kind === "v" ? 2 : 1) +
off;
if (declared.length > full) {
throw new InternalCompilerError(
`llvm emitter bug: stream '${kind}' callback declares ${declared.length} params (frontend must fence)`,
@@ -598,6 +715,31 @@ export function streamCbThunkFor(
}
continue;
}
if (kind === "v" && pos === 0) {
// writev's chunks: the batch array (borrowed from the runtime), as
// a dyn value or converted to the declared type
if (p.kind === "dyn") {
host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
d.push(` %vc${i} = call ptr @scr_dyn_retain_v(ptr %chunks)`);
passed.push(`ptr %vc${i}`);
} else {
const helper = host.dyn.dynCheckHelper(p, false);
const ty = host.llType(p);
host.declare(`declare zeroext i1 @scr_exc_pending()`);
if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`);
d.push(
` %vt${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %chunks, ptr null)`,
` %vtbad${i} = call zeroext i1 @scr_exc_pending()`,
` br i1 %vtbad${i}, label %vtfail${i}, label %vtok${i}`,
`vtfail${i}:`,
...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []),
` ret void`,
`vtok${i}:`,
);
passed.push(`${ty} %vt${i}`);
}
continue;
}
const isChunkPos = (kind === "w" || kind === "t") && pos === 0;
const isEncPos = (kind === "w" || kind === "t") && pos === 1;
const isErrPos = (kind === "d" || kind === "e") && pos === 0;
@@ -606,23 +748,19 @@ export function streamCbThunkFor(
? false
: kind === "w" || kind === "t"
? pos === 2
: kind === "f" || kind === "l"
: kind === "f" || kind === "l" || kind === "c"
? pos === 0
: 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
@@ -656,7 +794,7 @@ export function streamCbThunkFor(
d.push(` br label %ego${i}`, `ego${i}:`, ` %edv${i} = load ptr, ptr %edslot${i}`);
passed.push(`ptr %edv${i}`);
} else if (isDonePos) {
const glue = `scr_stream_done_dyn_${kind}`;
const glue = `scr_stream_done_dyn_${kind === "v" ? "w" : kind}`;
host.declare(`declare ptr @${glue}(ptr, ptr, ${host.sizeType})`);
host.declare(`declare ptr @scr_dyn_new_func(ptr, ptr, i32, ptr, ptr)`);
const clo = doneClosure(`@${glue}`, `dd${i}`);
@@ -673,15 +811,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 |
@@ -721,7 +915,8 @@ export function streamCbThunkFor(
);
passed.push(`ptr %euv${i}`);
} else if (isDonePos) {
const doneKind = kind as "w" | "f" | "d" | "t" | "l"; // "e" has no done position
// "e" has no done position; writev completes as a write
const doneKind = (kind === "v" ? "w" : kind) as "w" | "f" | "d" | "t" | "l" | "c";
if (p.kind !== "func")
throw new InternalCompilerError("llvm emitter bug: stream done callback not a func");
const doneFn = host.streamDoneFnFor(doneKind, p);
@@ -750,5 +945,35 @@ export function streamCbThunkFor(
}
d.push(` ret void`, `}`, ``);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
registerReplay("streamDataAdapter", (host, args) =>
streamDataAdapter(host, args[0] as IrType & { kind: "func" }),
);
registerReplay("streamDoneFnFor", (host, args) =>
streamDoneFnFor(
host,
args[0] as "w" | "f" | "d" | "t" | "l",
args[1] as IrType & { kind: "func" },
),
);
registerReplay("fsRenameThunkFor", (host, args) =>
fsRenameThunkFor(host, args[0] as IrType & { kind: "func" }),
);
registerReplay("cryptoIntThunkFor", (host, args) =>
cryptoIntThunkFor(host, args[0] as IrType & { kind: "func" }),
);
registerReplay("zlibBytesThunkFor", (host, args) =>
zlibBytesThunkFor(
host,
args[0] as IrType & { kind: "func" },
args[1] as boolean,
args[2] as string,
args[3] as BytesThunkValue,
),
);
registerReplay("streamCbThunkFor", (host, args) =>
streamCbThunkFor(host, args[0] as "r" | "w" | "f" | "d" | "t" | "l" | "e", args[1] as IrType),
);
@@ -0,0 +1,135 @@
import type { IrExpr, IrStmt } from "../../ir/ir.js";
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
import { MAX_LOOP_NODES, SIMPLE_STATEMENTS } from "./speculative-integers.js";
/** Int32 kind loops.
*
* An int32 array with checked stores (ir/int32-slots.ts) holds int32 slots
* (or doubles, when its fill was short) until a store of a value that is
* not an int32 widens it to doubles in place, so an access to it tests its
* element kind first (LlEmitter.emitByElementKind). An innermost loop that
* accesses such arrays tests their kinds once on entry instead
* (emitLoopCopies). The fast copy runs when every one of them holds int32
* slots and reads and writes them as int32 arrays; the wide copy runs when
* every one holds doubles and accesses them as double arrays; a loop over
* several arrays whose kinds differ runs the mixed copy, which tests the
* kinds at each access. A store of the fast copy whose value is not an
* int32 branches to the same statement in a third copy, which re-executes
* it with kind tests (widening the array) and finishes the iteration, then
* enters the mixed copy, or the wide copy when the loop has one array.
* Re-execution is exact because such a store's statement evaluates nothing
* but locals, literals, number arithmetic and element reads before it
* writes.
*
* Nothing else can widen these arrays: they never reach code other than
* the compiled element operations of their own function
* (ir/int32-arrays.ts). */
export interface Int32KindLoopPlan {
/** The arrays the fast copy reads and writes as int32 arrays, by local,
* with one of their references in the loop. */
arrays: Map<string, IrExpr>;
/** Element writes and pushes whose value may not be an int32: the
* statements the fast copy may leave at. */
writes: Set<IrStmt>;
}
type KindLoop = Extract<IrStmt, { kind: "for" | "while" | "doWhile" }>;
/** A number or boolean expression whose evaluation has no effect: locals,
* literals, arithmetic, comparisons and element reads of local arrays. */
function isPure(e: IrExpr): boolean {
if (e.type.kind !== "f64" && e.type.kind !== "bool") return false;
switch (e.kind) {
case "numLit":
case "boolLit":
case "varRef":
return true;
case "bin":
return (
(e.left.type.kind === "f64" || e.left.type.kind === "bool") &&
isPure(e.left) &&
isPure(e.right)
);
case "unary":
return isPure(e.operand);
case "ternary":
return isPure(e.cond) && isPure(e.then) && isPure(e.else_);
case "arrayGet":
case "arrayHas":
return e.arr.kind === "varRef" && isPure(e.index);
default:
return false;
}
}
/** The plan for `loop`, or null. `checked` names the int32 array locals
* with checked stores of the function; `provenInt32` the values the static
* range analysis proves int32s (stores that never leave the fast copy). */
export function planInt32KindLoop(
loop: KindLoop,
checked: (localId: string) => boolean,
provenInt32: (e: IrExpr) => boolean,
): Int32KindLoopPlan | null {
const arrays = new Map<string, IrExpr>();
const writes = new Set<IrStmt>();
const assigned = new Set<string>();
let nodes = 0;
let rejected = false;
const reject = (): false => {
rejected = true;
return false;
};
const array = (e: IrExpr): string | null =>
e.kind === "varRef" && checked(e.localId) ? e.localId : null;
const push = (e: IrExpr): (IrExpr & { kind: "arrIntrinsic" }) | null =>
e.kind === "arrIntrinsic" && e.method === "push" && array(e.receiver) !== null ? e : null;
const expr = (e: IrExpr): boolean => {
if (rejected) return false;
nodes++;
if (e.kind === "varRef" && checked(e.localId) && !arrays.has(e.localId))
arrays.set(e.localId, e);
// A push the statement walk did not take as a write: its values must
// be proven int32s, since it cannot leave the fast copy.
const pushed = push(e);
if (pushed && !pushed.args.every(provenInt32)) return reject();
return everyExprChild(e, expr, nested);
};
// Statements inside expressions run mid-expression, where the fast copy
// cannot leave: they may not write these arrays.
const nested = (s: IrStmt): boolean => {
nodes++;
if (s.kind === "assign" || s.kind === "varDecl") assigned.add(s.localId);
if (s.kind === "arraySet" && array(s.arr) !== null && !provenInt32(s.value)) return reject();
return everyStmtChild(s, expr, nested);
};
const stmt = (s: IrStmt): boolean => {
if (rejected) return false;
nodes++;
if (!SIMPLE_STATEMENTS.has(s.kind)) return reject();
if (s.kind === "assign" || s.kind === "varDecl") assigned.add(s.localId);
if (s.kind === "arraySet" && array(s.arr) !== null && !provenInt32(s.value)) {
if (!isPure(s.index) || !isPure(s.value)) return reject();
writes.add(s);
return everyStmtChild(s, expr, stmt);
}
const pushed = s.kind === "exprStmt" ? push(s.expr) : null;
if (pushed && !pushed.args.every(provenInt32)) {
if (!pushed.args.every(isPure)) return reject();
writes.add(s);
expr(pushed.receiver);
pushed.args.forEach(expr);
return !rejected;
}
return everyStmtChild(s, expr, stmt);
};
if (loop.cond) expr(loop.cond);
if (loop.kind === "for" && loop.update) stmt(loop.update);
for (const s of loop.body) stmt(s);
if (rejected || nodes > MAX_LOOP_NODES || arrays.size === 0) return null;
// A loop that rebinds one of the arrays could reach another array of
// either kind.
for (const id of arrays.keys()) if (assigned.has(id)) return null;
return { arrays, writes };
}
@@ -0,0 +1,130 @@
import { expect, test } from "vitest";
import { F64, arrayOf, bytesOf, type IrExpr, type IrModule, type IrStmt } from "../../ir/ir.js";
import { emitLlvmModule } from "./emitter.js";
const loc = { file: "integer-or-nan-indexes.ts", start: 0, end: 0 };
const nums = arrayOf(F64);
const u8 = bytesOf("u8");
const ref = (localId: string, type: IrExpr["type"]): IrExpr => ({
kind: "varRef",
localId,
type,
loc,
});
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
/** `t[i]!`: the element, or NaN for an index that misses. */
const byteRead = (index: IrExpr): IrExpr => ({
kind: "bytesIntrinsic",
method: "get",
receiver: ref("t", u8),
args: [index],
type: F64,
loc,
invalidNaN: true,
});
function module(body: IrStmt[]): IrModule {
return {
irVersion: 17,
sourceFile: loc.file,
entry: "__main",
unions: [],
functions: [
{ name: "__main", params: [], returnType: F64, locals: [], body: [], loc },
{
name: "read",
params: [
{ name: "a", localId: "a", type: nums },
{ name: "t", localId: "t", type: u8 },
{ name: "i", localId: "i", type: F64 },
],
returnType: F64,
locals: [
{ id: "a", name: "a", type: nums, mutable: false },
{ id: "t", name: "t", type: u8, mutable: false },
{ id: "i", name: "i", type: F64, mutable: false },
{ id: "c", name: "c", type: F64, mutable: true },
],
body,
loc,
},
],
};
}
function body(llvm: string): string {
return /^define internal [^\n]*@sc_bf_read(?:\.i32)?\([^]*?^}/m.exec(llvm)![0];
}
const count = (text: string, pattern: RegExp): number => text.match(pattern)?.length ?? 0;
test("an index bound to a byte read that may miss checks only that it is ordered", () => {
const llvm = emitLlvmModule(
module([
{ kind: "varDecl", localId: "c", init: byteRead(ref("i", F64)), loc },
{
kind: "return",
value: {
kind: "bin",
op: "+",
left: { kind: "arrayGet", arr: ref("a", nums), index: ref("c", F64), type: F64, loc },
right: {
kind: "bytesIntrinsic",
method: "get",
receiver: ref("t", u8),
args: [{ kind: "bin", op: "+", left: ref("c", F64), right: num(1), type: F64, loc }],
type: F64,
loc,
invalidNaN: true,
},
type: F64,
loc,
},
loc,
},
]),
);
const fn = body(llvm);
// Only the parameter index `i` round-trips through a double.
expect(count(fn, /fcmp oeq double/g)).toBe(1);
expect(count(fn, /fcmp ord double/g)).toBe(2);
});
test("a local also written an ordinary number keeps the exact index check", () => {
const llvm = emitLlvmModule(
module([
{ kind: "varDecl", localId: "c", init: byteRead(ref("i", F64)), loc },
{ kind: "assign", localId: "c", value: ref("i", F64), loc },
{
kind: "return",
value: { kind: "arrayGet", arr: ref("a", nums), index: ref("c", F64), type: F64, loc },
loc,
},
]),
);
const fn = body(llvm);
expect(count(fn, /fcmp oeq double/g)).toBe(2);
expect(fn).not.toContain("fcmp ord double");
});
test("a local whose writes grow from its own value keeps the exact index check", () => {
const llvm = emitLlvmModule(
module([
{ kind: "varDecl", localId: "c", init: byteRead(ref("i", F64)), loc },
{
kind: "assign",
localId: "c",
value: { kind: "bin", op: "+", left: ref("c", F64), right: num(1), type: F64, loc },
loc,
},
{
kind: "return",
value: { kind: "arrayGet", arr: ref("a", nums), index: ref("c", F64), type: F64, loc },
loc,
},
]),
);
const fn = body(llvm);
expect(count(fn, /fcmp oeq double/g)).toBe(2);
expect(fn).not.toContain("fcmp ord double");
});
@@ -1,6 +1,6 @@
import type { IrExpr, IrType } from "../../ir/ir.js";
import type { IntegerRange } from "../../ir/integer-ranges.js";
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import type { LlValue, LlvmEmitterContext, WideInteger } from "./expr-context.js";
export type LlInteger = NonNullable<LlValue["integer"]>;
@@ -51,6 +51,89 @@ export function exactInteger(
return { name, type: "i64", signed: true, range };
}
/** Doubles hold every integer of smaller magnitude exactly. */
const EXACT_DOUBLES = 2 ** 53;
/** Wide views stay within this magnitude: a product of two int32s reaches
* 2^62, one exact term of at most 2^53 may be added to it, and no i64
* result wraps. */
const WIDE_BOUND = 2 ** 62 + 2 ** 53;
/** Array and string indexes are below 2^32 - 1: a wide view exact below
* this many is exact at every index a length can admit. */
const INDEX_LIMIT = 2 ** 32;
function magnitude(range: IntegerRange): number {
return Math.max(Math.abs(range.min), Math.abs(range.max));
}
/** The wide view of `l op r` for `+`, `-` and `*` when the exact operands
* have no proven result range (their product or sum may pass 2^53).
*
* - A product of two exact integers equals its double whenever its
* magnitude is below 2^53: the double product of integers is exact there.
* - A sum or difference of two exact integers likewise.
* - A sum or difference of a wide view (exact below T) and an exact integer
* of magnitude at most B equals its double below min(T - B, 2^53): the
* view's magnitude is then below T, so its double is exact, and the
* double sum of two exact integers below 2^53 is exact.
*
* Every i64 result stays within 2^62 + 2^53 in magnitude, so none wraps. A view
* exact below fewer than 2^32 helps no index and is not kept. */
export function wideArithmetic(
host: LlvmEmitterContext,
op: string,
l: LlValue,
li: LlInteger | null,
r: LlValue,
ri: LlInteger | null,
): WideInteger | null {
if (op !== "+" && op !== "-" && op !== "*") return null;
const view = (value: LlValue, integer: LlInteger | null): WideInteger | null =>
integer
? { name: "", below: Infinity, bound: magnitude(integer.range) }
: (value.wideInteger ?? null);
const lv = view(l, li),
rv = view(r, ri);
if (!lv || !rv) return null;
let below: number, bound: number;
if (op === "*") {
if (lv.below !== Infinity || rv.below !== Infinity) return null;
below = EXACT_DOUBLES;
bound = lv.bound * rv.bound;
} else if (lv.below === Infinity && rv.below === Infinity) {
below = EXACT_DOUBLES;
bound = lv.bound + rv.bound;
} else if (lv.below === Infinity || rv.below === Infinity) {
const [exact, wide] = lv.below === Infinity ? [lv, rv] : [rv, lv];
below = Math.min(wide.below - exact.bound, EXACT_DOUBLES);
bound = exact.bound + wide.bound;
} else return null;
if (!(bound <= WIDE_BOUND) || below < INDEX_LIMIT) return null;
const operand = (value: LlValue, integer: LlInteger | null): string =>
integer ? widenInteger(host, integer) : value.wideInteger!.name;
const left = operand(l, li),
right = operand(r, ri);
const name = host.B.tmp();
host.B.line(
`${name} = ${op === "+" ? "add" : op === "-" ? "sub" : "mul"} nsw i64 ${left}, ${right}`,
);
return { name, below, bound };
}
/** An i64 index for a consumer that compares it, unsigned, with a length or
* capacity below 2^32 and takes the double's path otherwise: the exact
* integer of `value`, or its wide view, which equals the double at every
* such index. Null when the index needs the double round trip. */
export function indexInteger(
host: LlvmEmitterContext,
value: LlValue,
expr?: IrExpr,
): string | null {
const integer = exactInteger(host, value, expr);
if (integer) return widenInteger(host, integer);
const wide = value.wideInteger;
return wide && wide.below >= INDEX_LIMIT ? wide.name : null;
}
export function widenInteger(host: LlvmEmitterContext, value: LlInteger): string {
if (value.type === "i64") return value.name;
const wide = host.B.tmp();
@@ -3,6 +3,7 @@ import { typeKey } from "../../ir/ir.js";
import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js";
import { abiValue, callbackType, finishRuntimeCall, ptr } from "./lib-abi.js";
import { FN_ATTRS } from "./shapes.js";
import { registerReplay } from "./emit-journal.js";
/** CONNECT transfers owned request/socket/response values to the listener. */
function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): string {
@@ -18,6 +19,7 @@ function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): str
if (previous !== undefined) return previous;
const symbol = `sc_connect_${host.resolveThunks.size}`;
host.resolveThunks.set(key, symbol);
host.journal?.allocated(symbol, "connectAdapter", [cb, h2]);
const retain = h2 ? "scr_http_res_retain_v" : "scr_net_sock_retain_v";
const release = h2 ? "scr_http_res_release_v" : "scr_net_sock_release_v";
host.declare(`declare ptr @${retain}(ptr)`);
@@ -37,6 +39,7 @@ function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): str
"}",
"",
);
host.journal?.built();
return symbol;
}
@@ -70,3 +73,7 @@ export function emitConnectListener(host: LlvmEmitterContext, e: LibCallExpr): L
a[2]!,
]);
}
registerReplay("connectAdapter", (host, args) =>
connectAdapter(host, args[0] as LlValue, args[1] as boolean),
);
@@ -11,6 +11,7 @@ import {
ptr,
} from "./lib-abi.js";
import { FN_ATTRS } from "./shapes.js";
import { registerReplay } from "./emit-journal.js";
function messageAdapter(host: LlvmEmitterContext, type: IrType): string {
if (type.kind !== "record")
@@ -20,6 +21,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string {
if (previous !== undefined) return previous;
const symbol = `sc_dgram_message_${host.resolveThunks.size}`;
host.resolveThunks.set(key, symbol);
host.journal?.allocated(symbol, "messageAdapter", [type]);
const shape = host.recordsById.get(type.shapeId);
if (shape === undefined) throw new InternalCompilerError("missing datagram remote info shape");
const lines: string[] = [];
@@ -48,6 +50,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string {
"}",
"",
);
host.journal?.built();
return symbol;
}
@@ -67,6 +70,7 @@ function lookupAdapter(host: LlvmEmitterContext, cb: LlValue): string {
if (previous !== undefined) return previous;
const symbol = `sc_dns_lookup_${host.resolveThunks.size}`;
host.resolveThunks.set(key, symbol);
host.journal?.allocated(symbol, "lookupAdapter", [cb]);
host.declare("declare ptr @scr_error_new(i32, ptr)");
host.declare("declare ptr @scr_error_retain_v(ptr)");
host.declare("declare void @scr_error_release_v(ptr)");
@@ -93,6 +97,7 @@ function lookupAdapter(host: LlvmEmitterContext, cb: LlValue): string {
"}",
"",
);
host.journal?.built();
return symbol;
}
@@ -105,6 +110,10 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
"dgram.sendStr": "scr_dgram_send_str",
"dgram.sendBytes": "scr_dgram_send_bytes",
"dgram.sendChk": "scr_dgram_send_chk",
"dgram.unixSocket": "scr_dgram_unix_socket",
"dgram.unixConnect": "scr_dgram_unix_connect",
"dgram.unixSend": "scr_dgram_unix_send",
"dgram.unixClose": "scr_dgram_unix_close",
"dgram.ref": "scr_dgram_ref",
"dgram.unref": "scr_dgram_unref",
"dgram.setBroadcast": "scr_dgram_set_broadcast",
@@ -250,3 +259,6 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
throw new InternalCompilerError(`unhandled datagram call ${e.fn}`);
}
}
registerReplay("messageAdapter", (host, args) => messageAdapter(host, args[0] as IrType));
registerReplay("lookupAdapter", (host, args) => lookupAdapter(host, args[0] as LlValue));

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