Merge main

This commit is contained in:
Chris Tate
2026-10-22 17:39:40 -05:00
333 changed files with 15135 additions and 1254 deletions
+14
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:
+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,8 @@ 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 |
| `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods |
| `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 |
@@ -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/dynamic-methods.mjs",
"args": [
"300000"
]
}
@@ -0,0 +1,36 @@
// Untyped JavaScript calling builtin methods on values it never declares
// types for: string slice/indexOf/toUpperCase/startsWith/padStart, array
// push/pop/at/join/includes/indexOf, Map and Set get/set/has/add, number
// toFixed, and calls of an object's own methods. Argument: the number of
// iterations.
const rounds = Number(process.argv[2] ?? "300000");
const words = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta"];
const counts = new Map();
const seen = new Set();
const stack = [];
const recent = [];
const tally = {
total: 0,
add(value) {
this.total += value;
return this.total;
},
};
let checksum = 0;
for (let i = 0; i < rounds; i++) {
const word = words[i % words.length];
const head = word.slice(0, 3).toUpperCase();
const key = head.padStart(4, "-");
if (word.indexOf("e") >= 0 && !word.startsWith("z")) checksum += 1;
counts.set(key, (counts.get(key) ?? 0) + 1);
seen.add(i % 97);
if (seen.has(i % 89)) checksum += 2;
stack.push(i);
if (stack.length > 32) checksum += stack.pop() % 3;
recent.push(word);
if (recent.length > 4) recent.shift();
if (recent.includes("gamma")) checksum += recent.indexOf("gamma");
checksum += tally.add(i % 5) % 11;
if (i % 1000 === 0) checksum += Number((i / 7).toFixed(2)) | 0;
}
console.log(checksum, counts.size, seen.size, stack.at(-1), recent.join(","), tally.total);
+1 -1
View File
@@ -349,7 +349,7 @@ class Identifier extends SyntaxNode {
}
```
On Linux, except in builds with sanitizers, the instances also take less memory. An executable keeps them in one address range below 32 GiB that it reserves at startup (only the pages in use take memory), and a field typed `C`, `C | undefined` or `C | null`, where `C` is a program-lifetime class, stores the instance's address in 4 bytes instead of 8. In the example, `parent` takes 4 bytes.
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):
+7 -1
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" />
@@ -211,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)
@@ -8,7 +8,7 @@
`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 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.
@@ -25,6 +25,9 @@
`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.
@@ -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.
+12 -12
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"
}
},
{
@@ -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"
}
},
{
@@ -19328,6 +19328,10 @@
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/corpus/7122-webcrypto-node-crypto.ts",
"tests/corpus/7123-webcrypto-node-crypto.cjs",
"tests/corpus/7150-cipher-legacy-modes.ts",
"tests/corpus/7151-cipher-legacy-errors.cjs",
"tests/corpus/7152-crypto-prime.ts",
"tests/corpus/7153-crypto-prime-errors.cjs",
"tests/corpus/node24-crypto-kdf-errors.ts",
"tests/corpus/node24-crypto-kdf-options.ts",
"tests/corpus/node24-crypto-kdf.ts",
@@ -19658,8 +19662,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "surface-manifest:node-builtin.crypto.checkPrime",
"tests": [
"tests/corpus/7152-crypto-prime.ts",
"tests/corpus/7153-crypto-prime-errors.cjs"
]
},
"anchorSource": "exact"
},
@@ -19681,8 +19689,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "surface-manifest:node-builtin.crypto.checkPrimeSync",
"tests": [
"tests/corpus/7152-crypto-prime.ts",
"tests/corpus/7153-crypto-prime-errors.cjs"
]
},
"anchorSource": "exact"
},
@@ -19707,6 +19719,8 @@
"status": "partial",
"evidence": "surface-manifest:node-builtin.crypto.createCipheriv",
"tests": [
"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",
@@ -19746,6 +19760,8 @@
"status": "partial",
"evidence": "surface-manifest:node-builtin.crypto.createDecipheriv",
"tests": [
"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",
@@ -20364,8 +20380,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "surface-manifest:node-builtin.crypto.generatePrime",
"tests": [
"tests/corpus/7152-crypto-prime.ts",
"tests/corpus/7153-crypto-prime-errors.cjs"
]
},
"anchorSource": "exact"
},
@@ -20387,8 +20407,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "surface-manifest:node-builtin.crypto.generatePrimeSync",
"tests": [
"tests/corpus/7152-crypto-prime.ts",
"tests/corpus/7153-crypto-prime-errors.cjs"
]
},
"anchorSource": "exact"
},
+6 -2
View File
@@ -42,8 +42,10 @@
"node-builtin.child_process": ["tests/corpus/1361-spawn-events.ts", "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", "tests/corpus/2946-child-fork-ipc/main.ts", "tests/corpus/3127-spawn-runtime-options.ts", "tests/corpus/3130-child-generic-streams.ts", "tests/corpus/3131-child-rest-listeners.js", "tests/corpus/3132-child-generic-stream-completion.ts"],
"node-builtin.cluster": ["tests/corpus/957-builtins-namespace.ts"],
"node-builtin.console": ["tests/corpus/console-imports.cjs", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/console-native-value.js", "tests/harness/npm-static.test.ts"],
"node-builtin.crypto.createCipheriv": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"],
"node-builtin.crypto.createDecipheriv": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"],
"node-builtin.crypto.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"],
@@ -120,6 +122,8 @@
"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"],
+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",
+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: 27, marker: "scr_runtime_abi_v27" },
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
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_v27");
expect(error?.stderr).toContain("scr_runtime_abi_v28");
});
});
+60 -2
View File
@@ -3171,7 +3171,8 @@ declare module "crypto" {
callback: (error: Error | null, buffer: Buffer) => void,
): void;
export type BinaryLike = string | ArrayBuffer | ArrayBufferView;
/* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, and gcm modes and
/* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8,
* cfb1, and ofb modes, des-ede3 and des-ede, chacha20, and
* chacha20-poly1305. Both are Transforms: written chunks go through
* update(), and the end pushes final(). */
export type CipherGCMTypes = "aes-128-gcm" | "aes-192-gcm" | "aes-256-gcm";
@@ -3351,6 +3352,61 @@ declare module "crypto" {
): DiffieHellman;
export function getDiffieHellman(groupName: string): DiffieHellmanGroup;
export function createDiffieHellmanGroup(name: string): DiffieHellmanGroup;
export type LargeNumberLike = ArrayBufferView | SharedArrayBuffer | ArrayBuffer | bigint;
export interface GeneratePrimeOptions {
add?: LargeNumberLike | undefined;
rem?: LargeNumberLike | undefined;
safe?: boolean | undefined;
bigint?: boolean | undefined;
}
export interface GeneratePrimeOptionsBigInt extends GeneratePrimeOptions {
bigint: true;
}
export interface GeneratePrimeOptionsArrayBuffer extends GeneratePrimeOptions {
bigint?: false | undefined;
}
export function generatePrime(
size: number,
callback: (err: Error | null, prime: ArrayBuffer) => void,
): void;
export function generatePrime(
size: number,
options: GeneratePrimeOptionsBigInt,
callback: (err: Error | null, prime: bigint) => void,
): void;
export function generatePrime(
size: number,
options: GeneratePrimeOptionsArrayBuffer,
callback: (err: Error | null, prime: ArrayBuffer) => void,
): void;
export function generatePrime(
size: number,
options: GeneratePrimeOptions,
callback: (err: Error | null, prime: ArrayBuffer | bigint) => void,
): void;
export function generatePrimeSync(size: number): ArrayBuffer;
export function generatePrimeSync(size: number, options: GeneratePrimeOptionsBigInt): bigint;
export function generatePrimeSync(
size: number,
options: GeneratePrimeOptionsArrayBuffer,
): ArrayBuffer;
export function generatePrimeSync(
size: number,
options: GeneratePrimeOptions,
): ArrayBuffer | bigint;
export interface CheckPrimeOptions {
checks?: number | undefined;
}
export function checkPrime(
value: LargeNumberLike,
callback: (err: Error | null, result: boolean) => void,
): void;
export function checkPrime(
value: LargeNumberLike,
options: CheckPrimeOptions,
callback: (err: Error | null, result: boolean) => void,
): void;
export function checkPrimeSync(candidate: LargeNumberLike, options?: CheckPrimeOptions): boolean;
export interface ScryptOptions {
N?: number;
r?: number;
@@ -3512,7 +3568,8 @@ declare module "child_process" {
* killSignal at the deadline and the result carries error: ETIMEDOUT +
* the signal, never a throw), killSignal (a signal NAME literal),
* stdio ("ignore"/"inherit"/"pipe" or a 3-tuple of those — captures
* read "" for non-piped streams), and windowsHide (a POSIX no-op). */
* read "" for non-piped streams), env (replaces the child's
* environment), and windowsHide (a POSIX no-op). */
export function spawnSync(
command: string,
args?: string[],
@@ -3521,6 +3578,7 @@ declare module "child_process" {
timeout?: number;
killSignal?: string;
stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit")[];
env?: { [k: string]: string | undefined };
windowsHide?: boolean;
},
): SpawnSyncReturns;
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.7",
"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";
@@ -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}`);
@@ -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;
}
@@ -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));
}
}
@@ -177,6 +177,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", () => {
@@ -358,6 +363,63 @@ test("index temps of a version's counter are integers in the fast copy", () => {
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(),
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 => {
@@ -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";
@@ -23,6 +23,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;
@@ -51,6 +69,9 @@ export interface BytesLoopVersion {
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>;
@@ -83,7 +104,7 @@ export interface BytesLoopFacts {
}
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;
@@ -116,7 +137,7 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"])
* 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 {
@@ -155,10 +176,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
@@ -337,6 +358,44 @@ export function matchBytesLoopVersion(
return sum + Math.max(Math.abs(r.min), Math.abs(r.max));
}, 0);
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 }[] = [];
const receiverIndex = new Map<string, number>();
const fixed = new Map<number, { check: FixedCheck; nodes: Node[] }>();
@@ -356,6 +415,11 @@ export function matchBytesLoopVersion(
// 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 => {
const ref = root(array);
if (!ref) return;
@@ -390,11 +454,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);
@@ -416,6 +499,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) => {
@@ -431,8 +515,18 @@ export function matchBytesLoopVersion(
});
const fixedKept = [...fixed.values()].slice(0, MAX_CHECKS);
const affineKept = affineGroups.slice(0, MAX_CHECKS - fixedKept.length);
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 && ownedKept.length === 0) return null;
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>();
@@ -477,6 +571,39 @@ export function matchBytesLoopVersion(
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]));
@@ -500,6 +627,12 @@ 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,
@@ -507,6 +640,307 @@ export function matchBytesLoopVersion(
};
}
/** 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 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++`) 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 === "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 every view it proves thread-owned has no shared
* storage, and to `slow` otherwise (including a counted loop whose
@@ -553,6 +987,39 @@ export function emitBytesLoopVersionGuard(
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⌋ *
@@ -599,7 +1066,7 @@ export function emitBytesLoopVersionGuard(
for (const term of check.terms) {
const value = host.emitExpr(term.expr);
if (version.guarded.has(term.key)) {
const tested = emitGuardedTerm(host, value.name);
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}`);
@@ -653,10 +1120,15 @@ export function emitBytesLoopVersionGuard(
else B.condBr(valid, fast, slow);
}
/** Tests that a guarded term is an exact integer within GUARDED_TERM (not
* -0; NaN fails the ordered comparisons) and gives its i64. The integer is
* frozen, so it is a fixed value even when the test fails. */
function emitGuardedTerm(host: LlvmEmitterContext, value: string): { ok: string; integer: string } {
/** 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(),
@@ -668,8 +1140,8 @@ function emitGuardedTerm(host: LlvmEmitterContext, value: string): { ok: string;
backBits = B.tmp(),
exact = B.tmp(),
ok = B.tmp();
B.line(`${low} = fcmp oge double ${value}, ${f64Lit(GUARDED_TERM.min)}`);
B.line(`${high} = fcmp ole double ${value}, ${f64Lit(GUARDED_TERM.max)}`);
B.line(`${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}`);
@@ -1,5 +1,14 @@
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,
@@ -222,12 +231,13 @@ 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 }`);
@@ -332,6 +342,99 @@ test("references between ProgramLifetime instances are 32-bit indexes into their
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 = {
+13 -9
View File
@@ -703,11 +703,13 @@ 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,
@@ -751,10 +753,12 @@ export function assignCompactLayouts(
);
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;
@@ -860,9 +864,9 @@ export function emitClassShapes(
nullable?: NullableRefFields,
int32?: Int32Slots,
): { typeDefs: string[]; defs: string[] } {
// Whether compact instances live in the program-lifetime region (any
// field holds a 32-bit reference to one; emitter.ts assignCompactLayouts).
const regionRefs = [...metaMap.values()].some((m) => m.compressed.some(Boolean));
// 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);
@@ -252,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;
@@ -27,7 +27,10 @@ import {
storeArrCount,
} from "./common.js";
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import { compactRefLoadLines, compactRefStoreLines } from "./classes.js";
import { elemAccess } from "./shapes.js";
import { exactInteger, widenInteger } from "./integer-values.js";
import { integerOrNaNIndex } from "./wrapping-sums.js";
const SCR_ARR_VALUE = 1;
@@ -75,12 +78,17 @@ function emitDenseGuard(
const B = host.B;
const size = host.sizeType;
const integer = exactInteger(host, index, indexExpr);
const nanIndex = integer ? null : integerOrNaNIndex(host, index, indexExpr);
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);
} 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 +217,40 @@ 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, 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 four, every other element kind an 8-byte slot. */
function slotType(kind: SlotKind): "i8" | "i32" | "i64" {
return kind === "bool" ? "i8" : kind === "cref" ? "i32" : "i64";
}
/** Bytes per dense slot (scr_arr_slot_width). */
export function slotWidth(acc: ArrayAccess): number {
return acc === "bool" ? 1 : 8;
function slotWidth(kind: SlotKind): number {
return kind === "bool" ? 1 : kind === "cref" ? 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));
}
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;
@@ -300,6 +322,7 @@ export function emitDenseArrayGet(
const B = host.B;
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
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();
@@ -322,7 +345,7 @@ export function emitDenseArrayGet(
slow,
true,
valueLabel,
slotWidth(acc),
slotWidth(kind),
proven,
induction,
);
@@ -333,7 +356,7 @@ 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 fastLabel = valueLabel;
if (acc === "f64") {
@@ -347,7 +370,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);
@@ -448,6 +477,7 @@ export function emitDenseArraySet(
): void {
const B = host.B;
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
const kind = slotKind(host, acc, element);
const setter = `scr_arr_set_${acc}`;
host.declare(`declare void @${setter}(ptr, double, ${acc === "bool" ? "i1 zeroext" : ty})`);
const call = (attrs: string): void =>
@@ -467,7 +497,7 @@ export function emitDenseArraySet(
slow,
false,
filledLabel,
slotWidth(acc),
slotWidth(kind),
proven,
);
const { offset, len } = guard;
@@ -478,15 +508,20 @@ export function emitDenseArraySet(
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 statePtr = statePointer(host, guard);
const valuePtr = valuePointer(host, guard, acc);
const valuePtr = valuePointer(host, guard, kind);
let oldState: string | null = null;
let old: string | null = null;
if (acc === "ref") {
// Compact instances are never released: a replaced one needs nothing.
if (kind === "ref") {
oldState = B.tmp();
old = B.tmp();
B.line(`${oldState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
@@ -537,7 +572,7 @@ export function emitDenseArraySet(
storeArrCount(host, nextLen, lenPtr, host.fieldAliasAttachment(lenPtr));
B.br(storedLabel);
B.startBlock(storedLabel);
if (acc === "ref" && oldState && old) {
if (kind === "ref" && oldState && old) {
const hadValue = B.tmp();
B.line(`${hadValue} = icmp eq i8 ${oldState}, ${SCR_ARR_VALUE}`);
const releaseLabel = B.newLabel("arr.set.release");
@@ -549,9 +584,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 +613,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,16 +652,23 @@ 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") {
// 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;
}
B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
B.line(`store i8 0, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
@@ -648,10 +692,12 @@ export function emitDenseArrayPush(
host: LlvmEmitterContext,
array: string,
acc: ArrayAccess,
element: IrType | null,
value: string,
): string {
const B = host.B;
const size = host.sizeType;
const kind = element === null ? acc : slotKind(host, acc, element);
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
const pusher = `scr_arr_push_${acc}`;
host.declare(`declare double @${pusher}(ptr, ${acc === "bool" ? "i1 zeroext" : ty})`);
@@ -685,16 +731,20 @@ 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 === "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)}`);
}
+91
View File
@@ -196,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[] {
@@ -385,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 => {
@@ -442,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;
}
@@ -840,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();
@@ -1200,6 +1252,7 @@ export class LlDyn {
``,
);
if (wrapped) this.emitVisitWrapper(name, t, recursive ? "match" : "visit");
this.host.journalBuilt?.();
return name;
}
@@ -1293,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":
@@ -1364,6 +1418,7 @@ export class LlDyn {
`@${name} = internal constant { ${S}, ${S}, ptr, ptr, ptr, ptr, ptr, ptr, ${S} } { ${body} }`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -1453,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(
@@ -1460,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;
}
@@ -1478,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)`);
@@ -1504,6 +1562,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -1519,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;
@@ -1541,6 +1601,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
if (isRefCounted(t) && t.kind !== "dyn") {
@@ -2537,6 +2598,7 @@ export class LlDyn {
``,
);
if (recursive || memo) this.emitVisitWrapper(name, t, recursive ? "build" : "memo");
this.host.journalBuilt?.();
return name;
}
@@ -2551,6 +2613,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;
@@ -3157,6 +3220,7 @@ export class LlDyn {
``,
);
}
this.host.journalBuilt?.();
return name;
}
@@ -3173,6 +3237,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)`);
@@ -3250,6 +3315,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -3262,6 +3328,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)`);
@@ -3566,6 +3633,7 @@ export class LlDyn {
``,
);
}
this.host.journalBuilt?.();
return name;
}
@@ -3577,6 +3645,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();
@@ -3682,6 +3751,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -3695,6 +3765,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)`);
@@ -3732,6 +3803,7 @@ export class LlDyn {
"}",
"",
);
this.host.journalBuilt?.();
return name;
}
@@ -3741,6 +3813,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");
@@ -4134,6 +4207,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -4145,6 +4219,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)`);
@@ -4199,6 +4274,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -4210,6 +4286,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)`);
@@ -4414,6 +4491,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -4445,6 +4523,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[] = [];
@@ -4594,6 +4673,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -4604,6 +4684,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)`);
@@ -4618,6 +4699,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -4640,11 +4722,15 @@ export class LlDyn {
(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`);
@@ -4676,6 +4762,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -4737,6 +4824,7 @@ export class LlDyn {
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})`);
@@ -4909,6 +4997,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -4920,6 +5009,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)`);
@@ -5022,6 +5112,7 @@ export class LlDyn {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
}
@@ -54,6 +54,8 @@ 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();
@@ -80,6 +82,7 @@ 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();
@@ -215,6 +218,7 @@ 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
@@ -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;
}
+235 -40
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,
@@ -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,7 +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.usesCompactRefs ? [`declare void @scr_plr_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()`] : []),
@@ -727,6 +875,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
out.push(`@${sym} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr } { ${body} }`);
}
if (emitter.mapOpsConsts.size > 0) out.push(``);
for (const sym of emitter.methodIdSlots.values()) out.push(`@${sym} = internal global i32 -1`);
if (emitter.methodIdSlots.size > 0) out.push(``);
for (const line of ffiCallbacks.globals) out.push(line);
if (ffiCallbacks.globals.length > 0) out.push(``);
@@ -840,7 +990,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
` call void @scr_init()`,
// Compressed fields hold addresses in the program-lifetime region, which
// is reserved below 32 GiB before compiled code runs.
...(emitter.usesCompactRefs ? [` call void @scr_plr_init()`] : []),
...(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()`] : []),
@@ -1293,6 +1443,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
@@ -1301,36 +1463,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`,
`}`,
``,
);
@@ -37,7 +37,7 @@ import { InternalCompilerError } from "../../errors.js";
import { emitDenseArrayGet, emitDenseArrayPop, emitDenseArraySet } from "./dense-array-access.js";
import { emitBorrowedInput } from "./borrowed-inputs.js";
import { f64Lit, llvmCommentText, loadArrCount, sizeToDouble } from "./common.js";
import { RESULT_DST_ID, RESULT_ENTRY_PREFIX } from "./record-results.js";
import { resultSlotStore } from "../result-slots.js";
import {
matchIntegerArrayForLoop,
matchIntegerBytesForLoop,
@@ -51,7 +51,7 @@ import {
emitCountedLoopSafety,
emitPairedLoopSafety,
} from "./counted-loops.js";
import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js";
import { emitBytesLoopVersionGuard, type BytesLoopVersion } from "./bytes-loop-versions.js";
import { mangleLocal } from "../mangle.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlValue } from "./expr-context.js";
@@ -474,7 +474,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
B.startBlock(pop);
const elem = s.arr.type.elem;
const acc = elemAccess(elem);
const raw = emitDenseArrayPop(emitter, arr.name, acc, call, call);
const raw = emitDenseArrayPop(emitter, arr.name, acc, elem, call, call);
if (acc === "ref" && isRefCounted(elem)) {
const old = B.tmp();
B.line(`${old} = inttoptr i64 ${raw} to ptr`);
@@ -574,13 +574,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
? emitter.classFieldPtr(obj.name, s.className, s.field, "compressed")
: emitter.recordFieldPtr(obj.name, s.shapeId, s.field);
// A result-slot entry fills zeroed caller storage, each field once.
if (
s.kind === "recordSet" &&
s.obj.kind === "varRef" &&
s.obj.localId === RESULT_DST_ID &&
emitter.currentFunctionName.startsWith(RESULT_ENTRY_PREFIX)
)
emitter.initField(ptr, type, inline, v);
if (resultSlotStore(emitter.currentFunctionName, s)) emitter.initField(ptr, type, inline, v);
else emitter.replaceField(ptr, type, inline, v);
if (
s.kind === "fieldSet" &&
@@ -787,7 +781,12 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
const trueLabel = B.newLabel("if.t");
const joinLabel = B.newLabel("if.j");
const falseLabel = s.else_ ? B.newLabel("if.f") : joinLabel;
B.condBr(cond, trueLabel, falseLabel);
// A running maximum or minimum is rarely updated; the weights keep
// the branch, where a select would make each element wait on the last.
const weights = emitter.runningExtrema.has(s)
? `, !prof !{!"branch_weights", i32 1, i32 1000}`
: "";
B.condBr(cond, trueLabel, falseLabel, weights);
B.startBlock(trueLabel);
emitter.emitBlock(s.then);
B.br(joinLabel);
@@ -805,6 +804,16 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
emitCallbackVersionedLoop(emitter, s, callbacks);
break;
}
const version =
LOOPS.of(emitter).bytesLoopVersionsEnabled &&
LOOPS.of(emitter).bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH &&
!LOOPS.of(emitter).bytesLoopVersioning.has(s)
? emitter.matchBytesLoopVersion(s, null)
: null;
if (version) {
emitVersionedLoop(emitter, s, version);
break;
}
const workerBudget = emitter.workerLoopBudget(s);
const le = B.newLabel("loop.e");
emitter.emitLoopCopies(s, true, le, (exit) => {
@@ -1002,35 +1011,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
: countedLoop;
const version = versionable ? emitter.matchBytesLoopVersion(s, versionCounted) : null;
if (version) {
const fast = B.newLabel("bytes.version.fast"),
slow = B.newLabel("bytes.version.slow"),
done = B.newLabel("bytes.version.done");
emitBytesLoopVersionGuard(emitter, version, fast, slow);
const originalBounds = emitter.bytesBounds,
originalOwned = emitter.bytesOwned,
originalRanges = emitter.integerRanges;
LOOPS.of(emitter).bytesLoopVersioning.add(s);
try {
B.startBlock(slow);
emitter.emitStmt(s);
B.br(done);
B.startBlock(fast);
emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]);
if (version.owned.size > 0)
emitter.bytesOwned = new Set([...originalOwned, ...version.owned]);
if (version.ranges.size > 0)
emitter.integerRanges = new Map([...originalRanges, ...version.ranges]);
if (version.counted?.guarded) LOOPS.of(emitter).provenCountedGuards.add(s);
emitter.emitStmt(s);
B.br(done);
} finally {
emitter.bytesBounds = originalBounds;
emitter.bytesOwned = originalOwned;
emitter.integerRanges = originalRanges;
LOOPS.of(emitter).provenCountedGuards.delete(s);
LOOPS.of(emitter).bytesLoopVersioning.delete(s);
}
B.startBlock(done);
emitVersionedLoop(emitter, s, version);
break;
}
let countedJoin: string | null = null;
@@ -2044,6 +2025,45 @@ const SPECULATION_BAILS = 8;
* when speculative-integers.ts plans a fast integer copy. Both copies
* share the loop's setup and exit; the ordinary copy comes first so the
* fast copy is only emitted when every resume point exists. */
/** A typed-array loop version: the guard, then the checked copy, which
* runs whenever the guard fails, and the copy whose accesses the guard
* proves in bounds (or thread-owned). */
function emitVersionedLoop(
emitter: LlEmitter,
s: IrStmt & { kind: "for" | "while" },
version: BytesLoopVersion,
): void {
const B = emitter.B;
const fast = B.newLabel("bytes.version.fast"),
slow = B.newLabel("bytes.version.slow"),
done = B.newLabel("bytes.version.done");
emitBytesLoopVersionGuard(emitter, version, fast, slow);
const originalBounds = emitter.bytesBounds,
originalOwned = emitter.bytesOwned,
originalRanges = emitter.integerRanges;
LOOPS.of(emitter).bytesLoopVersioning.add(s);
try {
B.startBlock(slow);
emitter.emitStmt(s);
B.br(done);
B.startBlock(fast);
emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]);
if (version.owned.size > 0) emitter.bytesOwned = new Set([...originalOwned, ...version.owned]);
if (version.ranges.size > 0)
emitter.integerRanges = new Map([...originalRanges, ...version.ranges]);
if (version.counted?.guarded) LOOPS.of(emitter).provenCountedGuards.add(s);
emitter.emitStmt(s);
B.br(done);
} finally {
emitter.bytesBounds = originalBounds;
emitter.bytesOwned = originalOwned;
emitter.integerRanges = originalRanges;
LOOPS.of(emitter).provenCountedGuards.delete(s);
LOOPS.of(emitter).bytesLoopVersioning.delete(s);
}
B.startBlock(done);
}
export function emitLoopCopies(
emitter: LlEmitter,
loop: Extract<IrStmt, { kind: "while" | "doWhile" | "for" }>,
@@ -2099,8 +2119,7 @@ export function emitLoopCopies(
// count. After SPECULATION_BAILS bailing entries in a row, the site runs
// the ordinary copy only. The count is a hint: both copies give the same
// results, and a race between workers loses at most an update.
const bails = `@scr.spec.bails.${emitter.speculativeLoopSites++}`;
emitter.declare(`${bails} = internal global i8 0`);
const bails = emitter.speculativeLoopSite();
const count = B.tmp(),
trying = B.tmp();
B.line(`${count} = load atomic i8, ptr ${bails} unordered, align 1`);
+463 -43
View File
@@ -70,7 +70,15 @@ import {
import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
import { addRecordResultEntries } from "./record-results.js";
import { specializeCallbackCalls } from "../../ir/callback-specialization.js";
import {
type ChunkResult,
EmitJournal,
registerReplay,
renameChunk,
replayJournal,
} from "./emit-journal.js";
import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
import { versionLoopBoundParameters } from "../../ir/parameter-versions.js";
import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
import { INT32_RANGE, type IntegerRange, type IntegerRanges } from "../../ir/integer-ranges.js";
import { unshareNodes } from "../../ir/unshare.js";
@@ -174,6 +182,7 @@ import { emitDynamicExpr } from "./expr-dynamic.js";
import { emitIntrinsicExpr, emitSerializationExpr, emitAsyncExpr } from "./expr-async.js";
import { PublishWalkers } from "./publish.js";
import { computeImmutableFields, stableGlobals, type ImmutableFields } from "../immutability.js";
import { computeCycleMutability, hierarchyUnitOfClass } from "../cycle-mutability.js";
import { computePublishedTypes, type PublishedTypes } from "../publication.js";
import { emitExpr } from "./expr-dispatch.js";
import { LOOPS } from "./loop-state.js";
@@ -392,9 +401,32 @@ export interface LlScopeEntry {
lazy?: boolean;
}
/** Process primitives for forked emission workers. */
export interface ForkPrimitives {
/** 0 in the worker, the worker's pid in the parent, -1 on failure. */
fork(): number;
/** Ends the worker without running exit handlers. */
exit(code: number): void;
/** The worker's exit status, 128 plus a signal number, or -1. */
wait(pid: number): number;
}
export interface LlvmTargetOptions {
/** C ABI target; omitted direct emission uses the host ABI. */
targetTriple?: string;
/** Emit the function bodies in this many contiguous ranges, each by its
* own emitter, and merge them in function order (emit-journal.ts). The
* output is the same as one emitter's. Debug builds (`debugSources`)
* emit in one emitter. */
functionChunks?: number;
/** Emit the function bodies of a large module on this many forked worker
* processes at a time (emit-module.ts), forked with `forkPrimitives`. The
* output is the same as one emitter's. Debug builds (`debugSources`)
* emit in one emitter. */
functionWorkers?: number;
/** The host's process primitives, where it can fork (the native
* compiler's). */
forkPrimitives?: ForkPrimitives | undefined;
/** Exact frontend sources for dev-build locations and variable metadata. */
debugSources?: ReadonlyMap<string, string>;
/** Pointer width of the target C ABI. Native targets are 64-bit today. */
@@ -442,6 +474,14 @@ export function lowRegionTarget(platform: string, sanitize: boolean): boolean {
return platform === "linux" && !sanitize;
}
/** Function versions on loop-bounding number parameters
* (ir/parameter-versions.ts). Library builds have no int32 entries, and a
* module that observes call stacks could see the version's frame. */
function versionParameters(mod: IrModule): IrModule {
if (mod.lib !== undefined || observesStacks(mod)) return mod;
return versionLoopBoundParameters(mod, mangleFunction);
}
export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string {
// Facts are keyed by node identity: emit the same module whether it was
// lowered in memory or read back from its serialized form.
@@ -449,20 +489,22 @@ export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {})
// Keep source storage intact for debugger inspection in dev builds.
return new LlEmitter(
options.debugSources === undefined
? addRecordResultEntries(
scalarizeNumericRecords(
specializeCallbackCalls(
specializeNumericCalls(
foldConstantGlobals(
copyStableCaptures(
resolveSelfCaptures(
hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
? versionParameters(
addRecordResultEntries(
scalarizeNumericRecords(
specializeCallbackCalls(
specializeNumericCalls(
foldConstantGlobals(
copyStableCaptures(
resolveSelfCaptures(
hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
),
),
),
mangleFunction,
),
mangleFunction,
),
mangleFunction,
),
),
)
@@ -481,20 +523,22 @@ export function emitLlvmModuleSource(
const mod = unshareNodes(input);
const parts = new LlEmitter(
options.debugSources === undefined
? addRecordResultEntries(
scalarizeNumericRecords(
specializeCallbackCalls(
specializeNumericCalls(
foldConstantGlobals(
copyStableCaptures(
resolveSelfCaptures(
hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
? versionParameters(
addRecordResultEntries(
scalarizeNumericRecords(
specializeCallbackCalls(
specializeNumericCalls(
foldConstantGlobals(
copyStableCaptures(
resolveSelfCaptures(
hoistFinalFieldReads(freezeInitOnlyGlobals(dropUnwrittenPropertyBags(mod))),
),
),
),
mangleFunction,
),
mangleFunction,
),
mangleFunction,
),
),
)
@@ -643,9 +687,11 @@ export class LlEmitter {
* 32-bit references to compact class instances (LlClassMeta.compressed):
* loadField/loadRef decode them and storeField/storeRef encode them. */
readonly compressedFieldPtrs = new Set<string>();
/** Whether a class has compressed fields: main reserves the region before
* compiled code runs (scr_plr_init). */
usesCompactRefs = false;
/** Compact class instances live in the program-lifetime region below
* 32 GiB (LlvmTargetOptions.lowRegion, and the program has compact
* classes): main reserves it before compiled code runs (scr_plr_init), and
* fields and arrays of compactRefType hold 32-bit references. */
compactRegion = false;
/** Native class fields with different property names cannot occupy the
* same slot of one object. Inherited views of the same property share a
@@ -662,6 +708,7 @@ export class LlEmitter {
tag = 2 + this.fieldAliasTags.size * 2;
this.fieldAliasTags.set(field, tag);
}
this.journal?.entry("tag", field, tag);
this.fieldPointerTags.set(ptr, tag);
}
@@ -674,6 +721,7 @@ export class LlEmitter {
tag = 2 + this.fieldAliasTags.size * 2;
this.fieldAliasTags.set(category, tag);
}
this.journal?.entry("tag", category, tag);
return `, !tbaa !${tag}`;
}
@@ -784,6 +832,10 @@ export class LlEmitter {
readonly cstrs = new Map<string, { sym: string; len: number }>();
/** Interned ScrMapOps descriptors (scr_map.h): initializer → symbol. */
readonly mapOpsConsts = new Map<string, string>();
/** Method-id slots of checked-dynamic method calls
* (scr_dyn_invoke_prepared_id): method name → symbol of an i32 the
* runtime fills with the name's id on the first call. */
readonly methodIdSlots = new Map<string, string>();
/** Type-directed walker functions (JSON serializers, the indent
* rewriter, union ToString/join) — interned per typeKey/unionId, defs
* flushed with the shape helpers. */
@@ -833,6 +885,9 @@ export class LlEmitter {
readonly callLifetimes: CallLifetimes;
readonly constantCallbacks: Map<string, Map<string, string>>;
currentConstantCallbacks: ReadonlyMap<string, string> = new Map();
/** The current function's running maximum and minimum updates, whose
* branches are weighted as rarely taken (running-extrema.ts). */
runningExtrema: ReadonlySet<IrStmt> = new Set();
/** Callback bindings whose closures an enclosing callback loop version
* proved to hold their known function: calls take the direct entry with
* no fallback (emitCallbackVersionedLoop). */
@@ -1132,6 +1187,9 @@ export class LlEmitter {
int32Elements: ((arr: IrExpr) => boolean) | null = null;
/** Sums whose result only feeds 32-bit conversions (wrapping-sums.ts). */
wrappingSums = new Set<IrExpr>();
/** Number locals of the current function that hold exact integers or NaN
* (findIntegerOrNaNLocals in wrapping-sums.ts). */
integerOrNaNLocals: ReadonlyMap<string, IntegerRange> = new Map();
bytesBounds: ReadonlySet<IrExpr | IrStmt> = new Set();
/** Worker programs: typed-array accesses whose views an enclosing version
* guard proved thread-owned. */
@@ -1203,13 +1261,29 @@ export class LlEmitter {
readonly chainSlots = new Map<string, LlValue>();
logArgSlots = 0;
readonly stackTraces: boolean;
/** The options this emitter was built with (chunk emitters reuse them). */
readonly targetOptions: LlvmTargetOptions;
/** Records the module-level effects of emitting a range of functions,
* which the emitter that merges the range replays (emit-journal.ts). Null
* when this emitter emits the whole module itself. */
journal: EmitJournal | null;
/** The emitter's hosts forward to `journal` (journal set, or workers). */
private readonly journaled: boolean;
/** Stack-trace mode: functions no capture can include (no frame push). */
readonly unframedFunctions: ReadonlySet<string>;
constructor(
readonly mod: IrModule,
options: LlvmTargetOptions,
journal: EmitJournal | null = null,
) {
this.targetOptions = options;
this.journal = journal;
this.journaled =
journal !== null ||
((options.functionWorkers ?? 1) > 1 &&
options.forkPrimitives !== undefined &&
options.debugSources === undefined);
this.stackTraces = observesStacks(mod);
this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined);
this.mockTimers = this.runtimeFeatures.mockTimers;
@@ -1292,8 +1366,10 @@ export class LlEmitter {
// and per async fiber; WASI, Windows and library builds have none.
const triple = (options.targetTriple ?? "").toLowerCase();
const windows = triple === "" ? process.platform === "win32" : triple.includes("windows");
this.publishedTypes = computePublishedTypes(mod);
const mt = computeMayThrow(mod, {
stackChecks: !this.wasi && mod.lib === undefined && !windows,
published: this.publishedTypes,
});
this.mayThrow = mt.fns;
this.workerEntryPolls = mt.workerEntryPolls ?? null;
@@ -1302,11 +1378,12 @@ export class LlEmitter {
// runtime moves deep prefix nests to a fresh stack using the guard,
// which Windows lacks.
this.stacklessAsyncAllowed = this.stacklessGenerators && mod.lib === undefined && !windows;
this.publishedTypes = computePublishedTypes(mod);
this.unframedFunctions = this.stackTraces
? captureFreeFunctions(mod, this.stackChecks, this.publishedTypes)
: new Set();
this.immutableFields = computeImmutableFields(mod, dynamicStores);
// One field-mutability answer serves immutable fields and traced shapes.
const cycleMutability = computeCycleMutability(mod, hierarchyUnitOfClass(mod), dynamicStores);
this.immutableFields = computeImmutableFields(mod, dynamicStores, cycleMutability);
this.constructorFunctions = new Set((mod.classes ?? []).map((c) => `%${c.name}.constructor`));
this.indirectMayThrow = mt.indirect || mod.workers === true;
for (const cls of mod.classes ?? []) {
@@ -1431,7 +1508,7 @@ export class LlEmitter {
this.receiverReaders = new AmbientReceiverReaders(this.fnByName);
this.stackCaptures = new StackCaptures(this.fnByName, this.callLifetimes, this.stackCallbacks);
this.lazyCaptures = new LazyCaptures(this.fnByName);
const traced = computeTraced(mod, dynamicStores);
const traced = computeTraced(mod, dynamicStores, cycleMutability);
this.tracedShapes = traced.shapes;
this.tracedUnions = traced.unions;
// A shape union's value is whichever record it holds, and the collector
@@ -1456,6 +1533,22 @@ export class LlEmitter {
}
}
this.shapeHost = {
// Journal hooks only on emitters that can emit ranges (chunk
// emitters, and emitters whose workers take a journal after the
// fork), so the host's optional calls cost nothing otherwise.
journalAllocated: !this.journaled
? undefined
: (name, op, args) => this.journal?.allocated(name, op, args),
journalBuilt: !this.journaled ? undefined : () => this.journal?.built(),
journalOpened: !this.journaled ? undefined : (op) => this.journal?.opened(op),
journalUsed: !this.journaled
? undefined
: (name, op, args) => this.journal?.used(name, op, args),
journalEntry: !this.journaled
? undefined
: (table, key, value) => this.journal?.entry(table, key, value),
compactArrayElem: (elem) => this.compactArrayElem(elem),
compactRegion: () => this.compactRegion,
sizeType: this.sizeType,
cycleBufOffset: this.cycleBufOffset,
tracedShapes: this.tracedShapes,
@@ -1585,22 +1678,19 @@ export class LlEmitter {
? (cls, i, isCompact) => {
const f = cls.fields[i]!;
if (this.inlineUnions.classField(cls.name, f.name)) return false;
const t = f.type;
if (t.kind === "object") return isCompact(t.className);
if (t.kind !== "union") return false;
const single = this.nullableUnions.single(t.unionId);
if (single !== null)
return (
single.nullTag < 0 &&
single.arm.kind === "object" &&
isCompact(single.arm.className)
);
const nullable = this.nullableFields.get(cls.name, f.name);
if (this.compactRefType(f.type, isCompact)) return true;
const nullable =
f.type.kind === "union" && this.nullableUnions.single(f.type.unionId) === null
? this.nullableFields.get(cls.name, f.name)
: null;
return nullable !== null && isCompact(nullable.arm.className);
}
: undefined,
);
this.usesCompactRefs = [...this.classMeta.values()].some((m) => m.compressed.some(Boolean));
this.compactRegion =
mod.lib === undefined &&
options.lowRegion === true &&
[...this.classMeta.values()].some((m) => m.compact);
for (const [name, rec] of RUNTIME_ERROR_CLASSES) {
const meta = this.classMeta.get(name);
if (!meta) break; // hand-written IR without the builtin defs: no stamps
@@ -2212,7 +2302,7 @@ export class LlEmitter {
B.condBr(thrown, rejected, clean);
B.startBlock(clean);
if (acc === "ref") this.toHeap(value);
this.arrPush(values, acc, value.name);
this.arrPush(values, acc, value.name, elem);
B.br(next);
B.startBlock(rejected);
const promise = B.tmp();
@@ -2328,6 +2418,7 @@ export class LlEmitter {
};
this.literals.set(text, lit);
}
this.journal?.entry("literal", text, lit);
const name = `@${lit.sym}`;
this.immortalValues.add(name);
return name;
@@ -2338,6 +2429,7 @@ export class LlEmitter {
literalEquals(text: string): string {
const literal = this.internLiteral(text);
const name = `@sc_streq_${literal.slice("@sc_lit_".length)}`;
this.journal?.entry("literalEquality", text, name);
if (!this.literalEqualities.has(text)) {
this.literalEqualities.set(text, name);
if (Buffer.byteLength(text, "utf8") > INLINE_LITERAL_BYTES) {
@@ -2364,6 +2456,7 @@ export class LlEmitter {
lit = { sym: this.contentSymbol("sc_bigint_", key), value };
this.bigintLiterals.set(key, lit);
}
this.journal?.entry("bigint", key, { sym: lit.sym, value: lit.value.toString() });
const name = `@${lit.sym}`;
this.immortalValues.add(name);
return name;
@@ -2377,9 +2470,20 @@ export class LlEmitter {
c = { sym: this.contentSymbol("sc_cs_", text), len: Buffer.byteLength(text, "utf8") };
this.cstrs.set(text, c);
}
this.journal?.entry("cstr", text, c);
return `@${c.sym}`;
}
/** The method-id slot for a constant method name (methodIdSlots). */
methodIdSlot(name: string): string {
let sym = this.methodIdSlots.get(name);
if (!sym) {
sym = this.contentSymbol("sc_mid_", name);
this.methodIdSlots.set(name, sym);
}
return `@${sym}`;
}
needBadTag(): void {
this.needsBadTag = true;
}
@@ -2393,6 +2497,7 @@ export class LlEmitter {
sym = this.contentSymbol("sc_map_ops_", body);
this.mapOpsConsts.set(body, sym);
}
this.journal?.entry("mapOps", body, sym);
return `@${sym}`;
}
@@ -2419,6 +2524,7 @@ export class LlEmitter {
sym = `sc_unit_${this.unitInstances.size}`;
this.unitInstances.set(key, sym);
}
this.journal?.entry("unit", key, sym);
const name = `@${sym}`;
this.immortalValues.add(name);
return name;
@@ -2432,6 +2538,7 @@ export class LlEmitter {
sym = `sc_retype_guard_${this.retypeGuards.size}`;
this.retypeGuards.set(unionId, sym);
}
this.journal?.entry("retype", unionId, sym);
return `@${sym}`;
}
@@ -2453,6 +2560,7 @@ export class LlEmitter {
sym = `sc_absent_${this.absentInstances.size}`;
this.absentInstances.set(unionId, sym);
}
this.journal?.entry("absent", unionId, sym);
const name = `@${sym}`;
this.immortalValues.add(name);
return name;
@@ -2500,6 +2608,7 @@ export class LlEmitter {
}
declare(decl: string): void {
this.journal?.entry("decl", decl, null);
this.decls.add(decl);
}
@@ -3565,6 +3674,30 @@ export class LlEmitter {
return { ptr: p, type, inline: this.inlineUnions.classField(className, field), compressed };
}
/** Whether values of `t` are NULL or a compact class instance: `C`,
* `C | undefined` or `C | null` for a compact class `C`. `C | null |
* undefined` is not: its null is a sentinel outside the region. */
compactRefType(
t: IrType,
isCompact: (className: string) => boolean = (name) =>
this.classMeta.get(name)?.compact === true,
): boolean {
if (t.kind === "object") return isCompact(t.className);
if (t.kind !== "union") return false;
const single = this.nullableUnions.single(t.unionId);
return (
single !== null &&
single.nullTag < 0 &&
single.arm.kind === "object" &&
isCompact(single.arm.className)
);
}
/** Whether arrays of `elem` hold 32-bit references (SCR_ELEM_CREF). */
compactArrayElem(elem: IrType): boolean {
return this.compactRegion && this.compactRefType(elem);
}
/** Loads the reference a field pointer addresses: a pointer, or the
* decoded 32-bit reference of a compressed field (compressedFieldPtrs).
* `tagged`: the load carries the field's alias tags. */
@@ -3620,6 +3753,7 @@ export class LlEmitter {
const meta = this.classMetaOf(className);
this.classObjs.set(className, { nameSym: this.internLiteral(classJsName(meta.def)) });
}
this.journal?.entry("classObj", className, this.classObjs.get(className));
return mangleClassObj(className);
}
@@ -3829,7 +3963,7 @@ export class LlEmitter {
/** A binding's storage: a module global, a plain local slot, or a boxed
* local (the slot holds the capture BOX; access goes through it). */
matchBytesLoopVersion(
s: IrStmt & { kind: "for" },
s: IrStmt & { kind: "for" | "while" },
counted: IntegerCountedForLoop | null,
): ReturnType<typeof matchBytesLoopVersion> {
const fn = LOOPS.of(this).bytesLoopFunction;
@@ -4012,7 +4146,7 @@ export class LlEmitter {
const acc = boxAccess(t);
const cell = B.tmp();
B.line(`${cell} = ${arrNewCall(this.shapeHost, t, "1")} ; TDZ cell`);
this.arrPush(cell, acc === "bool" ? "bool" : "f64", value);
this.arrPush(cell, acc === "bool" ? "bool" : "f64", value, null);
this.declare(`declare void @scr_box_set_ref(ptr, ptr)`);
B.line(`call void @scr_box_set_ref(ptr ${box}, ptr ${cell})`);
}
@@ -4327,6 +4461,189 @@ export class LlEmitter {
return emitFunction(this, fn);
}
/** What this range emitter (chunkEmitter) hands the merging emitter, as
* plain data a worker process can write: its journal and the sets and
* flags its functions set. */
chunkResult(): ChunkResult {
return {
entries: this.journal!.entries,
flags: {
usesCoroutines: this.usesCoroutines,
usesTimers: this.usesTimers,
usesGenSlots: this.usesGenSlots,
needsUnionNone: this.needsUnionNone,
needsNullableAbsent: this.needsNullableAbsent,
needsOom: this.needsOom,
needsBadTag: this.needsBadTag,
needsBadKey: this.needsBadKey,
needsRetainBox: this.needsRetainBox,
},
publishedNamesSym: this.publishedNamesSym,
rcHelpers: this.rcHelpers === null ? [] : [...this.rcHelpers],
immortalValues: [...this.immortalValues],
fnValues: [...this.fnValues],
arraySearchKinds: [...this.arraySearchKinds],
recordCloneShapes: [...this.recordCloneShapes],
awaitCallees: [...this.awaitCallees],
adoptCallees: [...this.adoptCallees],
directAwaitCallees: [...this.directAwaitCallees],
inlineAwaitCallees: [...this.inlineAwaitCallees],
};
}
/** Merges a range emitter's result (chunkResult) into this emitter, in
* the range's order, and answers the rename of the range's text to this
* emitter's numbered helpers. Ranges merge in function order. */
mergeChunk(chunk: ChunkResult): (text: string) => string {
const names = new Map<string, string>();
const tags = new Map<number, number>();
// The range's helpers and table entries first, in its order; then the
// sets and flags, which replayed definitions must find as serial
// emission left them (a helper set guards the declarations its first
// request adds).
replayJournal(this, chunk.entries, names, tags);
const flags = chunk.flags;
this.usesCoroutines ||= flags.usesCoroutines;
this.usesTimers ||= flags.usesTimers;
this.usesGenSlots ||= flags.usesGenSlots;
this.needsUnionNone ||= flags.needsUnionNone;
this.needsNullableAbsent ||= flags.needsNullableAbsent;
this.needsOom ||= flags.needsOom;
this.needsBadTag ||= flags.needsBadTag;
this.needsBadKey ||= flags.needsBadKey;
this.needsRetainBox ||= flags.needsRetainBox;
this.publishedNamesSym ??= chunk.publishedNamesSym;
if (this.rcHelpers !== null) for (const key of chunk.rcHelpers) this.rcHelpers.add(key);
for (const name of chunk.immortalValues) this.immortalValues.add(name);
for (const name of chunk.fnValues) this.fnValues.add(name);
for (const kind of chunk.arraySearchKinds) this.arraySearchKinds.add(kind as ArraySearchKind);
for (const shapeId of chunk.recordCloneShapes) this.recordCloneShapes.add(shapeId);
for (const name of chunk.awaitCallees) this.awaitCallees.add(name);
for (const name of chunk.adoptCallees) this.adoptCallees.add(name);
for (const name of chunk.directAwaitCallees) this.directAwaitCallees.add(name);
for (const name of chunk.inlineAwaitCallees) this.inlineAwaitCallees.add(name);
return renameChunk(names, tags);
}
/** Adds a content-named constant a chunk interned. Its symbol is this
* emitter's own: a digest collision retries in first-use order, which a
* chunk alone does not know. */
private addContentEntry<V extends { sym: string }>(
into: Map<string, V>,
key: string,
entry: V,
prefix: string,
names: Map<string, string>,
): void {
let own = into.get(key);
if (own === undefined) {
own = { ...entry, sym: this.contentSymbol(prefix, key) };
into.set(key, own);
}
if (own.sym !== entry.sym) names.set(entry.sym, own.sym);
}
/** Applies a chunk's table entry (EmitJournal.entry) at its place in the
* chunk's order: a key this emitter lacks is added, numbered after its
* own entries where the table numbers them, and the chunk's name for it
* is renamed to this emitter's. */
applyJournalEntry(
table: string,
key: string,
value: unknown,
names: Map<string, string>,
tags: Map<number, number>,
): void {
switch (table) {
case "decl":
this.decls.add(key);
return;
case "literal":
this.addContentEntry(
this.literals,
key,
value as { sym: string; len: number },
"sc_lit_",
names,
);
return;
case "literalEquality":
addEntry(this.literalEqualities, key, value as string);
return;
case "bigint":
{
const entry = value as { sym: string; value: string };
this.addContentEntry(
this.bigintLiterals,
key,
{ sym: entry.sym, value: BigInt(entry.value) },
"sc_bigint_",
names,
);
}
return;
case "cstr":
this.addContentEntry(
this.cstrs,
key,
value as { sym: string; len: number },
"sc_cs_",
names,
);
return;
case "mapOps": {
let own = this.mapOpsConsts.get(key);
if (own === undefined) {
own = this.contentSymbol("sc_map_ops_", key);
this.mapOpsConsts.set(key, own);
}
if (own !== value) names.set(value as string, own);
return;
}
case "classObj":
addEntry(this.classObjs, key, value as { nameSym: string });
return;
case "retainWrapper":
addEntry(this.nullableUnions.retainWrappers, key, value as string);
return;
case "unit":
return addNumbered(this.unitInstances, key, value as string, "sc_unit_", names);
case "absent":
return addNumbered(this.absentInstances, key, value as string, "sc_absent_", names);
case "retype":
return addNumbered(this.retypeGuards, key, value as string, "sc_retype_guard_", names);
case "slots":
return addNumbered(this.threadSlotsBlocks, key, value as string, "@sc_slots_", names);
case "regex":
return addNumberedSymbol(this.regexInstances, key, value as never, "sc_re_", names);
case "templateStrings":
return addNumberedSymbol(
this.templateStringsInstances,
key,
value as never,
"sc_tsa_",
names,
);
case "tag": {
let own = this.fieldAliasTags.get(key);
if (own === undefined) {
own = 2 + this.fieldAliasTags.size * 2;
this.fieldAliasTags.set(key, own);
}
if (own !== value) tags.set(value as number, own);
return;
}
default:
throw new InternalCompilerError(`llvm emitter bug: no journal table ${table}`);
}
}
/** An emitter for a range of this module's functions: the same facts,
* empty module-level tables, and a journal of what the range adds. */
chunkEmitter(): LlEmitter {
return new LlEmitter(this.mod, { ...this.targetOptions, functionChunks: 1 }, new EmitJournal());
}
/** The double-ABI entry of a function with an i32 entry: every caller
* that is not a direct call (closures, dispatch, adapters, the runtime)
* enters here. A pending exception passes through to its caller. */
@@ -5409,6 +5726,7 @@ export class LlEmitter {
sym = `@sc_slots_${this.threadSlotsBlocks.size}`;
this.threadSlotsBlocks.set(name, sym);
}
this.journal?.entry("slots", name, sym);
return sym;
}
@@ -5558,6 +5876,20 @@ export class LlEmitter {
if (this.publishWalkersCache !== null) return this.publishWalkersCache;
const byPre = [...this.classMeta.values()].sort((a, b) => a.pre - b.pre);
this.publishWalkersCache = new PublishWalkers({
// Journal hooks only on emitters that can emit ranges (chunk
// emitters, and emitters whose workers take a journal after the
// fork), so the host's optional calls cost nothing otherwise.
journalAllocated: !this.journaled
? undefined
: (name, op, args) => this.journal?.allocated(name, op, args),
journalBuilt: !this.journaled ? undefined : () => this.journal?.built(),
journalOpened: !this.journaled ? undefined : (op) => this.journal?.opened(op),
journalUsed: !this.journaled
? undefined
: (name, op, args) => this.journal?.used(name, op, args),
journalEntry: !this.journaled
? undefined
: (table, key, value) => this.journal?.entry(table, key, value),
declare: (decl) => this.declare(decl),
cstr: (text) => this.cstr(text),
sizeType: this.sizeType,
@@ -6044,12 +6376,27 @@ export class LlEmitter {
}
jsIcSite(): string {
return `@sc_js_ic_${this.jsIcSiteCount++}`;
const name = `@sc_js_ic_${this.jsIcSiteCount++}`;
this.journal?.site(name, "jsIcSite");
return name;
}
/** A fresh read-site cache for one constant-key dyn read. */
dynReadIcSite(): string {
return `@sc_dyn_read_ic_${this.dynReadIcSites++}`;
const name = `@sc_dyn_read_ic_${this.dynReadIcSites++}`;
this.journal?.site(name, "dynReadIcSite");
return name;
}
/** A fresh speculative loop site: the global that counts its bailing
* entries in a row. */
speculativeLoopSite(): string {
const name = `@sc_spec_bails_${this.speculativeLoopSites++}`;
this.journal?.site(name, "speculativeLoopSite");
this.journal?.opened("speculativeLoopSite");
this.declare(`${name} = internal global i8 0`);
this.journal?.built();
return name;
}
dynKind(d: string): string {
@@ -6162,8 +6509,8 @@ export class LlEmitter {
return tagInSet(this, uName, unionId, tags);
}
arrPush(arr: string, acc: "f64" | "bool" | "ref", value: string): string {
return arrPush(this, arr, acc, value);
arrPush(arr: string, acc: "f64" | "bool" | "ref", value: string, element: IrType | null): string {
return arrPush(this, arr, acc, value, element);
}
emitArrayCopyLoop(dst: string, src: string, acc: "f64" | "bool" | "ref"): void {
@@ -6451,3 +6798,76 @@ export function adoptingAwaitOf(value: IrExpr | null): ExprOf<"awaitExpr"> | nul
});
return found;
}
/** Adds `key` to `into` when it lacks it. */
function addEntry<V>(into: Map<string, V>, key: string, value: V): void {
if (!into.has(key)) into.set(key, value);
}
/** Adds `key` to a numbered table when it lacks it, numbering it after the
* table's own entries (`prefix` and its index), and records the rename of
* the chunk's `name` for it. */
function addNumbered(
into: Map<string, string>,
key: string,
name: string,
prefix: string,
names: Map<string, string>,
): void {
let own = into.get(key);
if (own === undefined) {
own = `${prefix}${into.size}`;
into.set(key, own);
}
if (own !== name) names.set(name.replace(/^@/, ""), own.replace(/^@/, ""));
}
/** addNumbered for entries that keep their numbered name in `sym`. */
function addNumberedSymbol<V extends { sym: string }>(
into: Map<string, V>,
key: string,
entry: V,
prefix: string,
names: Map<string, string>,
): void {
let own = into.get(key);
if (own === undefined) {
own = { ...entry, sym: `${prefix}${into.size}` };
into.set(key, own);
}
if (own.sym !== entry.sym) names.set(entry.sym, own.sym);
}
registerReplay("jsIcSite", (emitter) => emitter.jsIcSite());
registerReplay("dynReadIcSite", (emitter) => emitter.dynReadIcSite());
registerReplay("speculativeLoopSite", (emitter) => emitter.speculativeLoopSite());
for (const op of [
"dyn.collectionDynOps",
"dyn.dynMatchHelper",
"dyn.jsonSchema",
"dyn.recordNameTable",
"dyn.jsonParseHelper",
"dyn.dynCheckHelper",
"dyn.toDynHelper",
"dyn.promiseDynAdapterHelper",
"dyn.dynFuncThunkHelper",
"dyn.dynFuncBoxHelper",
"dyn.dynFuncAdapterHelper",
"dyn.dynToStrHelper",
"dyn.dynIterNHelper",
"dyn.dynDestrCheckHelper",
"dyn.dynKeyGetHelper",
"dyn.dynComputedKeyGetHelper",
"dyn.caughtToDynHelper",
"dyn.messageListenerHelper",
"dyn.messageReaderHelper",
])
registerReplay(op, (emitter, args) => emitter.dyn.replayHelper(op, args));
for (const op of [
"walkers.jsonWriteHelper",
"walkers.recordFieldWriter",
"walkers.unionJoinHelper",
])
registerReplay(op, (emitter, args) => emitter.walkers.replayHelper(op, args));
for (const op of ["publish.walker", "publish.refusal"])
registerReplay(op, (emitter, args) => emitter.publishWalkers.replayHelper(op, args));
@@ -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;
}
@@ -455,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");
@@ -475,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
@@ -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";
@@ -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,
@@ -815,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;
@@ -1493,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));
@@ -31,8 +31,10 @@ import {
emitMapIterCount,
emitMapIterEntry,
emitMapIterSlot,
compactMapValues,
mapEntryLayout,
emitMapIterStart,
emitMapSize,
emitNumberMapGetRef,
emitNumberMapGetScalar,
emitNumberMapHas,
@@ -58,6 +60,7 @@ import {
emitDenseArrayPush,
} from "./dense-array-access.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 +68,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 +79,7 @@ export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string
`}`,
``,
);
host.journal?.built();
}
return sym;
}
@@ -102,13 +107,17 @@ 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",
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. */
@@ -116,6 +125,7 @@ export function emitArrayValues(
host: LlvmEmitterContext,
arr: string,
acc: "f64" | "bool" | "ref",
element: IrType,
values: LlValue[],
prepend = false,
sharedBuffer?: string,
@@ -129,7 +139,7 @@ export function emitArrayValues(
return result;
}
if (values.length === 1) {
if (!prepend) return arrPush(host, arr, acc, values[0]!.name);
if (!prepend) return arrPush(host, arr, acc, values[0]!.name, element);
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
host.declare(
`declare double @scr_arr_unshift_${acc}(ptr, ${ty}${acc === "bool" ? " zeroext" : ""})`,
@@ -628,7 +638,7 @@ 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 };
return { name: emitArrayValues(host, r.name, acc, elem, vs), type: e.type };
}
case "pushSpread": {
// `a.push(...src)`: append src's elements in order (borrowed src,
@@ -660,7 +670,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 +712,7 @@ export function emitArrIntrinsic(
let slowLabel = "";
if (method === "pop") {
slowLabel = B.newLabel("remove.slow");
denseRaw = emitDenseArrayPop(host, r.name, acc, la, slowLabel);
denseRaw = emitDenseArrayPop(host, r.name, acc, elem, la, slowLabel);
denseLabel = B.currentLabel();
B.br(lp);
B.startBlock(slowLabel);
@@ -771,7 +781,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 +799,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(),
@@ -1060,8 +1070,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 +1136,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 +1302,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 +1380,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 +1428,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 +1442,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 +1454,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 +1473,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 +1500,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 +1551,5 @@ export function emitSetNew(host: LlvmEmitterContext, e: IrExpr & { kind: "setNew
}
return out;
}
registerReplay("resolveThunkFor", (host, args) => resolveThunkFor(host, args[0] as IrType));
@@ -250,8 +250,10 @@ export function emitDynamicExpr(
}
// Buffer.prototype method names take their scr_bytes_dyn.c entry, which
// runs them on Buffer receivers and dispatches any other receiver.
const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared";
host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`);
// Each call passes its method name's id slot, so only the first call
// looks the name up.
const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared_id";
host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`);
let name = host.cstr(e.calleeName);
if (e.calleeNameValue !== undefined) {
const value = emitBorrowedInput(host, e.calleeNameValue);
@@ -260,7 +262,7 @@ export function emitDynamicExpr(
}
const t = B.tmp();
B.line(
`${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`,
`${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${host.methodIdSlot(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`,
);
const out = host.own({ name: t, type: e.type });
host.emitPendingCheck();
@@ -1296,9 +1298,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})`);
@@ -1306,9 +1306,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}`);
@@ -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);
});
@@ -32,7 +32,7 @@ import {
emitPowerOfTwoRemainder,
emitSignedIntegerRemainder,
} from "./integer-remainder.js";
import { emitDenseArrayGet, emitDenseArrayState, slotWidth } from "./dense-array-access.js";
import { denseSlotWidth, emitDenseArrayGet, emitDenseArrayState } from "./dense-array-access.js";
import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js";
export function emitLiteralExpr(
@@ -563,6 +563,7 @@ export function regexLiteralTemplate(
};
host.regexInstances.set(key, re);
}
host.journal?.entry("regex", key, re);
return `@${re.sym}`;
}
@@ -823,6 +824,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":
@@ -879,7 +881,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) => {
@@ -924,7 +926,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": {
@@ -962,7 +964,7 @@ export function emitContainerExpr(
idx,
e.index,
"has",
slotWidth(elemAccess(e.arr.type.elem)),
denseSlotWidth(host, e.arr.type.elem),
);
return { name: t, type: e.type };
}
@@ -977,7 +979,7 @@ export function emitContainerExpr(
idx,
e.index,
"state",
slotWidth(elemAccess(e.arr.type.elem)),
denseSlotWidth(host, e.arr.type.elem),
);
return { name: t, type: e.type };
}
@@ -44,6 +44,7 @@ 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
@@ -91,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)`);
@@ -146,6 +148,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri
`}`,
``,
);
host.journal?.built();
return sym;
}
@@ -602,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) {
@@ -684,6 +688,7 @@ export function liveDynUnionRefAdapter(
`}`,
``,
);
host.journal?.built();
return sym;
}
@@ -732,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 (
@@ -744,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;
}
}
@@ -1185,6 +1196,7 @@ export function streamTypedRefMaterializeAdapter(
`}`,
``,
);
host.journal?.built();
return adapter;
}
@@ -1469,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) {
@@ -1585,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,6 +15,7 @@ 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) {
throw new InternalCompilerError(
@@ -207,6 +209,7 @@ export function streamDataAdapter(
d.push(`}`, ``);
}
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
@@ -220,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) {
@@ -347,6 +351,7 @@ export function streamDoneFnFor(
d.push(` call void ${releaseSym(host.shapeHost, dataT)}(ptr %d)`);
d.push(` ret void`, `}`, ``);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
@@ -360,6 +365,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)}`,
@@ -415,6 +421,7 @@ export function fsRenameThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
``,
);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
@@ -447,6 +454,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:`,
@@ -489,6 +497,7 @@ export function cryptoIntThunkFor(
``,
);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
@@ -507,6 +516,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:`,
@@ -618,6 +628,7 @@ export function zlibBytesThunkFor(
``,
);
host.resolveThunkDefs.push(...d);
host.journal?.built();
return sym;
}
@@ -633,6 +644,7 @@ 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"]
@@ -901,5 +913,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,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 -1
View File
@@ -732,7 +732,7 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue {
/** dynInvoke's method read on an ordinary object through the site's
* cache, else scr_dyn_prepare_method. Returns the owned method (or NULL,
* which scr_dyn_invoke_prepared answers with the full dispatch). */
* which scr_dyn_invoke_prepared_id answers with the full dispatch). */
export function emitJsIcMethod(host: Host, e: ExprOf<"dynInvoke">, receiver: string): string {
const B = host.B;
const method = e.method;
@@ -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));
@@ -163,7 +163,7 @@ function emitObjEntryPairs(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
B.line(`${row} = call ptr @${mangleRecordNew(rowT.shapeId)}()`);
host.storeField(host.recordFieldPtr(row, rowT.shapeId, "0").ptr, STRING, key, false);
host.storeField(host.recordFieldPtr(row, rowT.shapeId, "1").ptr, DYN, item, false);
host.arrPush(arr, "ref", row); // push takes ownership of the row
host.arrPush(arr, "ref", row, null); // push takes ownership of the row
const next = B.tmp();
B.line(`${next} = add ${size} ${i}, 1`);
B.line(`store ${size} ${next}, ptr ${iSlot}`);
@@ -536,7 +536,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
const parent = B.tmp();
B.line(`${parent} = call ptr @scr_fs_glob_parent(ptr ${snap}, ${host.sizeType} ${i}) ; +1`);
B.line(`store ptr ${parent}, ptr ${host.recordFieldPtr(row, recT.shapeId, "parentPath").ptr}`);
host.arrPush(arr, "ref", row); // push takes ownership of the row
host.arrPush(arr, "ref", row, null); // push takes ownership of the row
const i2 = B.tmp();
B.line(`${i2} = add ${host.sizeType} ${i}, 1`);
B.line(`store ${host.sizeType} ${i2}, ptr ${iSlot}`);
@@ -620,7 +620,7 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
B.line(
`store ptr ${host.retainHeap(path.name, STRING)}, ptr ${host.recordFieldPtr(row, recT.shapeId, "parentPath").ptr}`,
);
host.arrPush(arr, "ref", row); // push takes ownership of the row
host.arrPush(arr, "ref", row, null); // push takes ownership of the row
const i2 = B.tmp();
B.line(`${i2} = add ${host.sizeType} ${i}, 1`);
B.line(`store ${host.sizeType} ${i2}, ptr ${iSlot}`);
@@ -955,7 +955,7 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
B.startBlock(lj);
const rows = B.tmp();
B.line(`${rows} = load ptr, ptr ${rowsSlot}`);
host.arrPush(rows, "ref", row); // push takes ownership of the row
host.arrPush(rows, "ref", row, null); // push takes ownership of the row
B.line(`call void @scr_arr_release(ptr ${rows})`);
B.line(`call void @scr_str_release(ptr ${nm})`);
const i2 = B.tmp();
@@ -13,6 +13,7 @@ import {
requestHandler,
} from "./lib-abi.js";
import { FN_ATTRS } from "./shapes.js";
import { registerReplay } from "./emit-journal.js";
/** The runtime owns the answer closure; its arguments arrive owned from the caller. */
function sniAnswer(host: LlvmEmitterContext, type: IrType): string {
@@ -22,6 +23,7 @@ function sniAnswer(host: LlvmEmitterContext, type: IrType): string {
if (previous !== undefined) return previous;
const symbol = `sc_sni_answer_${host.resolveThunks.size}`;
host.resolveThunks.set(key, symbol);
host.journal?.allocated(symbol, "sniAnswer", [type]);
const params = ["ptr %self"],
body: string[] = [];
let hasError = "false",
@@ -82,6 +84,7 @@ function sniAnswer(host: LlvmEmitterContext, type: IrType): string {
"}",
"",
);
host.journal?.built();
return symbol;
}
@@ -318,3 +321,5 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa
const handler = thunk === null ? [a[1]!] : callbackAdapter(host, cb, thunk, params);
return finishRuntimeCall(host, e, runtime, [a[0]!, ...handler, ...a.slice(2)]);
}
registerReplay("sniAnswer", (host, args) => sniAnswer(host, args[0] as IrType));
@@ -1,9 +1,10 @@
import { InternalCompilerError } from "../../errors.js";
import { typeKey } from "../../ir/ir.js";
import { type IrType, typeKey } from "../../ir/ir.js";
import { mangleRecordRelease, mangleRecordStruct } from "../mangle.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";
export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
const args = e.args.map((arg) => host.emitExpr(arg)),
@@ -12,6 +13,18 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV
answer = lookup.params[2];
if (answer?.kind !== "func" || answer.params.length !== 2)
throw new InternalCompilerError("lookup answer must have two parameters");
const symbol = lookupAnswer(host, answer);
host.usesTimers = true;
host.toHeap(cb);
return finishRuntimeCall(host, e, "scr_net_connect_lookup", [
...args.map((arg) => abiValue(host, arg)),
ptr("@" + symbol),
]);
}
/** The thunk that hands a lookup's answer (error, addresses) to the
* runtime, one per answer signature. */
function lookupAnswer(host: LlvmEmitterContext, answer: IrType & { kind: "func" }): string {
const error = answer.params[0]!,
addresses = answer.params[1]!;
if (error.kind !== "union" || addresses.kind !== "array" || addresses.elem.kind !== "record")
@@ -26,6 +39,7 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV
if (symbol === undefined) {
symbol = `sc_lookup_answer_${host.resolveThunks.size}`;
host.resolveThunks.set(key, symbol);
host.journal?.allocated(symbol, "lookupAnswer", [answer]);
for (const declaration of [
"declare ptr @scr_str_retain_v(ptr)",
"declare void @scr_str_release(ptr)",
@@ -84,11 +98,11 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV
"}",
"",
);
host.journal?.built();
}
host.usesTimers = true;
host.toHeap(cb);
return finishRuntimeCall(host, e, "scr_net_connect_lookup", [
...args.map((arg) => abiValue(host, arg)),
ptr("@" + symbol),
]);
return symbol;
}
registerReplay("lookupAnswer", (host, args) =>
lookupAnswer(host, args[0] as IrType & { kind: "func" }),
);
@@ -47,7 +47,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.fromEntries": "scr_dyn_from_entries",
"bytes.constructor": "scr_bytes_constructor",
"bytes.construct": "scr_bytes_construct",
"bytes.instanceOf": "scr_bytes_instanceof",
"bytes.instanceOf": "scr_bytes_instanceof_at",
"arrayBuffer.constructor": "scr_array_buffer_constructor",
"sharedArrayBuffer.new": "scr_shared_array_buffer_new",
"sharedArrayBuffer.is": "scr_shared_array_buffer_is",
@@ -579,6 +579,10 @@ export const LIB_FN_SYMS: Record<string, string> = {
"crypto.ecdhNew": "scr_crypto_ecdh_new",
"crypto.ecdhConvertKey": "scr_crypto_ecdh_convert_key",
"crypto.dhNew": "scr_crypto_dh_new",
"crypto.checkPrimeSync": "scr_crypto_check_prime_sync",
"crypto.checkPrimeCb": "scr_crypto_check_prime_async",
"crypto.generatePrimeSync": "scr_crypto_generate_prime_sync",
"crypto.generatePrimeCb": "scr_crypto_generate_prime_async",
"crypto.dhGroupNew": "scr_crypto_dh_group_new",
"crypto.hmacNewStr": "scr_crypto_hmac_new_str",
"crypto.hmacNewBytes": "scr_crypto_hmac_new_bytes",
@@ -754,6 +758,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"cp.spawnSync": "scr_spawn_sync",
"cp.spawnSyncOpts": "scr_spawn_sync_opts",
"cp.spawnSyncStdioStr": "scr_spawn_sync_stdio_str",
"cp.spawnSyncEnv": "scr_spawn_sync_env",
"cp.execSync": "scr_exec_sync",
"cp.execCapture": "scr_exec_capture",
"spawnRes.stdout": "scr_spawn_res_stdout",
@@ -1600,6 +1605,8 @@ export function mockTimerSym(
export const USES_TIMERS_LIB_FNS = new Set<string>([
"crypto.generateKeyPairCb",
"crypto.checkPrimeCb",
"crypto.generatePrimeCb",
"process.emitWarning",
// Constructor-function streams defer their events on the stream ticks.
"dyn.builtinStreamClass",
@@ -940,3 +940,17 @@ test("a dense read tests the hole-free prefix before the capacity", () => {
expect(ir.lastIndexOf("local.array.range", firstCap)).toBeGreaterThan(firstFilled);
}
});
test("an optional read at a proven integer index skips the double round trip", () => {
const module = fixture();
const call = module.functions[2]!.body[0]!;
if (call.kind !== "varDecl" || call.init?.kind !== "call") throw new Error("missing call");
call.init.args[1] = { kind: "bin", op: "|", left: ref("i", F64), right: num(0), type: F64, loc };
expect(candidates(module).get("value")?.borrow).toBe(true);
expect(validateModule(module)).toEqual([]);
// The int32 index is the dense offset on 64-bit targets; a 32-bit target
// converts the double, as a borrowed read does.
expect(workBody(module, 64)).not.toContain("local.array.exact");
expect(workBody(module, 64)).toContain("local.array.value");
expect(workBody(module, 32)).toContain("local.array.exact");
});
@@ -15,6 +15,13 @@ import { ReferenceEffects } from "./reference-effects.js";
import { SCR_ARR_FIELD, loadArrCount, scrArrStateAt } from "./common.js";
import { exactInteger, widenInteger } from "./integer-values.js";
import { READ_FALLBACK } from "./dense-array-access.js";
import { compactRefLoadLines } from "./classes.js";
/** The dense slot load of a reference read from an array of type
* `arrayType`: a pointer, or a compact class reference (SCR_ELEM_CREF). */
function referenceLoad(host: LlvmEmitterContext, arrayType: IrType): "ptr" | "cref" {
return arrayType.kind === "array" && host.compactArrayElem(arrayType.elem) ? "cref" : "ptr";
}
export interface LocalArrayRead {
type: IrType;
@@ -469,7 +476,7 @@ function emitDenseReferenceArrayRead(
no: string,
slow: string,
join: string,
loadType: "ptr" | "i64" | "i8" = "ptr",
loadType: "ptr" | "cref" | "i64" | "i8" = "ptr",
): void {
const B = host.B;
const header = (field: number): string => {
@@ -555,7 +562,14 @@ function emitDenseReferenceArrayRead(
const statePtr = B.tmp(),
denseState = B.tmp(),
densePresent = B.tmp();
scrArrStateAt(host, statePtr, data, cap, loadType === "i8" ? 1 : 8, offset);
scrArrStateAt(
host,
statePtr,
data,
cap,
loadType === "i8" ? 1 : loadType === "cref" ? 4 : 8,
offset,
);
host.markMemoryPointer(statePtr, "array:present");
B.line(`${denseState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
B.line(`${densePresent} = icmp eq i8 ${denseState}, 1`);
@@ -564,13 +578,20 @@ function emitDenseReferenceArrayRead(
B.startBlock(valueLabel);
const valuePtr = B.tmp(),
raw = B.tmp();
// Boolean arrays store one byte per element; every other kind an 8-byte slot.
const slotType = loadType === "i8" ? "i8" : "i64";
// Boolean arrays store one byte per element, compact references four,
// every other kind an 8-byte slot.
const slotType = loadType === "i8" ? "i8" : loadType === "cref" ? "i32" : "i64";
B.line(
`${valuePtr} = getelementptr inbounds ${slotType}, ptr ${data}, ${host.sizeType} ${offset}`,
);
host.markMemoryPointer(valuePtr, "array:elements");
B.line(`${raw} = load ${loadType}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
if (loadType === "cref") {
const ref = B.tmp();
B.line(`${ref} = load i32, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
for (const line of compactRefLoadLines(ref, raw, `${raw}.r`)) B.line(line.trimStart());
} else {
B.line(`${raw} = load ${loadType}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
}
present(raw);
B.br(join);
}
@@ -598,6 +619,18 @@ export function emitBorrowedArrayRead(
);
host.markMemoryPointer(dataPtr, "array:header");
B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
if (referenceLoad(host, read.arr.type) === "cref") {
// Compact references: 4-byte slots holding the address over 8.
const ref = B.tmp();
B.line(
`${valuePtr} = getelementptr inbounds i32, ptr ${data}, ${host.sizeType} ${loopIndex}`,
);
host.markMemoryPointer(valuePtr, "array:elements");
B.line(`${ref} = load i32, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
for (const line of compactRefLoadLines(ref, value, `${value}.r`)) B.line(line.trimStart());
B.line(`store ptr ${value}, ptr ${slot}`);
return;
}
B.line(`${valuePtr} = getelementptr inbounds i64, ptr ${data}, ${host.sizeType} ${loopIndex}`);
host.markMemoryPointer(valuePtr, "array:elements");
B.line(`${value} = load ptr, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
@@ -622,6 +655,7 @@ export function emitBorrowedArrayRead(
slow,
slow,
join,
referenceLoad(host, read.arr.type),
);
B.startBlock(slow);
host.declare("declare ptr @scr_arr_borrow_ref(ptr, double)");
@@ -644,8 +678,15 @@ export function emitLocalArrayRead(
): { slot: string; type: IrType } | null {
const B = host.B;
const array = host.emitStableReceiver(read.array, [read.index]);
const integerIndex = inline ? host.emitIntegerLoopIndex(read.index) : null;
const loopIndex = inline ? host.emitIntegerLoopIndex(read.index) : null;
const index = host.emitExpr(read.index);
// A proven integer index (a counter with a known range, say) needs no
// double round trip, as in a borrowed read.
const exact =
inline && loopIndex === null && host.sizeType === "i64"
? exactInteger(host, index, read.index)
: null;
const integerIndex = loopIndex ?? (exact ? widenInteger(host, exact) : null);
const nullable = read.type.kind === "union" ? host.nullableUnions.get(read.type.unionId) : null;
if (nullable) {
// A nullable union is the element pointer itself: borrowed from an
@@ -665,12 +706,12 @@ export function emitLocalArrayRead(
array,
index,
integerIndex,
integerIndex !== null,
loopIndex !== null,
store,
no,
slow,
join,
"ptr",
referenceLoad(host, read.array.type),
);
else B.br(slow);
B.startBlock(slow);
@@ -726,12 +767,12 @@ export function emitLocalArrayRead(
array,
index,
integerIndex,
integerIndex !== null,
loopIndex !== null,
storeValue,
no,
slow,
join,
!scalar ? "ptr" : read.element.kind === "bool" ? "i8" : "i64",
!scalar ? referenceLoad(host, read.array.type) : read.element.kind === "bool" ? "i8" : "i64",
);
else B.br(slow);
B.startBlock(slow);
@@ -181,3 +181,63 @@ test("hashed number-key lookups probe inline and call the runtime only for wide
);
}
});
/** work(map): map.size, and keep(map): map returned as an owned value. */
function sizeFixture(): IrModule {
const map: IrType = { kind: "map", key: F64, value: F64 };
const params = [{ localId: "map", name: "map", type: map }];
const locals = params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: true }));
const size: IrExpr = {
kind: "mapIntrinsic",
method: "size",
receiver: ref("map", map),
args: [],
type: F64,
loc,
};
return {
irVersion: 17,
sourceFile: loc.file,
entry: "main",
records: [],
unions: [],
functions: [
{ name: "main", params: [], locals: [], returnType: VOID, body: [], loc },
{
name: "work",
params,
returnType: F64,
loc,
locals,
body: [{ kind: "return", value: size, loc }],
},
{
name: "keep",
params,
returnType: map,
loc,
locals,
body: [{ kind: "return", value: ref("map", map), loc }],
},
],
};
}
test("Map.size and Map retains need no runtime call in release builds", () => {
const mod = sizeFixture();
expect(validateModule(mod)).toEqual([]);
for (const bits of [32, 64] as const) {
const module = emitLlvmModule(mod, { pointerBits: bits, inlineRc: "release" });
const fn = (name: string) =>
new RegExp(`^define internal [^\\n]*@sc_(?:b)?f_${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(
module,
)![0];
// Field 9 is the live entry count (scr_map.h).
expect(module).toMatch(/^define internal double @sc_map_size\(ptr %m\)/m);
expect(module).toContain(`getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 9`);
expect(fn("work")).toContain("call double @sc_map_size(");
expect(module).not.toContain("call double @scr_map_size(");
expect(fn("keep")).toContain("@sc_rc_retain_map(");
expect(module).not.toContain("call ptr @scr_map_retain_v(");
}
});
@@ -15,6 +15,12 @@
* retain inline rather than through the runtime's indirect adapter call,
* and scalar reads and has() share the one lookup.
*
* A string-key map whose values are compact class instances
* (SCR_MAP_VAL_CREF, LlEmitter.compactArrayElem of the value type) keeps
* 16-byte { key, hash, val } entries with the value as an address over 8:
* its reads go through sc_map_entry_str_c and sc_map_iter_entry_c, and
* decode the 4-byte value.
*
* The iteration steps behind for-of, forEach and the other entry walks read
* the entries array through the internal sc_map_iter_entry and
* sc_map_iter_count helpers: a live entry's key or value loads in place,
@@ -24,6 +30,7 @@
*/
import type { IrType } from "../../ir/ir.js";
import type { LlvmEmitterContext } from "./expr-context.js";
import { mapKeyAccess } from "./shapes.js";
export const NUMBER_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_f64(ptr, double)";
@@ -47,18 +54,59 @@ export function emitNumberMapSlot(host: LlvmEmitterContext, entry: string, type:
}
export const STRING_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_str(ptr, ptr)";
/** Declared by reads of a compact-value map; its presence makes the emitter
* define sc_map_entry_str_c and sc_map_iter_entry_c (emit-module.ts). */
export const STRING_MAP_ENTRY_C_DECL = "declare ptr @scr_map_entry_str_c(ptr, ptr)";
/** The entry for a string `key`, or null when absent. Valid until the next mutation. */
export function emitStringMapEntry(host: LlvmEmitterContext, map: string, key: string): string {
host.declare(STRING_MAP_ENTRY_DECL);
/** Whether a Map with key type `key` and value type `value` keeps compact
* values (SCR_MAP_VAL_CREF): string keys and a value type whose instances
* are compact (LlEmitter.compactArrayElem). */
export function compactMapValues(host: LlvmEmitterContext, key: IrType, value: IrType): boolean {
// SCR_MAP_KEY_STR (shapes.ts mapKeyKindNum 1): string access, not a union.
return key.kind !== "union" && mapKeyAccess(key) === "str" && host.compactArrayElem(value);
}
/** The entry for a string `key`, or null when absent. Valid until the next
* mutation. `compact`: the map keeps compact values (a %ScrMapCEntry). */
export function emitStringMapEntry(
host: LlvmEmitterContext,
map: string,
key: string,
compact = false,
): string {
host.declare(compact ? STRING_MAP_ENTRY_C_DECL : STRING_MAP_ENTRY_DECL);
const entry = host.B.tmp();
host.B.line(`${entry} = call ptr @sc_map_entry_str(ptr ${map}, ptr ${key})`);
host.B.line(
`${entry} = call ptr @${compact ? "sc_map_entry_str_c" : "sc_map_entry_str"}(ptr ${map}, ptr ${key})`,
);
return entry;
}
/** A found compact-value entry's value as a pointer: its 4-byte form, an
* address over 8 (scr_map.h ScrMapCEntry). */
export function emitCompactMapSlot(host: LlvmEmitterContext, entry: string): string {
const B = host.B;
const slot = B.tmp(),
ref = B.tmp(),
wide = B.tmp(),
addr = B.tmp(),
value = B.tmp();
B.line(`${slot} = getelementptr inbounds %ScrMapCEntry, ptr ${entry}, i32 0, i32 2`);
B.line(`${ref} = load i32, ptr ${slot}`);
B.line(`${wide} = zext i32 ${ref} to i64`);
B.line(`${addr} = shl nuw nsw i64 ${wide}, 3`);
B.line(`${value} = inttoptr i64 ${addr} to ptr`);
return value;
}
/** Map.has / Set.has under a string key. */
export function emitStringMapHas(host: LlvmEmitterContext, map: string, key: string): string {
const entry = emitStringMapEntry(host, map, key);
export function emitStringMapHas(
host: LlvmEmitterContext,
map: string,
key: string,
compact = false,
): string {
const entry = emitStringMapEntry(host, map, key, compact);
const found = host.B.tmp();
host.B.line(`${found} = icmp ne ptr ${entry}, null`);
return found;
@@ -92,12 +140,18 @@ export function emitStringMapGetRef(
map: string,
key: string,
value: IrType,
compact = false,
): string {
return emitMapEntryGetRef(host, emitStringMapEntry(host, map, key), value);
return emitMapEntryGetRef(host, emitStringMapEntry(host, map, key, compact), value, compact);
}
/** The +1 value of a found entry, or null for a missing (null) entry. */
function emitMapEntryGetRef(host: LlvmEmitterContext, entry: string, value: IrType): string {
function emitMapEntryGetRef(
host: LlvmEmitterContext,
entry: string,
value: IrType,
compact = false,
): string {
const B = host.B;
const found = B.tmp();
B.line(`${found} = icmp ne ptr ${entry}, null`);
@@ -106,7 +160,10 @@ function emitMapEntryGetRef(host: LlvmEmitterContext, entry: string, value: IrTy
join = B.newLabel("mapn.join");
B.condBr(found, hit, miss);
B.startBlock(hit);
const retained = host.retainFrame(emitNumberMapSlot(host, entry, "ptr"), value);
const retained = host.retainFrame(
compact ? emitCompactMapSlot(host, entry) : emitNumberMapSlot(host, entry, "ptr"),
value,
);
B.br(join);
B.startBlock(miss);
B.br(join);
@@ -199,20 +256,25 @@ export const SCR_MAP_NUM_DEAD = "9218868437227405313";
/** The entry layout a Map or Set of key type `key` stores:
* - "num": number keys, whose entries are { key, val } (scr_map.h ScrMapNumEntry);
* - "centry": string keys with compact values (compactMapValues), whose
* entries are { key, hash, val } (ScrMapCEntry);
* - "entry": every other typed key, whose entries are { key, val, hash };
* - "either": checked (`unknown`) keys. A `Map<unknown, unknown>` or
* `Set<unknown>` cast from a checked value keeps the collection's own
* storage (scr_dyn_native_collection_check), which can have number keys. */
export type MapEntryLayout = "num" | "entry" | "either";
* storage (scr_dyn_native_collection_check), which can have number keys
* or compact values. */
export type MapEntryLayout = "num" | "centry" | "entry" | "either";
export function mapEntryLayout(key: IrType): MapEntryLayout {
export function mapEntryLayout(key: IrType, compact = false): MapEntryLayout {
if (compact) return "centry";
return key.kind === "f64" ? "num" : key.kind === "dyn" ? "either" : "entry";
}
/** The entry at an iteration index, or null for a tombstone or an index at
* or past the entry count (the scr_map_iter_live test). With the "either"
* layout, the map's key kind (%ScrMapIx field 1, SCR_MAP_KEY_F64 = 0) picks
* the step at run time. */
* the step at run time; in a program with compact class instances, so does
* its value kind (field 2, SCR_MAP_VAL_CREF = 3). */
export function emitMapIterEntry(
host: LlvmEmitterContext,
map: string,
@@ -220,14 +282,20 @@ export function emitMapIterEntry(
layout: MapEntryLayout,
): string {
const B = host.B;
const step = (num: boolean): string => {
host.declare(num ? MAP_ITER_NUM_DECL : MAP_ITER_DECL);
const step = (kind: "num" | "centry" | "entry"): string => {
host.declare(kind === "num" ? MAP_ITER_NUM_DECL : MAP_ITER_DECL);
if (kind === "centry") host.declare(STRING_MAP_ENTRY_C_DECL);
const entry = B.tmp();
const helper = num ? "sc_map_iter_entry_num" : "sc_map_iter_entry";
const helper =
kind === "num"
? "sc_map_iter_entry_num"
: kind === "centry"
? "sc_map_iter_entry_c"
: "sc_map_iter_entry";
B.line(`${entry} = call ptr @${helper}(ptr ${map}, double ${index})`);
return entry;
};
if (layout !== "either") return step(layout === "num");
if (layout !== "either") return step(layout);
host.declare(MAP_ITER_DECL);
const kindPtr = B.tmp(),
kind = B.tmp(),
@@ -240,16 +308,31 @@ export function emitMapIterEntry(
join = B.newLabel("mapit.layout");
B.condBr(isNum, numLabel, entryLabel);
B.startBlock(numLabel);
const numEntry = step(true);
const numEntry = step("num");
const numEnd = B.currentLabel();
B.br(join);
B.startBlock(entryLabel);
const otherEntry = step(false);
const entryEnd = B.currentLabel();
const arms: { value: string; label: string }[] = [{ value: numEntry, label: numEnd }];
if (host.shapeHost.compactRegion?.() === true) {
const valPtr = B.tmp(),
valKind = B.tmp(),
isCompact = B.tmp();
B.line(`${valPtr} = getelementptr inbounds %ScrMapIx, ptr ${map}, i32 0, i32 2`);
B.line(`${valKind} = load i8, ptr ${valPtr}`);
B.line(`${isCompact} = icmp eq i8 ${valKind}, 3`);
const compactLabel = B.newLabel("mapit.centry"),
wideLabel = B.newLabel("mapit.wide");
B.condBr(isCompact, compactLabel, wideLabel);
B.startBlock(compactLabel);
arms.push({ value: step("centry"), label: B.currentLabel() });
B.br(join);
B.startBlock(wideLabel);
}
arms.push({ value: step("entry"), label: B.currentLabel() });
B.br(join);
B.startBlock(join);
const entry = B.tmp();
B.line(`${entry} = phi ptr [ ${numEntry}, %${numEnd} ], [ ${otherEntry}, %${entryEnd} ]`);
B.line(`${entry} = phi ptr ${arms.map((a) => `[ ${a.value}, %${a.label} ]`).join(", ")}`);
return entry;
}
@@ -270,6 +353,16 @@ export function emitMapIterCount(host: LlvmEmitterContext, map: string): string
return count;
}
export const MAP_SIZE_DECL = "declare double @scr_map_size(ptr)";
/** Map.size / Set.size: the live entry count, read inline (sc_map_size). */
export function emitMapSize(host: LlvmEmitterContext, map: string): string {
host.declare(MAP_SIZE_DECL);
const size = host.B.tmp();
host.B.line(`${size} = call double @sc_map_size(ptr ${map})`);
return size;
}
/** The key (field 0) or value (field 1) of the live entry at an iteration
* index, read inline: a number as double, a boolean as i1, and a string or
* reference retained with `type`'s retain (+1), the adapter the collection
@@ -294,18 +387,24 @@ export function emitMapIterSlot(
join = B.newLabel("mapit.join");
B.condBr(found, hit, miss);
B.startBlock(hit);
const slot = B.tmp();
B.line(`${slot} = getelementptr inbounds %ScrMapEntry, ptr ${entry}, i32 0, i32 ${field}`);
const llTy = access === "f64" ? "double" : access === "bool" ? "i1" : "ptr";
let loaded = B.tmp();
if (access === "bool") {
const bits = loaded;
B.line(`${bits} = load i64, ptr ${slot}`);
loaded = B.tmp();
B.line(`${loaded} = icmp ne i64 ${bits}, 0`);
let loaded: string;
if (layout === "centry" && field === 1) {
// A compact value; its type's retain emits no counting.
loaded = host.retainFrame(emitCompactMapSlot(host, entry), type);
} else {
B.line(`${loaded} = load ${llTy}, ptr ${slot}`);
if (access === "ref") loaded = host.retainFrame(loaded, type);
const slot = B.tmp();
B.line(`${slot} = getelementptr inbounds %ScrMapEntry, ptr ${entry}, i32 0, i32 ${field}`);
loaded = B.tmp();
if (access === "bool") {
const bits = loaded;
B.line(`${bits} = load i64, ptr ${slot}`);
loaded = B.tmp();
B.line(`${loaded} = icmp ne i64 ${bits}, 0`);
} else {
B.line(`${loaded} = load ${llTy}, ptr ${slot}`);
if (access === "ref") loaded = host.retainFrame(loaded, type);
}
}
const hitEnd = B.currentLabel();
B.br(join);
@@ -15,6 +15,7 @@ import {
emitNumberMapGetScalar,
emitStringMapGetScalar,
emitStringMapGetRef,
compactMapValues,
} from "./map-number-lookup.js";
import { mapKeyAccess, mapKeyParamType } from "./shapes.js";
import { rememberMapEntry } from "./map-entry-fusion.js";
@@ -323,7 +324,10 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead):
} else if (isRefCounted(read.value)) {
const stringKey = entryStringKey(key);
let raw: string;
if (stringKey !== null) raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value);
const mapType = read.receiver.type;
const compact = mapType.kind === "map" && compactMapValues(host, mapType.key, mapType.value);
if (stringKey !== null)
raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value, compact);
else {
raw = B.tmp();
host.declare(`declare ptr @scr_map_get_${access}_ref(ptr, ${keyType})`);
@@ -279,6 +279,8 @@ function shapeUnion(
* tag and payload inside the C map. */
function boxedUnionIds(mod: IrModule): Set<string> {
const boxed = new Set<string>();
// Only the module's union definitions consult the result.
if (!mod.unions?.length) return boxed;
const mark = (t: IrType): void => {
if (t.kind === "union") boxed.add(t.unionId);
};
@@ -31,6 +31,13 @@ import type { InlineUnionFields } from "./inline-unions.js";
import { LlvmUnsupportedError } from "./unsupported.js";
export interface PublishHost {
/** Chunk emitters only (emit-journal.ts): EmitJournal's allocated, built,
* opened, used and entry. */
journalAllocated?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined;
journalBuilt?: (() => void) | undefined;
journalOpened?: ((op: string) => void) | undefined;
journalUsed?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined;
journalEntry?: ((table: string, key: string, value: unknown) => void) | undefined;
declare(decl: string): void;
/** An interned NUL-terminated constant. */
cstr(text: string): string;
@@ -103,6 +110,14 @@ export class PublishWalkers {
constructor(private readonly host: PublishHost) {}
/** Journal replay (emit-journal.ts): builds or finds the walker a chunk
* emitter recorded, and answers its name. */
replayHelper(op: string, args: readonly unknown[]): string {
if (op === "publish.walker")
return this.walker(args[0] as IrType, args[1] as string, args[2] as SrcLoc)!;
return this.refusal(args[0] as string, args[1] as string);
}
/** The definitions emitted so far (module-level LLVM text). */
defs(): string[] {
return this.defsOut;
@@ -119,6 +134,7 @@ export class PublishWalkers {
}
const fn = this.walker(t, "the published value", loc);
if (fn === null) return [];
this.host.journalUsed?.(fn, "publish.walker", [t, "the published value", loc]);
this.host.declare(`declare void @scr_publish(ptr, ptr)`);
return [` call void @scr_publish(ptr ${v}, ptr ${fn})`];
}
@@ -156,8 +172,10 @@ export class PublishWalkers {
if (known !== undefined) return known;
const sym = `@sc_pub_${this.counter++}`;
this.walkers.set(key, sym);
this.host.journalOpened?.("publish.walker");
const body = this.body(t, sym, path, loc);
this.defsOut.push(...body, ``);
this.host.journalBuilt?.();
return sym;
}
@@ -172,6 +190,7 @@ export class PublishWalkers {
if (known !== undefined) return known;
const sym = `@sc_pub_${this.counter++}`;
this.refusals.set(key, sym);
this.host.journalOpened?.("publish.refusal");
this.host.declare(`declare void @scr_pub_refuse_at(ptr, ptr, ptr)`);
this.defsOut.push(
`define internal void ${sym}(ptr %o, ptr %ctx) ${FN_ATTRS} { ; refuse ${llvmComment(detail)}`,
@@ -181,6 +200,7 @@ export class PublishWalkers {
`}`,
``,
);
this.host.journalBuilt?.();
return sym;
}
@@ -12,20 +12,9 @@ import {
import { dynamicFieldStores } from "../../ir/int32-slots.js";
import { everyExpr, everyStmtList, transformStmtList } from "../../ir/traverse.js";
import { InlineUnionFields } from "./inline-unions.js";
import { RESULT_DST_ID, resultEntryName } from "../result-slots.js";
/** Prefix of a function's result-slot entry: `%into.<name>(...params, dst)`
* writes the function's fresh result object into `dst` and returns nothing. */
export const RESULT_ENTRY_PREFIX = "%into.";
export function resultEntryName(name: string): string {
return `${RESULT_ENTRY_PREFIX}${name}`;
}
/** The local id of a result-slot entry's destination parameter. The
* caller passes zeroed frame storage, and on every path the entry stores
* each of its fields at most once, so a store into it is an
* initialization: there is no old value to release. */
export const RESULT_DST_ID = "%into.dst";
export { RESULT_DST_ID, RESULT_ENTRY_PREFIX, resultEntryName } from "../result-slots.js";
function distinctNames(names: readonly string[]): boolean {
return new Set(names).size === names.length;
@@ -118,6 +118,7 @@ const INERT_LIB_CALLS = new Set([
"worker.parentPort",
"worker.root",
"cp.spawnSyncOpts",
"cp.spawnSyncEnv",
]);
/** jsval conversions (ir/jsval.ts): no user code, no stores. */
@@ -0,0 +1,97 @@
import { expect, test } from "vitest";
import { BOOL, F64, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
import { runningExtremumUpdates } from "./running-extrema.js";
const loc = { file: "extrema.ts", start: 0, end: 0 };
const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc });
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
const assign = (localId: string, value: IrExpr): IrStmt => ({
kind: "assign",
localId,
value,
loc,
});
const decl = (localId: string): IrStmt => ({ kind: "varDecl", localId, init: num(0), loc });
const compare = (op: "<" | ">", left: IrExpr, right: IrExpr): IrExpr => ({
kind: "bin",
op,
left,
right,
type: BOOL,
loc,
});
/** `if (v > max) max = v;` */
const update = (): IrStmt & { kind: "if" } => ({
kind: "if",
cond: compare(">", ref("v"), ref("max")),
then: [assign("max", ref("v"))],
else_: null,
loc,
});
const loop = (body: IrStmt[]): IrStmt => ({
kind: "while",
cond: compare(">", ref("n"), num(0)),
body,
loc,
});
const fn = (body: IrStmt[], captures: string[] = []): IrFunction => ({
name: "scan",
params: [],
locals: ["v", "max", "n"].map((id) => ({ id, name: id, type: F64, mutable: true })),
captures: captures.map((localId) => ({ localId, name: localId, type: F64 })),
returnType: F64,
body,
loc,
});
test("a loop's update of an accumulator declared outside it qualifies", () => {
const s = update();
expect(runningExtremumUpdates(fn([decl("max"), loop([decl("v"), s])])).has(s)).toBe(true);
});
test("the local may be on either side of the comparison", () => {
const s = update();
s.cond = compare("<", ref("max"), ref("v"));
expect(runningExtremumUpdates(fn([decl("max"), loop([s])])).has(s)).toBe(true);
});
test("a local declared in the loop is not an accumulator", () => {
const s = update();
expect(runningExtremumUpdates(fn([loop([decl("max"), s])])).size).toBe(0);
});
test("another write in the loop disqualifies the update", () => {
const s = update();
const body = [decl("max"), loop([assign("max", num(1)), s])];
expect(runningExtremumUpdates(fn(body)).size).toBe(0);
});
test("the assigned value must be the compared expression", () => {
const s = update();
s.then = [assign("max", num(5))];
expect(runningExtremumUpdates(fn([decl("max"), loop([s])])).size).toBe(0);
});
test("outside a loop only a captured binding qualifies", () => {
const captured = update();
expect(runningExtremumUpdates(fn([captured], ["max"])).has(captured)).toBe(true);
expect(runningExtremumUpdates(fn([update()])).size).toBe(0);
});
test("an else branch disqualifies the update", () => {
const s = update();
s.else_ = [assign("v", num(0))];
expect(runningExtremumUpdates(fn([decl("max"), loop([s])])).size).toBe(0);
});
test("an update nested in another branch does not qualify", () => {
const s = update();
const guard: IrStmt = {
kind: "if",
cond: compare(">", ref("v"), num(0)),
then: [s],
else_: null,
loc,
};
expect(runningExtremumUpdates(fn([decl("max"), loop([guard])])).size).toBe(0);
});
@@ -0,0 +1,110 @@
// Running maximum and minimum updates, `if (v > max) { max = v; ... }`.
// With the accumulator in a register LLVM turns such an update into a
// select (`maxsd`, or a compare and mask), so every element waits on the
// previous one's. The update gets rarer as a scan goes on, so a branch
// predicts well; emitStmtBody gives these `if`s branch weights that say the
// update is rarely taken, and LLVM keeps the branch. A clamp of a value read
// in the same iteration is not an accumulator and keeps its select: its
// branch would go whichever way the data goes. An update nested in another
// branch keeps its select too: kept, its branch can stop LLVM from turning
// the enclosing branch into selects, and that branch may follow the data.
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
type Loop = IrStmt & { kind: "for" | "while" | "doWhile" | "forOf" };
const RELATIONAL = new Set(["<", "<=", ">", ">="]);
/** The `if` statements of `fn` that update a running maximum or minimum:
* no `else`, a numeric `<`, `<=`, `>` or
* `>=` between a local and another expression, and a then-block that
* assigns that expression to the local. The local must carry across
* iterations: inside a loop the `if` is a statement of the innermost loop's
* body, the local is declared outside that loop, and the loop writes it only
* in this `if`; outside any loop the `if` is a statement of the function
* body and the local is a binding the function captures and writes only in
* this `if` (a callback that a loop elsewhere calls). */
export function runningExtremumUpdates(fn: IrFunction): Set<IrStmt> {
const found = new Set<IrStmt>();
const captured = new Set((fn.captures ?? []).map((c) => c.localId));
const loops: Loop[] = [];
const candidates: { s: IrStmt; local: string; loop: Loop | undefined }[] = [];
const expr = (e: IrExpr): boolean => everyExprChild(e, expr, stmt);
const stmt = (s: IrStmt): boolean => {
if (s.kind === "if") {
const local = updatedExtremum(s);
if (local !== undefined) candidates.push({ s, local, loop: loops[loops.length - 1] });
}
const loop =
s.kind === "for" || s.kind === "while" || s.kind === "doWhile" || s.kind === "forOf";
if (loop) loops.push(s);
everyStmtChild(s, expr, stmt);
if (loop) loops.pop();
return true;
};
for (const s of fn.body) stmt(s);
for (const { s, local, loop } of candidates) {
const scope = loop ?? (captured.has(local) ? fn.body : undefined);
const body = loop ? loop.body : fn.body;
if (scope !== undefined && body.includes(s) && writes(scope, local) === 1) found.add(s);
}
return found;
}
/** The local an `if` updates to a new maximum or minimum, if it is one. */
function updatedExtremum(s: IrStmt & { kind: "if" }): string | undefined {
if (s.else_ !== null && s.else_.length > 0) return undefined;
const c = s.cond;
if (c.kind !== "bin" || !RELATIONAL.has(c.op)) return undefined;
if (c.left.type.kind !== "f64" || c.right.type.kind !== "f64") return undefined;
for (const [local, other] of [
[c.left, c.right],
[c.right, c.left],
]) {
if (local!.kind !== "varRef") continue;
const id = local!.localId;
const assigns = s.then.some(
(x) => x.kind === "assign" && x.localId === id && sameExpr(x.value, other!),
);
if (assigns) return id;
}
return undefined;
}
/** Declarations and writes of `local` under `scope`. A declaration counts
* as a write too, so a local declared inside a loop does not qualify. */
function writes(scope: IrStmt | IrStmt[], local: string): number {
let n = 0;
const expr = (e: IrExpr): boolean => {
if ((e.kind === "assignExpr" || e.kind === "incDec") && e.localId === local) n++;
return everyExprChild(e, expr, stmt);
};
const stmt = (s: IrStmt): boolean => {
if ((s.kind === "assign" || s.kind === "varDecl" || s.kind === "forOf") && s.localId === local)
n++;
else if (s.kind === "tryCatch" && s.catchLocalId === local) n++;
return everyStmtChild(s, expr, stmt);
};
if (Array.isArray(scope)) for (const s of scope) stmt(s);
else stmt(scope);
return n;
}
/** Structural equality of two small expressions (`v`, `a[i]`), ignoring
* source locations and types; larger expressions do not compare equal. */
function sameExpr(a: IrExpr, b: IrExpr): boolean {
let budget = 64;
const same = (x: unknown, y: unknown): boolean => {
if (--budget < 0) return false;
if (typeof x !== "object" || x === null || typeof y !== "object" || y === null) return x === y;
if (Array.isArray(x) !== Array.isArray(y)) return false;
const keys = Object.keys(x).filter((k) => k !== "loc" && k !== "type");
if (keys.length !== Object.keys(y).filter((k) => k !== "loc" && k !== "type").length)
return false;
return keys.every((k) =>
same((x as Record<string, unknown>)[k], (y as Record<string, unknown>)[k]),
);
};
return same(a, b);
}
+33 -5
View File
@@ -68,9 +68,22 @@ const CYC_MAX_TYPES = 32768;
/** What the tables need from the emitter: the extern-declaration ledger
* and the module-wide cycle/shape indexes. */
export interface ShapeHost {
/** Chunk emitters only (emit-journal.ts): EmitJournal's allocated, built,
* opened, used and entry. */
journalAllocated?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined;
journalBuilt?: (() => void) | undefined;
journalOpened?: ((op: string) => void) | undefined;
journalUsed?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined;
journalEntry?: ((table: string, key: string, value: unknown) => void) | undefined;
/** The hierarchy root of a class (a program class or a runtime base),
* or undefined for classes outside the class graph. */
classRootOf?(className: string): string | undefined;
/** Whether arrays of `elem` hold 32-bit references to compact class
* instances (SCR_ELEM_CREF; LlEmitter.compactArrayElem). */
compactArrayElem?(elem: IrType): boolean;
/** Whether compact class instances live in the program-lifetime region
* (LlEmitter.compactRegion). */
compactRegion?(): boolean;
/** Byte offset from an object pointer back to ScrCycHdr.buf. */
readonly cycleBufOffset: number;
declare(decl: string): void;
@@ -386,6 +399,7 @@ function nullableRetainSym(host: ShapeHost, arm: IrType): string {
const sym = `@sc_nretain_${arm.kind}`;
if (!host.nullableUnions.retainWrappers.has(sym))
host.nullableUnions.retainWrappers.set(sym, retainSym(host, arm));
host.journalEntry?.("retainWrapper", sym, host.nullableUnions.retainWrappers.get(sym));
return sym;
}
@@ -564,14 +578,16 @@ export function inlineRcFamily(host: ShapeHost, stem: string, op: RcOp): InlineR
}
/** Which RC fast paths an emission inlines. `speed` mirrors every family
* with a fast path. `release` mirrors only the string retain and the dyn
* retain and release: the operations that dominate dynamic-value and string
* code, at almost no size cost. String releases stay calls in `release`
* because string-heavy programs have many release sites. */
* with a fast path. `release` mirrors only the string and Map retains and
* the dyn retain and release: the operations that dominate dynamic-value,
* string and Map code (a Map read from an array or another Map), at almost
* no size cost. String releases stay calls in `release` because
* string-heavy programs have many release sites. */
export type InlineRcMode = "release" | "speed";
const RELEASE_RC_OPS = new Set([
"str:retain",
"map:retain",
"dyn:retain",
"dyn:release",
"js:retain",
@@ -1076,18 +1092,30 @@ export function arrNewCall(
/** The %ScrArrOps descriptor (retain, release, trace, registry id) of a REF
* element type, defined once per module and shared by all its arrays. It is
* writable: the runtime stores its registry id on the first array. */
* writable: the runtime stores its registry id on the first array. Its tail
* padding is the runtime's flags word (0); arrays of compact class
* references (ShapeHost.compactArrayElem) spell the word out as
* SCR_ARR_OPS_COMPACT. */
export function arrOps(host: ShapeHost, elem: IrType): string {
const v = vAdapters(host, elem);
const trace = traceArg(host, elem);
const part = (sym: string): string => sym.replace(/^@/, "").replace(/[^A-Za-z0-9_.]/g, "_");
const name = `@sc_arrops.${part(v.retain)}.${part(v.release)}.${part(trace)}`;
if (host.compactArrayElem?.(elem) === true) {
host.declare(
`${name}.c = internal global { ptr, ptr, ptr, i32, i32 } { ptr ${v.retain}, ptr ${v.release}, ptr ${trace}, i32 0, i32 ${SCR_ARR_OPS_COMPACT} }`,
);
return `${name}.c`;
}
host.declare(
`${name} = internal global %ScrArrOps { ptr ${v.retain}, ptr ${v.release}, ptr ${trace}, i32 0 }`,
);
return name;
}
/** scr_array.h SCR_ARR_OPS_COMPACT. */
const SCR_ARR_OPS_COMPACT = 1;
/* ── capture boxes ────────────────────────────────────────────────────── */
/** Box construction call text (boxNewC's dispatch): plain-kind boxes for
@@ -62,6 +62,21 @@ export class LlWalkers {
constructor(private readonly host: WalkerHost) {}
/** 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 "walkers.jsonWriteHelper":
return this.jsonWriteHelper(args[0] as IrType);
case "walkers.recordFieldWriter":
return this.recordFieldWriter(args[0] as string);
case "walkers.unionJoinHelper":
return this.unionJoinHelper(args[0] as string);
default:
throw new InternalCompilerError(`llvm emitter bug: no walker replay ${op}`);
}
}
private get S(): "i32" | "i64" {
return this.host.sizeType;
}
@@ -191,6 +206,7 @@ export class LlWalkers {
if (existing) return existing;
const name = `sc_jw_${this.jsonWriters.size}`;
this.jsonWriters.set(key, name);
this.host.journalAllocated?.(name, "walkers.jsonWriteHelper", [t]);
const B = new BlockBuilder();
switch (t.kind) {
case "f64":
@@ -244,6 +260,7 @@ export class LlWalkers {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -457,6 +474,7 @@ export class LlWalkers {
throw new InternalCompilerError(`llvm emitter bug: field writer of unknown shape ${shapeId}`);
const name = `sc_jwf_${this.recordFieldWriters.size}`;
this.recordFieldWriters.set(shapeId, name);
this.host.journalAllocated?.(name, "walkers.recordFieldWriter", [shapeId]);
const fieldBase = this.host.nullableUnions.fieldBase(shapeId);
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + fieldBase]));
const byName = new Map(shape.fields.map((f) => [f.name, f]));
@@ -508,6 +526,7 @@ export class LlWalkers {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
@@ -852,6 +871,7 @@ export class LlWalkers {
if (!def) throw new InternalCompilerError(`llvm emitter bug: join of unknown union ${unionId}`);
const name = `sc_uj_${this.unionJoinFns.size}`;
this.unionJoinFns.set(unionId, name);
this.host.journalAllocated?.(name, "walkers.unionJoinHelper", [unionId]);
const host = this.host;
host.declare(`declare void @scr_jb_init(ptr)`);
@@ -946,6 +966,7 @@ export class LlWalkers {
`}`,
``,
);
this.host.journalBuilt?.();
return name;
}
}
@@ -1,4 +1,5 @@
import type { IrExpr } from "../../ir/ir.js";
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
import { integerArithmeticRange, type IntegerRange } from "../../ir/integer-ranges.js";
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import type { LlInteger } from "./integer-values.js";
@@ -47,6 +48,7 @@ export function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerR
return ELEMENT_RANGES.i32!;
// A UTF-16 code unit, or NaN past either end.
if (e.kind === "strIntrinsic" && e.method === "charCodeAt") return { min: 0, max: 65535 };
if (e.kind === "varRef") return host.integerOrNaNLocals.get(e.localId) ?? null;
if (e.kind === "bin" && (e.op === "+" || e.op === "-")) {
const left = integerOrNaNRange(host, e.left);
const right = left && integerOrNaNRange(host, e.right);
@@ -55,6 +57,34 @@ export function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerR
return null;
}
/** An index that is an exact integer or NaN (`integerOrNaNRange`): the integer as an i64, and an
* i1 that is false when the index is NaN. An index check then needs no round trip through a double:
* the integer is the value's ToUint32 view where the range proves it holds the whole value, or one
* conversion, which is exact for a safe integer and frozen, since the conversion of NaN is poison.
* Null for any other index. */
export function integerOrNaNIndex(
host: LlvmEmitterContext,
value: LlValue,
expr: IrExpr | undefined,
): { wide: string; ordered: string } | null {
const range = expr ? integerOrNaNRange(host, expr) : null;
if (!range) return null;
const B = host.B;
const ordered = B.tmp();
const wide = B.tmp();
B.line(`${ordered} = fcmp ord double ${value.name}, 0.0`);
if (value.uint32 !== undefined && range.min >= 0 && range.max <= 4294967295) {
B.line(`${wide} = zext i32 ${value.uint32} to i64`);
} else if (value.uint32 !== undefined && range.min >= -2147483648 && range.max <= 2147483647) {
B.line(`${wide} = sext i32 ${value.uint32} to i64`);
} else {
const raw = B.tmp();
B.line(`${raw} = fptosi double ${value.name} to i64`);
B.line(`${wide} = freeze i64 ${raw}`);
}
return { wide, ordered };
}
const ELEMENT_RANGES: Partial<Record<string, IntegerRange>> = {
u8: { min: 0, max: 255 },
u8c: { min: 0, max: 255 },
@@ -131,3 +161,85 @@ export function emitWrappingSum(
B.line(`${out} = select i1 ${valid}, i32 ${sum}, i32 0`);
return out;
}
/** The number locals of `fn` that only hold exact integers (never -0) or NaN, with the range of
* those integers: locals declared once with an initializer, neither parameters nor captured or
* boxed, whose initializer and every later write `integerOrNaNRange` covers. A local joins once
* the values it is written depend only on locals that have joined, so `x = x + 1` keeps `x` out:
* its values have no bound. Uses the function's `integerRanges` and `int32Elements`. */
export function findIntegerOrNaNLocals(
host: LlvmEmitterContext,
fn: IrFunction,
): ReadonlyMap<string, IntegerRange> {
const found = new Map<string, IntegerRange>();
if (fn.async || fn.generator) return found;
const excluded = new Set<string>();
for (const param of fn.params) excluded.add(param.localId);
for (const capture of fn.captures ?? []) excluded.add(capture.localId);
for (const capture of fn.classCaptures ?? []) excluded.add(capture.localId);
const candidates = new Set<string>();
for (const local of fn.locals)
if (local.type.kind === "f64" && !local.boxed && !local.tdz && !excluded.has(local.id))
candidates.add(local.id);
if (candidates.size === 0) return found;
const declared = new Set<string>();
const writes = new Map<string, IrExpr[]>();
const drop = (id: string): void => {
candidates.delete(id);
writes.delete(id);
};
const write = (id: string, value: IrExpr): void => {
if (!candidates.has(id)) return;
const list = writes.get(id);
if (list) list.push(value);
else writes.set(id, [value]);
};
function walkExpr(e: IrExpr): boolean {
if (e.kind === "assignExpr") write(e.localId, e.value);
else if (e.kind === "incDec") drop(e.localId);
return everyExprChild(e, walkExpr, walkStmt);
}
function walkStmt(s: IrStmt): boolean {
if (s.kind === "assign") write(s.localId, s.value);
else if (s.kind === "varDecl") {
if (s.init === null || declared.has(s.localId)) drop(s.localId);
else {
declared.add(s.localId);
write(s.localId, s.init);
}
} else if (s.kind === "forOf") drop(s.localId);
else if (s.kind === "tryCatch" && s.catchLocalId) drop(s.catchLocalId);
return everyStmtChild(s, walkExpr, walkStmt);
}
for (const s of fn.body) walkStmt(s);
// `integerOrNaNRange` reads the joined locals while the next ones are tested.
const previous = host.integerOrNaNLocals;
host.integerOrNaNLocals = found;
try {
let joined = true;
while (joined) {
joined = false;
for (const [id, values] of writes) {
if (found.has(id) || !declared.has(id)) continue;
let range: IntegerRange | null = null;
for (const value of values) {
const next = integerOrNaNRange(host, value);
if (next === null) {
range = null;
break;
}
range =
range === null
? next
: { min: Math.min(range.min, next.min), max: Math.max(range.max, next.max) };
}
if (range === null) continue;
found.set(id, range);
joined = true;
}
}
} finally {
host.integerOrNaNLocals = previous;
}
return found;
}
+8 -2
View File
@@ -35,7 +35,12 @@ export type MayThrowReason =
* and a generator's `.return()` unwind through `catch`. */
export function computeMayThrow(
mod: IrModule,
options: { stackChecks?: boolean; explain?: boolean } = {},
options: {
stackChecks?: boolean;
explain?: boolean;
/** computePublishedTypes(mod), when the caller already has it. */
published?: ReturnType<typeof computePublishedTypes>;
} = {},
): {
fns: Set<string>;
/** With `explain`: the reason each may-throw function was marked. */
@@ -50,7 +55,8 @@ export function computeMayThrow(
const terminations = mod.workers === true ? workerTerminationPolls(mod) : null;
const stackChecks = options.stackChecks === true ? stackCheckedFunctions(mod) : null;
// Stores that can reach a published object throw the frozen TypeError.
const published = computePublishedTypes(mod);
const published =
options.published !== undefined ? options.published : computePublishedTypes(mod);
interface Facts {
throws: boolean;
callees: string[];
@@ -0,0 +1,29 @@
import type { IrStmt } from "../ir/ir.js";
/** Prefix of a function's result-slot entry: `%into.<name>(...params, dst)`
* writes the function's fresh result object into `dst` and returns nothing
* (llvm/record-results.ts). */
export const RESULT_ENTRY_PREFIX = "%into.";
export function resultEntryName(name: string): string {
return `${RESULT_ENTRY_PREFIX}${name}`;
}
/** The local id of a result-slot entry's destination parameter. The
* caller passes zeroed frame storage, and on every path the entry stores
* each of its fields at most once, so a store into it is an
* initialization: there is no old value to release. */
export const RESULT_DST_ID = "%into.dst";
/** Whether `s`, a statement of the function `fnName`, is a result-slot
* entry's store into its destination. The destination is frame storage of
* a caller that only projects the value (llvm/stack-objects.ts), so the
* store initializes a field of an object no other object can point to. */
export function resultSlotStore(fnName: string, s: IrStmt): boolean {
return (
s.kind === "recordSet" &&
s.obj.kind === "varRef" &&
s.obj.localId === RESULT_DST_ID &&
fnName.startsWith(RESULT_ENTRY_PREFIX)
);
}
+2 -2
View File
@@ -2,8 +2,8 @@
* of the link contract: a program object keeps this symbol undefined and the
* matching runtime defines it, so incompatible manual links fail before the
* program can start. */
export const RUNTIME_ABI_VERSION = 27 as const;
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v27" as const;
export const RUNTIME_ABI_VERSION = 28 as const;
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v28" as const;
/** Object consumption is useful today, but the complete scr_* surface may
* still change before 1.0. The versioned marker prevents accidental mixing;
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
flavors: {
@@ -127,7 +127,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
runtime_abi: { version: 28, marker: "scr_runtime_abi_v28" },
compiler: {
command: "clang",
identity: "fixture clang",
@@ -29,35 +29,50 @@ const NON_STACK_KEY_TYPES: ReadonlySet<IrType["kind"]> = new Set(["f64", "bigint
* containers or calls; a key the program assembles from other strings or
* from data it reads keeps the static answer. */
class StackKeys {
private readonly spelled: boolean;
private readonly calls = new Map<string, { caller: IrFunction; args: IrExpr[] }[]>();
private readonly named: Set<string>;
private spelled: boolean | undefined;
private census:
| { calls: Map<string, { caller: IrFunction; args: IrExpr[] }[]>; named: Set<string> }
| undefined;
private readonly inits = new Map<IrFunction, Map<string, IrExpr | null>>();
/** Settled answers by function and local: true where a search reached a
* key that can be "stack", false where a search that found none passed. */
private readonly answers = new Map<string, boolean>();
constructor(mod: IrModule) {
this.spelled = spellsStack(mod);
this.named = new Set(moduleFunctionReferences(mod));
// The module walks run on the first question that needs them: a program
// without a dynamic key read asks none.
constructor(private readonly mod: IrModule) {}
private spellsStack(): boolean {
return (this.spelled ??= spellsStack(this.mod));
}
private callCensus(): {
calls: Map<string, { caller: IrFunction; args: IrExpr[] }[]>;
named: Set<string>;
} {
if (this.census !== undefined) return this.census;
const mod = this.mod;
const calls = new Map<string, { caller: IrFunction; args: IrExpr[] }[]>();
const named = new Set(moduleFunctionReferences(mod));
// Class methods, accessors and constructors are called through dispatch.
const classNames = new Set((mod.classes ?? []).map((cls) => cls.name));
for (const fn of mod.functions) {
if (isClassMember(fn.name, classNames)) this.named.add(fn.name);
if (isClassMember(fn.name, classNames)) named.add(fn.name);
everyStmtList(fn.body, {
stmt: () => true,
expr: (expr) => {
if (expr.kind === "call") {
const sites = this.calls.get(expr.callee) ?? [];
const sites = calls.get(expr.callee) ?? [];
sites.push({ caller: fn, args: expr.args });
this.calls.set(expr.callee, sites);
calls.set(expr.callee, sites);
}
const referenced = exprFunctionReference(expr);
if (referenced !== undefined) this.named.add(referenced);
if (referenced !== undefined) named.add(referenced);
return true;
},
});
}
return (this.census = { calls, named });
}
mayBeStack(fn: IrFunction, key: IrExpr): boolean {
@@ -71,7 +86,7 @@ class StackKeys {
private keyMayBeStack(fn: IrFunction, key: IrExpr, visited: Set<string>): boolean {
if (key.kind === "strLit") return key.value === "stack";
if (!this.spelled) return false;
if (!this.spellsStack()) return false;
if (NON_STACK_KEY_TYPES.has(key.type.kind)) return false;
if (key.kind === "toString" && NON_STACK_KEY_TYPES.has(key.operand.type.kind)) return false;
if (key.kind === "dynFrom" && NON_STACK_KEY_TYPES.has(key.value.type.kind)) return false;
@@ -93,8 +108,9 @@ class StackKeys {
if (local === undefined || local.mutable) return true;
const index = fn.params.findIndex((param) => param.localId === localId);
if (index !== -1) {
const sites = this.calls.get(fn.name);
if (this.named.has(fn.name) || sites === undefined) return true;
const { calls, named } = this.callCensus();
const sites = calls.get(fn.name);
if (named.has(fn.name) || sites === undefined) return true;
return sites.some(
(site) =>
site.args.length !== fn.params.length ||
@@ -3,6 +3,7 @@ import type { LlvmUnsupportedError } from "./llvm/emitter.js";
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
import { type IrModule, type SrcLoc } from "../ir/ir.js";
import {
DH_LIB_FNS,
PKI_LIB_FNS,
moduleUsesFetch,
moduleUsesWebCryptoAsymmetric,
@@ -232,9 +233,12 @@ export function moduleWasiUnavailableSurface(
};
return false;
}
if (node.fn === "crypto.dhNew" || node.fn === "crypto.dhGroupNew") {
if (DH_LIB_FNS.has(node.fn)) {
found = {
surface: "DiffieHellman (the wasm32-wasi runtime does not include the mbedTLS library)",
surface:
node.fn === "crypto.dhNew" || node.fn === "crypto.dhGroupNew"
? "DiffieHellman (the wasm32-wasi runtime does not include the mbedTLS library)"
: "checkPrime and generatePrime (the wasm32-wasi runtime does not include the mbedTLS library)",
loc,
};
return false;
+14 -3
View File
@@ -318,12 +318,23 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions
// statements the module carries): they compile, but executing one
// throws its SC-coded error — the report lists them so a "compiling"
// JS program never hides its dynamic gaps.
// Fences in a module's top-level code run when the module initializes:
// they are listed first, as sites that run at startup.
const fences = input.runtimeFences ?? [];
if (fences.length > 0) {
const startup = fences.filter((d) => d.startup === true);
const later = fences.filter((d) => d.startup !== true);
if (startup.length > 0) {
out.push(
` ${c(YELLOW, "deferred to runtime")} ${fences.length} site${fences.length === 1 ? "" : "s"} ${c(DIM, "(JS statements that throw their fence if executed)")}`,
` ${c(YELLOW, "runs at startup")} ${startup.length} site${startup.length === 1 ? "" : "s"} ${c(DIM, "(JS statements in module top-level code, which runs when the module loads)")}`,
);
renderRows(groupBlockers(fences, "deferred"), YELLOW);
renderRows(groupBlockers(startup, "deferred"), YELLOW);
out.push("");
}
if (later.length > 0) {
out.push(
` ${c(YELLOW, "deferred to runtime")} ${later.length} site${later.length === 1 ? "" : "s"} ${c(DIM, "(JS statements that throw their fence if executed)")}`,
);
renderRows(groupBlockers(later, "deferred"), YELLOW);
out.push("");
}
@@ -92,6 +92,10 @@ export interface ScrDiagnostic {
* the error may not reproduce under the project's tsc. Machine-readable
* output reports these under the "environment" category. */
typeWorld?: boolean;
/** A runtime fence in a module's top-level code, outside every function
* and class member body: it throws when the module initializes, which is
* program startup for a module the entry imports. */
startup?: true;
}
/** SC3002–SC3004 — native-helper target, installation, and execution
@@ -67,6 +67,9 @@ export interface LocatedDiagnostic extends SourcePosition {
hint: string;
/** The id of the root-cause group this site belongs to. */
group: string;
/** Deferred scope only: the statement is module top-level code, so it
* runs when its module loads. */
startup?: true;
}
export interface DiagnosticGroup {
@@ -290,6 +293,7 @@ export function locateDiagnostic(
message: diag.message,
hint: hintOf(diag),
group: groupId(diag, scope),
...(scope === "deferred" && diag.startup === true ? { startup: true as const } : {}),
};
}
@@ -115,6 +115,8 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo
// "pipe"`) — accepted when its TYPE proves every arm is a supported
// literal; the runtime maps the value to the modes at the call.
let stdioStr: IrExpr | null = null;
// The `env` option's [k,v,...] pairs, which replace the child's environment.
let envPairs: IrExpr | null = null;
let plain = true; // no behavior-changing option: the historical libCall
if (optsNode) {
@@ -227,11 +229,16 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo
case "windowsHide":
lowerer.lowerExpr(m.value); // Node no-op on POSIX
break;
case "env":
// A REPLACEMENT environment, as for spawn and execFileSync.
envPairs = recordToEnvPairs(lowerer, m.value);
plain = false;
break;
default:
lowerer.noLowering(
`spawnSync option '${m.name}'`,
p,
"encoding, timeout, killSignal, stdio, and windowsHide are the supported options",
"encoding, timeout, killSignal, stdio, env, and windowsHide are the supported options",
);
}
}
@@ -239,6 +246,13 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo
if (plain) {
return { kind: "libCall", fn: "cp.spawnSync", args: [cmd, argv], type: SPAWNRES_T, loc };
}
if (stdioStr !== null && envPairs !== null) {
lowerer.noLowering(
"spawnSync with a runtime stdio value and env",
optsNode!,
'pass stdio as a "pipe"/"ignore"/"inherit" literal when env is set',
);
}
if (stdioStr !== null) {
return {
kind: "libCall",
@@ -250,7 +264,7 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo
}
return {
kind: "libCall",
fn: "cp.spawnSyncOpts",
fn: envPairs !== null ? "cp.spawnSyncEnv" : "cp.spawnSyncOpts",
args: [
cmd,
argv,
@@ -259,6 +273,7 @@ export function lowerSpawnSyncCall(lowerer: Lowerer, expr: ts.CallExpression, lo
numLit(inMode, loc),
numLit(outMode, loc),
numLit(errMode, loc),
...(envPairs !== null ? [envPairs] : []),
],
type: SPAWNRES_T,
loc,
@@ -702,6 +702,8 @@ export function lowerCryptoModuleCall(
}
const pki = lowerPkiCall(lowerer, expr, bi.member, args, loc);
if (pki) return pki;
const prime = lowerPrimeCall(lowerer, expr, bi.member, args, loc);
if (prime) return prime;
if (bi.member === "createSecretKey") {
if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement))
lowerer.noLowering(
@@ -867,11 +869,29 @@ const IMPLEMENTED_CIPHERS = new Set([
"id-aes192-gcm",
"id-aes256-gcm",
"chacha20-poly1305",
...["128", "192", "256"].flatMap((bits) =>
["ecb", "cfb", "cfb8", "cfb1", "ofb"].map((mode) => `aes-${bits}-${mode}`),
),
"des-ede3-cbc",
"des3",
"des-ede3",
"des-ede3-ecb",
"des-ede3-cfb",
"des-ede3-cfb8",
"des-ede3-cfb1",
"des-ede3-ofb",
"des-ede-cbc",
"des-ede",
"des-ede-ecb",
"des-ede-cfb",
"des-ede-ofb",
"chacha20",
]);
const SUPPORTED_CIPHERS_HINT =
"the supported ciphers are aes-128-cbc, aes-192-cbc, aes-256-cbc, aes-128-ctr, aes-192-ctr, " +
"aes-256-ctr, aes-128-gcm, aes-192-gcm, aes-256-gcm, and chacha20-poly1305";
"the supported ciphers are 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";
/** A literal cipher name that Node knows but the runtime does not
* implement is refused at compile time. Unknown literal names keep Node's
@@ -1346,6 +1366,59 @@ export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression):
return false;
}
/** Node calls checkPrime's and generatePrime's callbacks with undefined as
* the error, where @types/node declares Error | null: whether this
* callback's error parameter can hold undefined. One that cannot receives
* null instead. */
function takesUndefinedError(lowerer: Lowerer, callback: ts.Expression): boolean {
const type = lowerer.irTypeOf(callback);
if (type.kind !== "func") return true;
const error = type.params[0];
if (error === undefined || error.kind === "dyn" || error.kind === "undefinedT") return true;
if (error.kind !== "union") return false;
return !!lowerer.unions.get(error.unionId)?.arms.some((arm) => arm.kind === "undefinedT");
}
/** checkPrime, checkPrimeSync, generatePrime and generatePrimeSync
* (scr_dh.c): checked-dynamic arguments, which the runtime checks as Node
* does (a function in the options position is the callback), and the
* sync forms' boolean, ArrayBuffer or bigint converted to the call site's
* type. Null for other members. */
function lowerPrimeCall(
lowerer: Lowerer,
expr: ts.CallExpression,
member: string,
args: readonly ts.Expression[],
loc: SrcLoc,
): IrExpr | null {
const forms: Readonly<Record<string, readonly [IrLibFn, number, number, string]>> = {
checkPrimeSync: ["crypto.checkPrimeSync", 1, 2, "checkPrimeSync(candidate[, options])"],
checkPrime: ["crypto.checkPrimeCb", 1, 3, "checkPrime(candidate[, options], callback)"],
generatePrimeSync: ["crypto.generatePrimeSync", 1, 2, "generatePrimeSync(size[, options])"],
generatePrime: ["crypto.generatePrimeCb", 1, 3, "generatePrime(size[, options], callback)"],
};
const form = forms[member];
if (!form) return null;
const [fn, min, max, supported] = form;
if (args.length < min || args.length > max || args.some(ts.isSpreadElement))
lowerer.noLowering(
`crypto.${member} with ${args.length} arguments`,
expr,
`${supported} is supported`,
);
const values = Array.from({ length: max }, (_, i) => dynArg(lowerer, args[i], loc));
if (fn === "crypto.checkPrimeCb" || fn === "crypto.generatePrimeCb") {
const callback = args[args.length - 1];
const nullError = callback !== undefined && !takesUndefinedError(lowerer, callback);
values.push({ kind: "boolLit", value: nullError, type: BOOL, loc });
return { kind: "libCall", fn, args: values, type: VOID, loc };
}
const call: IrExpr = { kind: "libCall", fn, args: values, type: DYN, loc };
const result = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (result === null || result.kind === "dyn") return call;
return lowerer.coerceInto(expr, call, result);
}
/** The asymmetric-key and signature members (scr_pki.c): checked-dynamic
* arguments, and the result converted to the call site's type
* (KeyObject, Sign and Verify are checked-dynamic handles; key pairs,
@@ -25,16 +25,21 @@ const PREFIX = "%class.construct:";
const PACKED = `${PREFIX}packed`;
const INSTANCEOF = "%class.instanceof:dynamic";
/** `value instanceof callee` for an untyped constructor value, through one
* shared helper. `refusal` is the site's message for a runtime
* constructor with no instance test. */
export function checkedClassInstanceOf(
lowerer: Lowerer,
value: IrExpr,
callee: IrExpr,
refusal: string,
loc: SrcLoc,
): IrExpr {
if (!lowerer.liftedFns.some((fn) => fn.name === INSTANCEOF)) {
const params = [
{ localId: "value", name: "value", type: DYN },
{ localId: "callee", name: "callee", type: DYN },
{ localId: "refusal", name: "refusal", type: STRING },
];
lowerer.liftedFns.push({
name: INSTANCEOF,
@@ -47,7 +52,11 @@ export function checkedClassInstanceOf(
value: {
kind: "libCall",
fn: "bytes.instanceOf",
args: [varRef("value", DYN, loc), varRef("callee", DYN, loc)],
args: [
varRef("value", DYN, loc),
varRef("callee", DYN, loc),
varRef("refusal", STRING, loc),
],
type: BOOL,
loc,
},
@@ -57,7 +66,13 @@ export function checkedClassInstanceOf(
loc,
});
}
return { kind: "call", callee: INSTANCEOF, args: [value, callee], type: BOOL, loc };
return {
kind: "call",
callee: INSTANCEOF,
args: [value, callee, { kind: "strLit", value: refusal, type: STRING, loc }],
type: BOOL,
loc,
};
}
/** Checked construction dispatches to emitted native constructor thunks. */
@@ -59,6 +59,7 @@ import {
} from "./array-iteration.js";
import { dynamicReceiverThrows } from "./dynamic-receivers.js";
import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "../optional-arguments.js";
import { checkedFilterNarrowing } from "./filter-predicates.js";
/** `a.map(fn)` / `a.filter(fn)` / `a.forEach(fn)` desugar to a direct
* call of a synthetic module function — one per method + element/result
@@ -123,10 +124,11 @@ export function lowerArrayHofCall(
}
lowerer.badType(argNode, lowerer.typeOf(argNode));
}
const outElem =
const filtered =
method === "filter"
? filterResultElem(lowerer, call, argNode, elem, valueT, booleanFilter)
: callbackArrayElement(lowerer, call, fnRet);
: null;
const outElem = filtered !== null ? filtered.elem : callbackArrayElement(lowerer, call, fnRet);
// A filter that re-tags its elements keeps the single union-typed visit.
const fast =
present || booleanFilter || (method === "filter" && !typeEquals(outElem, elem))
@@ -145,19 +147,33 @@ export function lowerArrayHofCall(
present,
);
const resultType: IrType = method === "map" || method === "filter" ? arrayOf(outElem) : VOID;
return {
const result: IrExpr = {
kind: "call",
callee: helper,
args: [receiver, fnArg, ...(fast ? [fast] : [])],
type: resultType,
loc,
};
const narrowing = filtered?.narrowing ?? null;
if (narrowing === null) return result;
return {
kind: "call",
callee: narrowing.helper,
args: [result],
type: arrayOf(narrowing.elem),
loc,
};
}
/** The checker-selected element of a filter result, with the same trust
* boundary as the historical narrowing filter: an inferred inline type
* predicate may prove one source arm; a written predicate annotation may
* not. Boolean is safe when every discarded arm is a nullish unit. */
/** The element a filter keeps, and the checked conversion of the result
* to the checker's narrower element when the predicate does not prove it.
* An inferred inline type predicate proves one source arm, or a class
* view, so the loop keeps that type directly. A written predicate (`(p): p
* is Cat => ...`, a guard function passed as a value) is an assertion
* TypeScript does not check, and an inferred predicate that keeps several
* arms has no single arm to re-tag: the loop keeps the receiver's element
* and `narrowing` (checkedFilterNarrowing) converts each kept element.
* Boolean is safe when every discarded arm is a nullish unit. */
function filterResultElem(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -165,7 +181,37 @@ function filterResultElem(
elem: IrType,
valueT: IrType,
booleanFilter: boolean,
): IrType {
): { elem: IrType; narrowing: { helper: string; elem: IrType } | null } {
const kept = filterKeptElem(lowerer, call, argNode, elem, valueT, booleanFilter);
if (typeof kept !== "string") return { elem: kept, narrowing: null };
return { elem, narrowing: { helper: kept, elem: filterCheckerElem(lowerer, call, elem) } };
}
/** The checker's spelling of a filter result's element type. */
function filterTypeText(lowerer: Lowerer, call: ts.CallExpression): string {
const result = lowerer.typeOf(call);
const element = lowerer.checker.isArrayType(result)
? lowerer.checker.getTypeArguments(result as ts.TypeReference)[0]
: undefined;
return element === undefined ? "" : lowerer.checker.typeToString(element);
}
/** The checker's element of a filter result, or the receiver's when the
* filter introduces no refinement. */
function filterCheckerElem(lowerer: Lowerer, call: ts.CallExpression, elem: IrType): IrType {
const result = lowerer.mapTypeOf(lowerer.typeOf(call));
return result?.kind === "array" ? result.elem : elem;
}
/** filterResultElem's element, or the name of the checked narrowing helper. */
function filterKeptElem(
lowerer: Lowerer,
call: ts.CallExpression,
argNode: ts.Expression,
elem: IrType,
valueT: IrType,
booleanFilter: boolean,
): IrType | string {
const callT = lowerer.typeOf(call);
const result = lowerer.mapTypeOf(callT);
let outElem = result?.kind === "array" ? result.elem : elem;
@@ -191,23 +237,31 @@ function filterResultElem(
"keep the receiver's element type instead — annotate the callback's return ': boolean' " +
"(the checker then skips the predicate) or annotate the result with the receiver's own " +
"element type — and narrow the elements after";
if (!booleanFilter) {
const written =
!booleanFilter &&
((!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) ||
argNode.type !== undefined);
const narrowing = booleanFilter
? null
: (): string | null =>
checkedFilterNarrowing(lowerer, elem, outElem, filterTypeText(lowerer, call), locOf(call));
if (written) {
const helper = narrowing!();
if (helper !== null) return helper;
if (!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) {
lowerer.unsupported(
"SC1090",
argNode,
`narrowing '.filter' through a callback VALUE (only an inline callback whose predicate ` +
`the checker inferred can re-tag — ${annotateEscape})`,
);
}
if (argNode.type) {
lowerer.unsupported(
"SC1090",
argNode,
`narrowing '.filter' with a hand-written type predicate (a written 'x is T' is an ` +
`unchecked assertion nothing validates at runtime — ${annotateEscape})`,
`narrowing '.filter' to '${lowerer.fmt(outElem)}' through a callback VALUE (no checked ` +
`conversion from '${lowerer.fmt(elem)}' — ${annotateEscape})`,
);
}
lowerer.unsupported(
"SC1090",
argNode,
`narrowing '.filter' to '${lowerer.fmt(outElem)}' with a hand-written type predicate (no ` +
`checked conversion from '${lowerer.fmt(elem)}' — ${annotateEscape})`,
);
}
const classRefinement =
!booleanFilter && outElem.kind === "object" && canAssertClassValue(lowerer, valueT, outElem);
@@ -215,6 +269,8 @@ function filterResultElem(
!classRefinement &&
(valueT.kind !== "union" || lowerer.armTag(valueT.unionId, outElem) < 0)
) {
const helper = narrowing?.() ?? null;
if (helper !== null) return helper;
const multiArm = outElem.kind === "union";
lowerer.unsupported(
"SC1090",
@@ -2650,6 +2706,7 @@ export function lowerTuplePredicateCall(
// the value type an array find narrows from.
let foundT: IrType = elem;
let retag: string | null = null;
let narrowing: { helper: string; elem: IrType } | null = null;
if (method === "filter") {
// The element union is built in position order, so the checker's
// result element can list the same arms in another order.
@@ -2660,9 +2717,11 @@ export function lowerTuplePredicateCall(
checkerResult?.kind === "array" &&
arms(checkerResult.elem).length === arms(elem).length &&
arms(checkerResult.elem).every((arm) => arms(elem).some((other) => typeEquals(arm, other)));
outElem = unchanged
? elem
: filterResultElem(lowerer, call, argNode, elem, elem, booleanFilter);
if (!unchanged) {
const filtered = filterResultElem(lowerer, call, argNode, elem, elem, booleanFilter);
outElem = filtered.elem;
narrowing = filtered.narrowing;
}
resultT = arrayOf(outElem);
} else if (method === "find") {
resultT = lowerer.irTypeOf(call);
@@ -2795,5 +2854,19 @@ export function lowerTuplePredicateCall(
loc,
});
}
return { kind: "call", callee: helper, args: [receiver, fnArg], type: resultT, loc };
const result: IrExpr = {
kind: "call",
callee: helper,
args: [receiver, fnArg],
type: resultT,
loc,
};
if (narrowing === null) return result;
return {
kind: "call",
callee: narrowing.helper,
args: [result],
type: arrayOf(narrowing.elem),
loc,
};
}
@@ -53,6 +53,8 @@ export function lowerArrayCallback(
bindUntyped = false,
expectedReturn?: IrType,
): { fnArg: IrExpr & { type: IrType & { kind: "func" } }; arity: number } {
// `xs.map(((x) => x))`: a parenthesized callback is the function itself.
while (ts.isParenthesizedExpression(argNode)) argNode = argNode.expression;
// The ambient `Boolean` as the callback (`flags.every(Boolean)`,
// `xs.some(Boolean)`, `xs.map(Boolean)`): Boolean(value) answers the
// ToBoolean of its FIRST argument and ignores the rest, so the callback
@@ -0,0 +1,261 @@
/* Checked narrowing for `.filter` with a written type predicate.
*
* A written predicate (`(p): p is Cat => ...`, or a guard function passed
* as a value) is an assertion TypeScript does not check, so the filter loop
* keeps the receiver's element type and the result converts to the
* predicate's type afterwards, element by element:
*
* - a union element to one of its arms, or to a union of some of its
* arms, by its tag;
* - a checked-dynamic element by dynCheck;
* - a class instance to a subclass by its class;
* - a tuple or record to one with the same fields, field by field.
*
* Each conversion throws the catchable TypeError of a checked extraction
* when a kept element is not of the predicate's type, which is when the
* predicate returned true for a value it does not describe. */
import { varRef } from "../../../ir/build.js";
import {
BOOL,
F64,
STRING,
isUnitType,
typeEquals,
type IrExpr,
type IrStmt,
type IrType,
type SrcLoc,
} from "../../../ir/ir.js";
import { checkedClassAssertion } from "../class-assertions.js";
import { buildArrayConversion } from "../coercions/builders.js";
import type { Lowerer } from "../lowerer.js";
import { keyedHelperName } from "../stable-names.js";
import { typeKey } from "../../type-mapper.js";
/** The interned helper converting a `from[]` filter result to `to[]`, or
* null when an element of `from` has no checked conversion to `to`.
* `typeText` names the predicate's type in the TypeError. */
export function checkedFilterNarrowing(
lowerer: Lowerer,
from: IrType,
to: IrType,
typeText: string,
loc: SrcLoc,
): string | null {
const element = elementHelper(lowerer, from, to, loc, typeText);
if (element === null) return null;
const key = `filter.narrow:${typeKey(from)}:${typeKey(to)}:${typeText}`;
const existing = lowerer.valueHelpers.get(key);
if (existing) return existing;
const name = keyedHelperName(lowerer, "%arr.narrow", key);
lowerer.valueHelpers.set(key, name);
const outT: IrType = { kind: "array", elem: to };
lowerer.liftedFns.push(
buildArrayConversion(
name,
{ kind: "array", elem: from },
{ kind: "arrayLit", elems: [], type: outT, loc },
(value, _index, result) => ({
kind: "arrIntrinsic",
method: "push",
receiver: result,
args: [{ kind: "call", callee: element, args: [value], type: to, loc }],
type: F64,
loc,
}),
loc,
),
);
return name;
}
/** The interned helper converting one `from` value to `to`, or null. */
function elementHelper(
lowerer: Lowerer,
from: IrType,
to: IrType,
loc: SrcLoc,
typeText = lowerer.fmt(to),
): string | null {
const key = `filter.narrow.value:${typeKey(from)}:${typeKey(to)}:${typeText}`;
const existing = lowerer.valueHelpers.get(key);
if (existing) return existing;
// A recursive type pair converts through the helper being built.
if (lowerer.coercions.planning.has(key)) return null;
lowerer.coercions.planning.add(key);
const input = varRef("v.0", from, loc);
let body: IrStmt[] | null;
try {
body = conversionBody(lowerer, input, from, to, typeText, loc);
} finally {
lowerer.coercions.planning.delete(key);
}
if (body === null) return null;
const name = keyedHelperName(lowerer, "%narrow.checked", key);
lowerer.valueHelpers.set(key, name);
lowerer.liftedFns.push({
name,
params: [{ localId: "v.0", name: "v", type: from }],
returnType: to,
locals: [{ id: "v.0", name: "v", type: from, mutable: false }],
body,
loc,
});
return name;
}
/** A converted value: `value` itself when the types are equal, or a call of
* the conversion helper. */
function converted(
lowerer: Lowerer,
value: IrExpr,
from: IrType,
to: IrType,
loc: SrcLoc,
): IrExpr | null {
if (typeEquals(from, to)) return value;
const helper = elementHelper(lowerer, from, to, loc);
return helper === null ? null : { kind: "call", callee: helper, args: [value], type: to, loc };
}
/** The TypeError a kept element of another type throws. */
function typeError(message: string, loc: SrcLoc): IrStmt {
return {
kind: "throw",
value: {
kind: "libCall",
fn: "error.new",
args: [{ kind: "strLit", value: message, type: STRING, loc }],
type: { kind: "object", className: "%TypeError" },
loc,
},
loc,
};
}
function conversionBody(
lowerer: Lowerer,
input: IrExpr,
from: IrType,
to: IrType,
typeText: string,
loc: SrcLoc,
): IrStmt[] | null {
if (typeEquals(from, to)) return [{ kind: "return", value: input, loc }];
if (from.kind === "dyn")
return [{ kind: "return", value: { kind: "dynCheck", value: input, type: to, loc }, loc }];
if (from.kind === "object" && to.kind === "object") {
const asserted = checkedClassAssertion(lowerer, input, to, loc);
return asserted === null ? null : [{ kind: "return", value: asserted, loc }];
}
if (from.kind === "union")
return unionConversion(lowerer, input, from.unionId, to, typeText, loc);
if (from.kind === "record" && to.kind === "record") {
const source = lowerer.shapes.get(from.shapeId);
const target = lowerer.shapes.get(to.shapeId);
if (
!source ||
!target ||
source.tuple !== target.tuple ||
source.indexValue ||
target.indexValue
)
return null;
if (source.fields.length !== target.fields.length) return null;
const fields: { name: string; value: IrExpr }[] = [];
for (const field of target.fields) {
const own = source.fields.find((f) => f.name === field.name);
if (!own) return null;
const read: IrExpr = {
kind: "recordGet",
obj: input,
shapeId: from.shapeId,
field: field.name,
type: own.type,
loc,
};
const value = converted(lowerer, read, own.type, field.type, loc);
if (value === null) return null;
fields.push({
name: field.name,
value: keepAbsence(lowerer, input, from.shapeId, field, value, loc),
});
}
// The copy keeps the source's own keys in the source's order, as a width
// copy does (key-order-witness.ts).
const copy: IrExpr = { kind: "recordLit", fields, type: to, loc };
return [{ kind: "return", value: lowerer.keyOrder.copy(copy, from.shapeId), loc }];
}
return null;
}
/** A converted field value that keeps the source field's absence: an
* optional field the source object lacks stays absent in the copy rather
* than becoming a property that holds undefined. */
function keepAbsence(
lowerer: Lowerer,
input: IrExpr,
shapeId: string,
field: { name: string; type: IrType },
value: IrExpr,
loc: SrcLoc,
): IrExpr {
if (field.type.kind !== "union") return value;
if (!lowerer.unions.get(field.type.unionId)?.arms.some((arm) => arm.kind === "undefinedT"))
return value;
return {
kind: "ternary",
cond: { kind: "recordHas", obj: input, shapeId, field: field.name, type: BOOL, loc },
then: value,
else_: { kind: "fieldAbsent", unionId: field.type.unionId, type: field.type, loc },
type: field.type,
loc,
};
}
/** A union value to one of its arms, or to a union of some of its arms:
* each arm the target holds returns, any other arm throws. */
function unionConversion(
lowerer: Lowerer,
input: IrExpr,
unionId: string,
to: IrType,
typeText: string,
loc: SrcLoc,
): IrStmt[] | null {
const source = lowerer.unions.get(unionId);
if (!source) return null;
const targetArms = to.kind === "union" ? (lowerer.unions.get(to.unionId)?.arms ?? null) : [to];
if (targetArms === null) return null;
const body: IrStmt[] = [];
let kept = 0;
for (let tag = 0; tag < source.arms.length; tag++) {
const arm = source.arms[tag]!;
const targetTag = targetArms.findIndex((t) => typeEquals(t, arm));
if (targetTag < 0) continue;
kept++;
const payload: IrExpr = isUnitType(arm)
? { kind: "unitLit", unit: arm.kind === "undefinedT" ? "undefined" : "null", type: arm, loc }
: { kind: "unionNarrow", unionId, tag, value: input, type: arm, loc };
const value: IrExpr =
to.kind === "union"
? { kind: "unionWrap", unionId: to.unionId, tag: targetTag, value: payload, type: to, loc }
: payload;
body.push({
kind: "if",
cond: { kind: "unionIsTag", unionId, tag, negated: false, value: input, type: BOOL, loc },
then: [{ kind: "return", value, loc }],
else_: null,
loc,
});
}
// Every arm the target names must be one of the union's own arms.
if (kept === 0 || kept !== targetArms.length) return null;
body.push(
typeError(
`a kept element is not a '${typeText}' (a type predicate returned true for a value of another type)`,
loc,
),
);
return body;
}

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