mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Merge main
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -11,6 +11,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any |
|
||||
| `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` |
|
||||
| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner |
|
||||
| `dictionary-walks` | cpu | `Record<string, number>` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted |
|
||||
| `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 |
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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,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,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,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,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,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,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,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,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",
|
||||
|
||||
+60
-2
@@ -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.
@@ -56,3 +56,18 @@ void scr_collect_cycles_phase(void);
|
||||
void scriptc_native_collect_cycles(void) {
|
||||
scr_collect_cycles_phase();
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"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": "compilerNativeBoundCycleGrowth", "symbol": "scriptc_native_bound_cycle_growth", "params": [], "returns": "void" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,6 +27,8 @@ 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";
|
||||
|
||||
const SCR_ARR_VALUE = 1;
|
||||
@@ -209,26 +211,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 +316,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 +339,7 @@ export function emitDenseArrayGet(
|
||||
slow,
|
||||
true,
|
||||
valueLabel,
|
||||
slotWidth(acc),
|
||||
slotWidth(kind),
|
||||
proven,
|
||||
induction,
|
||||
);
|
||||
@@ -333,7 +350,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 +364,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 +471,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 +491,7 @@ export function emitDenseArraySet(
|
||||
slow,
|
||||
false,
|
||||
filledLabel,
|
||||
slotWidth(acc),
|
||||
slotWidth(kind),
|
||||
proven,
|
||||
);
|
||||
const { offset, len } = guard;
|
||||
@@ -478,15 +502,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 +566,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 +578,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 +607,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 +646,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 +686,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 +725,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)}`);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ import { INT32_ABI_SUFFIX, SCALAR_UNION_RESULT } from "./int32-abi.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { RESULT_ENTRY_PREFIX } from "./record-results.js";
|
||||
import { emitIntegerClone, emitIntegerDispatcher } from "./integer-clones.js";
|
||||
import { runningExtremumUpdates } from "./running-extrema.js";
|
||||
|
||||
export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
const B = new BlockBuilder();
|
||||
@@ -81,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();
|
||||
|
||||
@@ -35,9 +35,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";
|
||||
@@ -456,15 +458,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 +484,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 +555,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()`] : []),
|
||||
@@ -842,7 +852,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()`] : []),
|
||||
@@ -1295,6 +1305,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
|
||||
@@ -1303,36 +1325,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";
|
||||
@@ -473,7 +473,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`);
|
||||
@@ -573,13 +573,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" &&
|
||||
@@ -786,7 +780,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);
|
||||
@@ -804,6 +803,16 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
emitCallbackVersionedLoop(emitter, s, callbacks);
|
||||
break;
|
||||
}
|
||||
const version =
|
||||
emitter.bytesLoopVersionsEnabled &&
|
||||
emitter.bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH &&
|
||||
!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) => {
|
||||
@@ -1001,35 +1010,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;
|
||||
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) emitter.provenCountedGuards.add(s);
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
} finally {
|
||||
emitter.bytesBounds = originalBounds;
|
||||
emitter.bytesOwned = originalOwned;
|
||||
emitter.integerRanges = originalRanges;
|
||||
emitter.provenCountedGuards.delete(s);
|
||||
emitter.bytesLoopVersioning.delete(s);
|
||||
}
|
||||
B.startBlock(done);
|
||||
emitVersionedLoop(emitter, s, version);
|
||||
break;
|
||||
}
|
||||
let countedJoin: string | null = null;
|
||||
@@ -2040,6 +2021,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;
|
||||
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) emitter.provenCountedGuards.add(s);
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
} finally {
|
||||
emitter.bytesBounds = originalBounds;
|
||||
emitter.bytesOwned = originalOwned;
|
||||
emitter.integerRanges = originalRanges;
|
||||
emitter.provenCountedGuards.delete(s);
|
||||
emitter.bytesLoopVersioning.delete(s);
|
||||
}
|
||||
B.startBlock(done);
|
||||
}
|
||||
|
||||
export function emitLoopCopies(
|
||||
emitter: LlEmitter,
|
||||
loop: Extract<IrStmt, { kind: "while" | "doWhile" | "for" }>,
|
||||
|
||||
@@ -72,6 +72,7 @@ import { scalarizeNumericRecords } from "../../ir/scalar-records.js";
|
||||
import { addRecordResultEntries } from "./record-results.js";
|
||||
import { specializeCallbackCalls } from "../../ir/callback-specialization.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";
|
||||
@@ -175,6 +176,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";
|
||||
|
||||
@@ -442,6 +444,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 +459,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 +493,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,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -641,9 +655,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
|
||||
@@ -835,6 +851,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). */
|
||||
@@ -1316,8 +1335,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;
|
||||
@@ -1326,11 +1347,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 ?? []) {
|
||||
@@ -1455,7 +1477,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
|
||||
@@ -1480,6 +1502,8 @@ export class LlEmitter {
|
||||
}
|
||||
}
|
||||
this.shapeHost = {
|
||||
compactArrayElem: (elem) => this.compactArrayElem(elem),
|
||||
compactRegion: () => this.compactRegion,
|
||||
sizeType: this.sizeType,
|
||||
cycleBufOffset: this.cycleBufOffset,
|
||||
tracedShapes: this.tracedShapes,
|
||||
@@ -1609,22 +1633,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
|
||||
@@ -2236,7 +2257,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();
|
||||
@@ -3599,6 +3620,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. */
|
||||
@@ -3863,7 +3908,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 = this.bytesLoopFunction;
|
||||
@@ -4046,7 +4091,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})`);
|
||||
}
|
||||
@@ -6196,8 +6241,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 {
|
||||
|
||||
@@ -455,11 +455,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 +477,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
|
||||
|
||||
@@ -31,8 +31,10 @@ import {
|
||||
emitMapIterCount,
|
||||
emitMapIterEntry,
|
||||
emitMapIterSlot,
|
||||
compactMapValues,
|
||||
mapEntryLayout,
|
||||
emitMapIterStart,
|
||||
emitMapSize,
|
||||
emitNumberMapGetRef,
|
||||
emitNumberMapGetScalar,
|
||||
emitNumberMapHas,
|
||||
@@ -102,13 +104,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 +122,7 @@ export function emitArrayValues(
|
||||
host: LlvmEmitterContext,
|
||||
arr: string,
|
||||
acc: "f64" | "bool" | "ref",
|
||||
element: IrType,
|
||||
values: LlValue[],
|
||||
prepend = false,
|
||||
sharedBuffer?: string,
|
||||
@@ -129,7 +136,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 +635,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 +667,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 +709,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 +778,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 +796,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 +1067,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 +1133,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 +1299,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 +1377,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 +1425,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 +1439,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 +1451,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 +1470,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 +1497,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 });
|
||||
}
|
||||
|
||||
@@ -1298,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})`);
|
||||
@@ -1308,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(
|
||||
@@ -879,7 +879,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 +924,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 +962,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 +977,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 };
|
||||
}
|
||||
|
||||
@@ -105,6 +105,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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -71,6 +71,12 @@ export interface ShapeHost {
|
||||
/** 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;
|
||||
@@ -564,14 +570,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 +1084,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
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,18 @@ import { everyStmtList } from "../../ir/traverse.js";
|
||||
import { toPrimitiveHint } from "./class-dynamic-dispatch.js";
|
||||
import { GENERATOR_STEP_METHODS, HELPER_METHODS } from "./generator-dynamic-dispatch.js";
|
||||
|
||||
/** Runtime entry points that step a checked-dynamic iterable's iterator:
|
||||
* destructuring rest, and the collection constructors' and
|
||||
* Object.fromEntries' seeds. */
|
||||
const RUNTIME_STEP_LIB_FNS: ReadonlySet<string> = new Set([
|
||||
"dyn.destructureRest",
|
||||
"dyn.nativeSetNew",
|
||||
"dyn.mapSeedEntries",
|
||||
"dyn.fromEntries",
|
||||
"weakMap.new",
|
||||
"weakSet.new",
|
||||
]);
|
||||
|
||||
/** What one function's body holds for the class dispatch pass and its
|
||||
* caught-value, generator and class construction sub-passes, collected in
|
||||
* one walk. Lists keep the order the walk meets their entries. */
|
||||
@@ -186,7 +198,7 @@ function scanSites(statements: IrStmt[]): FunctionSites {
|
||||
expr.key.kind === "strLit" &&
|
||||
HELPER_METHODS.has(expr.key.value)) ||
|
||||
expr.kind === "dynIterN" ||
|
||||
(expr.kind === "libCall" && expr.fn === "dyn.destructureRest")
|
||||
(expr.kind === "libCall" && RUNTIME_STEP_LIB_FNS.has(expr.fn))
|
||||
)
|
||||
sites.runtimeSteps = true;
|
||||
switch (expr.kind) {
|
||||
|
||||
@@ -58,6 +58,22 @@ export function jsHoldsDynObject(lowerer: Lowerer, expr: ts.Expression): boolean
|
||||
return false;
|
||||
}
|
||||
|
||||
/** An empty TypeScript literal whose own type is `never[]`: tsc's type for
|
||||
* `[]` with no contextual array type (`return []` in an async function,
|
||||
* whose contextual type is `string[] | PromiseLike<string[]>`; `{ files: [] }`).
|
||||
* It holds no element, as the empty tuple does, so it builds as the
|
||||
* unit-only element array, and an array slot receives it through the
|
||||
* checked empty-array lift. never's f64 element mapping would make it a
|
||||
* `number[]`, which no `string[]` slot accepts. */
|
||||
function emptyNeverLiteral(lowerer: Lowerer, expr: ts.ArrayLiteralExpression): boolean {
|
||||
if (expr.elements.nodes.length > 0 || isJsSourceFile(ts.getSourceFileOfNode(expr))) return false;
|
||||
const own = lowerer.typeOf(expr);
|
||||
return (
|
||||
lowerer.checker.isArrayType(own) &&
|
||||
(lowerer.checker.getTypeArguments(own as ts.TypeReference)[0]!.flags & ts.TypeFlags.Never) !== 0
|
||||
);
|
||||
}
|
||||
|
||||
/** `[a, b, c]`. The element type comes from the contextual type when tsc
|
||||
* has one (`const a: number[] = []`, arguments, nested literals) and from
|
||||
* the literal's own inferred type otherwise. A bare `[]` with no context
|
||||
@@ -80,6 +96,8 @@ export function lowerArrayLiteral(
|
||||
? lowerer.typeOf(expr)
|
||||
: (ctxType ?? lowerer.typeOf(expr));
|
||||
let mapped = expected ?? lowerer.mapTypeOf(tsType);
|
||||
if (expected === undefined && ctxType === undefined && emptyNeverLiteral(lowerer, expr))
|
||||
mapped = arrayOf(unitOnlyUnion(lowerer.unions));
|
||||
// Inferred JavaScript arrays can contain unrelated constructors with
|
||||
// structurally equal checker types. Their native class ABIs still differ.
|
||||
if (
|
||||
@@ -131,11 +149,14 @@ export function lowerArrayLiteral(
|
||||
// the literal's own inferred type is the real tuple.
|
||||
if (!mapped || mapped.kind === "union" || mapped.kind === "dyn") {
|
||||
const ctxUnion = mapped?.kind === "union" ? mapped : null;
|
||||
const ctxUnmapped = mapped === null;
|
||||
mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
// The never-tainted own type carries no element information here
|
||||
// either (the JS story above) — only a contextual union's single
|
||||
// array arm below may still supply a static home.
|
||||
if (neverTaintedOwnJs) mapped = null;
|
||||
if (ctxUnmapped && emptyNeverLiteral(lowerer, expr))
|
||||
mapped = arrayOf(unitOnlyUnion(lowerer.unions));
|
||||
// An EMPTY literal in a union slot (`const t: string[] | undefined =
|
||||
// []`, the `[]` default argument against `string[] | undefined`) has
|
||||
// no useful own type — tsc infers `never[]`, whose f64-element
|
||||
|
||||
@@ -666,14 +666,20 @@ export function lowerPropertyAccessExpr(
|
||||
);
|
||||
if (fixedArity) {
|
||||
// A `this` parameter is type-only: callers never pass it.
|
||||
return {
|
||||
kind: "numLit",
|
||||
value: (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter(
|
||||
(p) => !isThisParameter(p),
|
||||
).length,
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const declared = (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter(
|
||||
(p) => !isThisParameter(p),
|
||||
).length;
|
||||
const count = Math.max(declared, contextualArgumentCount(lowerer, fn) ?? 0);
|
||||
// Calls through the expression's own name pass the declared count,
|
||||
// calls through the wider slot pass more: no constant answers both.
|
||||
if (count > declared && referencesOwnName(lowerer, fn)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"'arguments.length' in a function expression that is called both through its own name and through a function type with more parameters (the count varies per call)",
|
||||
);
|
||||
}
|
||||
return { kind: "numLit", value: count, type: F64, loc };
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -4440,3 +4446,47 @@ function lowerPromiseConstructorRead(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether a named function expression refers to itself by name in its
|
||||
* body (a recursive call, or the function escaping under its own name). */
|
||||
function referencesOwnName(lowerer: Lowerer, fn: ts.Node): boolean {
|
||||
if (!ts.isFunctionExpression(fn) || fn.name === undefined || fn.body === undefined) return false;
|
||||
const self = lowerer.checker.getSymbolAtLocation(fn.name);
|
||||
if (self === undefined) return false;
|
||||
const name = fn.name.text;
|
||||
let found = false;
|
||||
ts.walkPreorder(fn.body, (node) => {
|
||||
if (found) return "stop";
|
||||
if (
|
||||
ts.isIdentifier(node) &&
|
||||
node.text === name &&
|
||||
lowerer.checker.getSymbolAtLocation(node) === self
|
||||
)
|
||||
found = true;
|
||||
return undefined;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/** The argument count every caller passes a function expression whose
|
||||
* value flows into one fixed-arity function type, an array method's
|
||||
* callback or an annotated slot: that type's parameter count. Null when
|
||||
* there is no such type or its arity varies. */
|
||||
function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | null {
|
||||
if (!ts.isFunctionExpression(fn)) return null;
|
||||
const contextual = lowerer.checker.getContextualType(fn);
|
||||
if (contextual === undefined) return null;
|
||||
const signatures = lowerer.checker.getCallSignatures(
|
||||
lowerer.checker.getNonNullableType(contextual),
|
||||
);
|
||||
if (signatures.length !== 1) return null;
|
||||
const declaration = signatures[0]!.declaration;
|
||||
if (declaration === undefined) return null;
|
||||
let count = 0;
|
||||
for (const parameter of (declaration.parameters?.nodes ?? []) as ts.ParameterDeclaration[]) {
|
||||
if (isThisParameter(parameter)) continue;
|
||||
if (parameter.questionToken || parameter.initializer || parameter.dotDotDotToken) return null;
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ import { InternalCompilerError } from "../../../errors.js";
|
||||
import { tryLowerExpression } from "./try-lower-expression.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { checkedClassInstanceOf } from "../class-construction.js";
|
||||
import { unsupportedDiag } from "../../../diagnostics/diagnostic.js";
|
||||
import { globalSymbolKey } from "./global-symbols.js";
|
||||
import { foldedStringKeyOf } from "./object-literals.js";
|
||||
import { NARROW_FIRST, sideEffectFreeOptionValue } from "../surfaces.js";
|
||||
@@ -490,7 +491,13 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
const constructor = tryLowerExpression(lowerer, expr.right);
|
||||
if (constructor?.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) {
|
||||
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
|
||||
return checkedClassInstanceOf(lowerer, value, constructor, loc);
|
||||
return checkedClassInstanceOf(
|
||||
lowerer,
|
||||
value,
|
||||
constructor,
|
||||
dynInstanceOfRefusal(lowerer, expr),
|
||||
loc,
|
||||
);
|
||||
}
|
||||
// `this instanceof Glob` in a JavaScript constructor function: the
|
||||
// function's prototype against the value's prototype chain.
|
||||
@@ -2000,3 +2007,25 @@ function lowerRuntimeKeyIn(
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [keyIr, recv], type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** Standard-library constructors the untyped instanceof helper has no
|
||||
* instance test for: an untyped `x instanceof Object` always refuses. */
|
||||
const DYN_INSTANCEOF_REFUSED: ReadonlySet<string> = new Set(["Object", "Function"]);
|
||||
|
||||
/** The SC1090 message `x instanceof C` throws, with the site's position,
|
||||
* when `C` is a runtime constructor with no instance test. A right-hand
|
||||
* side that names one of DYN_INSTANCEOF_REFUSED always reaches that
|
||||
* refusal, so the site is listed with the runtime fences. */
|
||||
function dynInstanceOfRefusal(lowerer: Lowerer, expr: ts.BinaryExpression): string {
|
||||
const diag = unsupportedDiag("SC1090", locOf(expr), "instanceof against this native constructor");
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(expr.right);
|
||||
if (
|
||||
symbol !== undefined &&
|
||||
lowerer.isStdlibSymbol(symbol) &&
|
||||
DYN_INSTANCEOF_REFUSED.has(ts.symbolName(symbol))
|
||||
)
|
||||
lowerer.runtimeFences.push(diag);
|
||||
const sf = ts.getSourceFileOfNode(expr);
|
||||
const line = ts.getLineAndCharacterOfPosition(sf, diag.loc.start).line + 1;
|
||||
return `${diag.message} [${diag.code} at ${diag.loc.file}:${line}]`;
|
||||
}
|
||||
|
||||
@@ -157,6 +157,36 @@ test("a synthesized construction pins today's table", () => {
|
||||
expect(f.order(ab)).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
test("a runtime builder typed by its use site pins today's table", () => {
|
||||
const f = fixture();
|
||||
const ab = f.record(
|
||||
[
|
||||
["a", F64],
|
||||
["b", F64],
|
||||
],
|
||||
["a", "b"],
|
||||
);
|
||||
// net.serverAddress's signature result is VOID: the call takes its record
|
||||
// type from its use site, and the runtime builds the record in that order.
|
||||
const built: IrExpr = { kind: "libCall", fn: "net.serverAddress", args: [], type: ab, loc };
|
||||
f.run([observe(f.literal(ab, ["b", "a"])), observe(built)]);
|
||||
expect(f.order(ab)).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
test("an always-throwing call typed as the record leaves its literals' order", () => {
|
||||
const f = fixture();
|
||||
const ab = f.record(
|
||||
[
|
||||
["a", F64],
|
||||
["b", F64],
|
||||
],
|
||||
["a", "b"],
|
||||
);
|
||||
const thrown: IrExpr = { kind: "libCall", fn: "error.nodeThrow", args: [], type: ab, loc };
|
||||
f.run([observe(f.literal(ab, ["b", "a"])), observe(thrown)]);
|
||||
expect(f.order(ab)).toEqual(["b", "a"]);
|
||||
});
|
||||
|
||||
test("a checked conversion alone pins today's table", () => {
|
||||
const f = fixture();
|
||||
const ab = f.record(
|
||||
|
||||
@@ -45,6 +45,7 @@ import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType } from "../../ir
|
||||
import { shapeHasAccessorSlots, typeEquals, UNDEFINED_T } from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild, everyTypeChild } from "../../ir/traverse.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { ALWAYS_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
|
||||
|
||||
/** One property of an object literal, in source order. */
|
||||
export type KeyOrderSegment =
|
||||
@@ -312,7 +313,11 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
// Runtime surfaces (console, util.format, workers) that take a
|
||||
// record may walk its keys; runtime builders make new records.
|
||||
for (const arg of e.args) observedRoots.push(arg.type);
|
||||
producedBy(e.type, true);
|
||||
// An always-throwing call stands in for a value (`error.nodeThrow`
|
||||
// in a narrowing ternary) and never produces one. Other calls
|
||||
// whose signature result is VOID are typed by their use site and
|
||||
// build the record at run time (`server.address()`).
|
||||
if (!ALWAYS_THROW_LIB_FNS.has(e.fn)) producedBy(e.type, true);
|
||||
return;
|
||||
case "intrinsic":
|
||||
// Intrinsics hand back values built elsewhere, except allSettled's
|
||||
|
||||
@@ -45,6 +45,7 @@ import { errorToStringCall } from "./error-methods.js";
|
||||
import { lowerClassPrototypeAssign, lowerClassPrototypeDescriptors } from "./class-prototypes.js";
|
||||
import {
|
||||
BYTES_ELEMENT_NAME,
|
||||
BYTES_U8,
|
||||
BIGINT_T,
|
||||
BOOL,
|
||||
CAUGHT,
|
||||
@@ -53,6 +54,7 @@ import {
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLibFn,
|
||||
type IrLocal,
|
||||
type IrParam,
|
||||
type IrStmt,
|
||||
@@ -76,6 +78,7 @@ import {
|
||||
} from "../../ir/ir.js";
|
||||
import { lowerProtocolMethodCall } from "./class-protocol-dispatch.js";
|
||||
import { isJsSourceFile, locOf, requireSpecOf } from "../program.js";
|
||||
import { isPackageShimFile, isShimIntrinsicName } from "../package-shims.js";
|
||||
import { lowerJsonRequireCall } from "./json-require.js";
|
||||
import { lowerAssertInstanceCall } from "./assert-instances.js";
|
||||
import {
|
||||
@@ -292,6 +295,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
const codeGeneration = codeGenerationCalleeOf(lowerer, expr.expression);
|
||||
if (codeGeneration !== null) return lowerCodeGenerationCall(lowerer, expr, codeGeneration);
|
||||
}
|
||||
// A package shim's runtime intrinsic (a bodiless `declare function`
|
||||
// named scriptcUnixDgram* in a shipped shim): a direct runtime call.
|
||||
if (ts.isIdentifier(expr.expression) && isShimIntrinsicName(expr.expression.text)) {
|
||||
const intrinsic = lowerShimIntrinsicCall(lowerer, expr, expr.expression, loc);
|
||||
if (intrinsic) return intrinsic;
|
||||
}
|
||||
// Inline require() of a program module (cjs-inline-require.ts): a
|
||||
// member call over its export table resolves like a binding's member
|
||||
// call after the module's %init at this position; the require itself
|
||||
@@ -1619,7 +1628,16 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
}
|
||||
const accessorTarget = lowerer.fieldTarget(expr.expression);
|
||||
if (accessorTarget?.container === "accessor") {
|
||||
const callee = lowerer.fieldGetExpr(accessorTarget, locOf(expr.expression), expr.expression);
|
||||
let callee = lowerer.fieldGetExpr(accessorTarget, locOf(expr.expression), expr.expression);
|
||||
// A getter returning a callable class instance (chalk's
|
||||
// `chalk.red("x")`): call through its `%call` field.
|
||||
if (callee.type.kind === "object") {
|
||||
const direct = callableInstanceStringCall(lowerer, expr, callee, locOf(expr));
|
||||
if (direct) return direct;
|
||||
callee = lowerer.hybridCallUnwrap(callee);
|
||||
if (callee.type.kind === "func")
|
||||
return lowerCallThroughValue(lowerer, expr, callee, locOf(expr));
|
||||
}
|
||||
if (callee.type.kind === "func") {
|
||||
const args = completeFuncValueArgs(lowerer, expr, callee.type, locOf(expr));
|
||||
return { kind: "callValue", callee, args, type: callee.type.ret, loc: locOf(expr) };
|
||||
@@ -1871,7 +1889,55 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
// a non-func IR type was already rejected while lowering the callee.
|
||||
// HYBRID (function-with-properties) values call through their %call slot.
|
||||
let callee = lowerer.lowerExpr(expr.expression);
|
||||
if (callee.type.kind === "record") callee = lowerer.hybridCallUnwrap(callee);
|
||||
{
|
||||
const direct = callableInstanceStringCall(lowerer, expr, callee, loc);
|
||||
if (direct) return direct;
|
||||
}
|
||||
if (callee.type.kind === "record" || callee.type.kind === "object")
|
||||
callee = lowerer.hybridCallUnwrap(callee);
|
||||
return lowerCallThroughValue(lowerer, expr, callee, loc);
|
||||
}
|
||||
|
||||
/** A callable class instance called with one string: its `%call1` method
|
||||
* when the class has one (the chalk shim's typed fast path), skipping the
|
||||
* variadic closure's checked-dynamic arguments. */
|
||||
function callableInstanceStringCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
callee: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (callee.type.kind !== "object" || expr.arguments.nodes.length !== 1) return null;
|
||||
const arg = expr.arguments.nodes[0]!;
|
||||
if (ts.isSpreadElement(arg)) return null;
|
||||
const info = lowerer.classes.get(callee.type.className);
|
||||
if (info === undefined || !info.fields.has("%call") || !lowerer.findMethodOn(info, "%call1"))
|
||||
return null;
|
||||
const value = lowerer.lowerExpr(arg);
|
||||
if (value.type.kind === "string")
|
||||
return lowerer.accessorCall(callee.type.className, "%call1", callee, [value], STRING, loc);
|
||||
// Any other argument (a number behind a cast): the variadic closure,
|
||||
// through the checked-dynamic call as lowerCallThroughValue would.
|
||||
const closure = lowerer.hybridCallUnwrap(callee);
|
||||
if (closure.type.kind !== "func") return null;
|
||||
return {
|
||||
kind: "dynCall",
|
||||
callee: { kind: "dynFrom", value: closure, type: DYN, loc },
|
||||
calleeName: ts.isIdentifier(expr.expression) ? expr.expression.text : "value",
|
||||
args: [lowerer.coerceToExpected(value, DYN)],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A call through an evaluated callee value: checked-dynamic callees,
|
||||
* function unions, typed-rest and runtime-arity closures. */
|
||||
function lowerCallThroughValue(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
callee: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
// A CHECKED-DYNAMIC callee — `fn(a, b)` where fn is an implicit-any
|
||||
// JS binding (the mustCall body's `fn(...args)`), a dyn capture, or a
|
||||
// keyed read off a dyn value: the dynCall boundary. Arguments convert
|
||||
@@ -9181,7 +9247,16 @@ export function lowerRecordFieldCall(
|
||||
// A HYBRID (function-with-properties) field is callable through its
|
||||
// reserved %call slot — `colors.blue("x")` where blue also carries
|
||||
// `.bold` (the chalk shape).
|
||||
if (callee.type.kind === "record") callee = lowerer.hybridCallUnwrap(callee);
|
||||
if (callee.type.kind === "object" && !init) {
|
||||
const direct = callableInstanceStringCall(lowerer, call, callee, loc);
|
||||
if (direct) return direct;
|
||||
// Other argument counts call the %call closure through its value, as
|
||||
// a call on the builder itself does (`theme.error("a", "b")`).
|
||||
const closure = lowerer.hybridCallUnwrap(callee);
|
||||
if (closure !== callee) return lowerCallThroughValue(lowerer, call, closure, loc);
|
||||
}
|
||||
if (callee.type.kind === "record" || callee.type.kind === "object")
|
||||
callee = lowerer.hybridCallUnwrap(callee);
|
||||
// Inferred JavaScript option records can retain checked callables in
|
||||
// fields even when the checker only describes those members as any.
|
||||
// TypeScript reaches here for callable library types that are checked
|
||||
@@ -10939,3 +11014,37 @@ function fenceClassCopyIntoFreshObject(
|
||||
`Object.assign copying a '${lowerer.fmt(rawSource.type)}' instance into a fresh object typed as the class (the copy is a plain object holding the instance's own fields, not an instance — build the object literal from the fields you need)`,
|
||||
);
|
||||
}
|
||||
|
||||
/** The runtime calls package shims declare as bodiless functions. */
|
||||
const SHIM_INTRINSICS: ReadonlyMap<string, { fn: IrLibFn; args: IrType[]; ret: IrType }> = new Map<
|
||||
string,
|
||||
{ fn: IrLibFn; args: IrType[]; ret: IrType }
|
||||
>([
|
||||
["scriptcUnixDgramSocket", { fn: "dgram.unixSocket", args: [], ret: F64 }],
|
||||
["scriptcUnixDgramConnect", { fn: "dgram.unixConnect", args: [F64, STRING], ret: F64 }],
|
||||
["scriptcUnixDgramSend", { fn: "dgram.unixSend", args: [F64, BYTES_U8], ret: F64 }],
|
||||
["scriptcUnixDgramClose", { fn: "dgram.unixClose", args: [F64], ret: F64 }],
|
||||
]);
|
||||
|
||||
function lowerShimIntrinsicCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
callee: ts.Identifier,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const intrinsic = SHIM_INTRINSICS.get(callee.text);
|
||||
if (intrinsic === undefined) return null;
|
||||
const decl = lowerer.checker.getSymbolAtLocation(callee)?.valueDeclaration;
|
||||
if (
|
||||
decl === undefined ||
|
||||
!ts.isFunctionDeclaration(decl) ||
|
||||
decl.body ||
|
||||
!isPackageShimFile(ts.getSourceFileOfNode(decl).fileName) ||
|
||||
expr.arguments.nodes.length !== intrinsic.args.length
|
||||
)
|
||||
return null;
|
||||
const args = expr.arguments.nodes.map((arg, i) =>
|
||||
lowerer.lowerExprExpecting(arg, intrinsic.args[i]!),
|
||||
);
|
||||
return { kind: "libCall", fn: intrinsic.fn, args, type: intrinsic.ret, loc };
|
||||
}
|
||||
|
||||
@@ -612,8 +612,10 @@ export function lowerDgramMethodCall(
|
||||
const fn: IrLibFn = data.type.kind === "string" ? "dgram.sendStr" : "dgram.sendBytes";
|
||||
return { kind: "libCall", fn, args: [receiver, data, port, host], type: VOID, loc };
|
||||
}
|
||||
// TypeScript reaches the ladder for the completion-callback form
|
||||
// send(msg, port, address, callback) (the hot-shots package shim).
|
||||
if (
|
||||
isJsSourceFile(ts.getSourceFileOfNode(call)) &&
|
||||
(isJsSourceFile(ts.getSourceFileOfNode(call)) || args.length === 4) &&
|
||||
args.length <= 6 &&
|
||||
!args.some(ts.isSpreadElement)
|
||||
) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js";
|
||||
import { declareTypedArguments } from "./typed-arguments.js";
|
||||
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
|
||||
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
type IrLocal,
|
||||
type IrParam,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
VOID,
|
||||
canBoxFuncIntoDyn,
|
||||
@@ -858,3 +860,79 @@ function isBufferCheckOf(e: IrExpr, slotId: string): boolean {
|
||||
e.type.elem === "u8"
|
||||
);
|
||||
}
|
||||
|
||||
/** The diagnostics of a TypeScript function whose signature did not
|
||||
* compile, for the coverage remainder: its statements lower for their
|
||||
* diagnostics only (the remainder's output is discarded), so the report
|
||||
* lists them with the signature's blocker instead of hiding them behind it.
|
||||
* Each parameter whose type maps is declared, and a use of any other
|
||||
* parameter reports SC2004 back to the declaration. The statements already
|
||||
* count as failed (countSkippedBody), so the pass leaves the stats alone.
|
||||
* The pass adds diagnostics only: an internal error while it lowers drops
|
||||
* what it found, and the signature's blocker stands alone as before. */
|
||||
export function blockedBodyDiagnostics(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.FunctionDeclaration,
|
||||
): ScrDiagnostic[] {
|
||||
const body = decl.body;
|
||||
if (!body || decl.typeParameters || decl.asteriskToken) return [];
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(decl))) return [];
|
||||
const symbol = declSymbolOf(lowerer, decl);
|
||||
if (!symbol || lowerer.fnSigsBySymbol.has(symbol)) return [];
|
||||
const diagsBefore = lowerer.diags.length;
|
||||
const fencesBefore = lowerer.runtimeFences.length;
|
||||
const suppressStats = lowerer.suppressStats;
|
||||
lowerer.suppressStats = true;
|
||||
let declared: IrType = DYN;
|
||||
lowersCleanly(lowerer, () => {
|
||||
declared = lowerer.declaredReturnType(decl, decl);
|
||||
});
|
||||
const isAsync = decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;
|
||||
const ctx = newFnCtx(false, null, null, lowerer.bodyReturnType(isAsync, declared));
|
||||
ctx.isAsync = isAsync;
|
||||
lowerer.fnStack.push(ctx);
|
||||
let internal = false;
|
||||
try {
|
||||
// A declared `this` parameter gives the body its receiver, as
|
||||
// lowerFunction declares it.
|
||||
if (decl.parameters.nodes.some(isThisParameter)) declareCallReceiver(lowerer, decl, false);
|
||||
for (const param of decl.parameters.nodes) {
|
||||
if (isThisParameter(param)) continue;
|
||||
const declaredParam = lowersCleanly(lowerer, () => {
|
||||
lowerer.declareParams([param], lowerer.paramShapes([param]));
|
||||
});
|
||||
if (declaredParam) continue;
|
||||
const symbol = ts.isIdentifier(param.name)
|
||||
? lowerer.checker.getSymbolAtLocation(param.name)
|
||||
: undefined;
|
||||
if (symbol) lowerer.blockedBindings.add(symbol);
|
||||
}
|
||||
lowersCleanly(lowerer, () => {
|
||||
lowerer.lowerStmts(body.statements.nodes);
|
||||
});
|
||||
} catch (error) {
|
||||
if (!(error instanceof InternalCompilerError)) throw error;
|
||||
internal = true;
|
||||
} finally {
|
||||
lowerer.fnStack.pop();
|
||||
lowerer.suppressStats = suppressStats;
|
||||
}
|
||||
lowerer.runtimeFences.splice(fencesBefore);
|
||||
const diags = lowerer.diags.splice(diagsBefore);
|
||||
return internal || diags.some((d) => d.code === "SC9001") ? [] : diags;
|
||||
}
|
||||
|
||||
/** Runs `lower`, for blockedBodyDiagnostics: false when it poisons, with the
|
||||
* diagnostics it recorded dropped (a failed mapping reports what the
|
||||
* signature's blocker already reports). */
|
||||
function lowersCleanly(lowerer: Lowerer, lower: () => void): boolean {
|
||||
const before = lowerer.diags.length;
|
||||
try {
|
||||
lower();
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (!(error instanceof PoisonError)) throw error;
|
||||
lowerer.diags.splice(before);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2929,6 +2929,38 @@ export function lowerDefaultExport(lowerer: Lowerer, stmt: ts.ExportAssignment):
|
||||
return { kind: "assign", localId: g.id, value, loc: locOf(stmt) };
|
||||
}
|
||||
|
||||
/** Marks the runtime fences recorded since `fencesBefore` that lie in
|
||||
* `sf`'s top-level code: they run when the module initializes. A fence
|
||||
* inside a function, or inside a class member that runs later (a method,
|
||||
* accessor, constructor or instance field initializer), runs when that
|
||||
* code is called. */
|
||||
function markModuleInitFences(lowerer: Lowerer, sf: ts.SourceFile, fencesBefore: number): void {
|
||||
for (let i = fencesBefore; i < lowerer.runtimeFences.length; i++) {
|
||||
const fence = lowerer.runtimeFences[i]!;
|
||||
if (fence.loc.file !== sf.fileName || runsAfterModuleInit(sf, fence.loc.start)) continue;
|
||||
lowerer.runtimeFences[i] = { ...fence, startup: true };
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the code at `pos` sits in a function or a later-running class
|
||||
* member body. */
|
||||
function runsAfterModuleInit(sf: ts.SourceFile, pos: number): boolean {
|
||||
let later = false;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (node.pos > pos || node.end <= pos) return "skip";
|
||||
if (
|
||||
ts.isFunctionLike(node) ||
|
||||
(ts.isPropertyDeclaration(node) &&
|
||||
!node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.StaticKeyword))
|
||||
) {
|
||||
later = true;
|
||||
return "stop";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return later;
|
||||
}
|
||||
|
||||
export function lowerFileInit(
|
||||
lowerer: Lowerer,
|
||||
sf: ts.SourceFile,
|
||||
@@ -2938,6 +2970,7 @@ export function lowerFileInit(
|
||||
const isAsync = lowerer.asyncInitFiles.has(sf);
|
||||
const ctx = newFnCtx(false, null, null, VOID);
|
||||
ctx.isAsync = isAsync;
|
||||
const fencesBefore = lowerer.runtimeFences.length;
|
||||
lowerer.fnStack.push(ctx);
|
||||
try {
|
||||
const loc0: SrcLoc = { file: sf.fileName, start: 0, end: 0 };
|
||||
@@ -3288,6 +3321,7 @@ export function lowerFileInit(
|
||||
};
|
||||
} finally {
|
||||
lowerer.fnStack.pop();
|
||||
markModuleInitFences(lowerer, sf, fencesBefore);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
* calls). */
|
||||
import { positionTag, type PositionTagOwner } from "./structural-positions.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { packageShimFor } from "../package-shims.js";
|
||||
import { isPackageShimFile, isShimIntrinsicName, packageShimFor } from "../package-shims.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { boundIdentifiersOf } from "./lowerer.js";
|
||||
import type { FileParts } from "./lower-modules.js";
|
||||
@@ -654,6 +654,12 @@ export function ambientUndefinedFnSymbolOf(
|
||||
const sym = lowerer.resolveValueSymbol(ident);
|
||||
if (!sym || !(sym.flags & ts.SymbolFlags.Function)) return null;
|
||||
if (lowerer.isStdlibSymbol(sym)) return null;
|
||||
// A package shim's runtime intrinsic (lower-calls.ts) is defined.
|
||||
if (
|
||||
isShimIntrinsicName(ident.text) &&
|
||||
sym.declarations.some((d) => isPackageShimFile(ts.getSourceFileOfNode(d).fileName))
|
||||
)
|
||||
return null;
|
||||
let sawFn = false;
|
||||
for (const d of sym.declarations) {
|
||||
if (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) continue; // type-world merge partners
|
||||
|
||||
@@ -4799,9 +4799,10 @@ export function lowerVarDeclList(
|
||||
* `never[]` or `any[]` and each later read by the elements written before
|
||||
* it, and reads inside closures as the unsettled array. When every read
|
||||
* with a settled element type maps to the same array type, the binding has
|
||||
* that type. When reads disagree or the binding is reassigned, it is
|
||||
* checked-dynamic, as the same binding is at module scope. Null for other
|
||||
* bindings. */
|
||||
* that type. When reads disagree, a write (`xs.push(v)`, `xs.unshift(v)`,
|
||||
* `xs.push(...values)`, `xs[i] = v`) stores a value that type's element does
|
||||
* not hold, or the binding is reassigned, it is checked-dynamic, as the same
|
||||
* binding is at module scope. Null for other bindings. */
|
||||
function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null {
|
||||
if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null;
|
||||
let initializer: ts.Expression = decl.initializer;
|
||||
@@ -4823,6 +4824,9 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy
|
||||
};
|
||||
const scope = owner(decl);
|
||||
let result: IrType | null = null;
|
||||
// The values the binding's writes store, checked against the element
|
||||
// type once the reads settle it.
|
||||
const written: ts.Expression[] = [];
|
||||
// The file's identifiers are indexed by name once (identifiersNamed), so
|
||||
// each binding resolves only the uses spelled like it.
|
||||
for (const node of identifiersNamed(lowerer, ts.getSourceFileOfNode(decl), decl.name.text)) {
|
||||
@@ -4842,6 +4846,22 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy
|
||||
parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
||||
parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment;
|
||||
if (reassigned) return DYN;
|
||||
if (
|
||||
ts.isPropertyAccessExpression(parent) &&
|
||||
parent.expression === target &&
|
||||
(parent.name.text === "push" || parent.name.text === "unshift") &&
|
||||
ts.isCallExpression(parent.parent) &&
|
||||
parent.parent.expression === parent
|
||||
)
|
||||
written.push(...parent.parent.arguments.nodes);
|
||||
else if (
|
||||
ts.isElementAccessExpression(parent) &&
|
||||
parent.expression === target &&
|
||||
ts.isBinaryExpression(parent.parent) &&
|
||||
parent.parent.left === parent &&
|
||||
parent.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
||||
)
|
||||
written.push(parent.parent.right);
|
||||
const t = lowerer.typeOf(node);
|
||||
if (unsettled(t)) continue;
|
||||
const mapped = lowerer.mapTypeOf(t);
|
||||
@@ -4849,7 +4869,49 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy
|
||||
return DYN;
|
||||
result = mapped;
|
||||
}
|
||||
return result ?? DYN;
|
||||
if (result === null || result.kind !== "array") return DYN;
|
||||
// The element types a write stores: its value's, or for a spread
|
||||
// (`xs.push(...values)`) the source array's element or tuple's elements.
|
||||
// Another spread source stores elements this check cannot see.
|
||||
const storedTypes = (value: ts.Expression): ts.Type[] | null => {
|
||||
if (!ts.isSpreadElement(value)) return [lowerer.typeOf(value)];
|
||||
const source = lowerer.typeOf(value.expression);
|
||||
if (lowerer.checker.isArrayType(source) || lowerer.checker.isTupleType(source))
|
||||
return [...lowerer.checker.getTypeArguments(source as ts.TypeReference)];
|
||||
return null;
|
||||
};
|
||||
for (const value of written) {
|
||||
const stored = storedTypes(value);
|
||||
if (stored === null) return DYN;
|
||||
for (const type of stored) {
|
||||
const t = lowerer.mapTypeOf(lowerer.checker.getBaseTypeOfLiteralType(type));
|
||||
if (!t || (!typeEquals(t, result.elem) && lowerer.widthLiftPlan(t, result.elem) === null))
|
||||
return DYN;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** An unannotated alias of an evolving array binding whose reads disagree
|
||||
* (`const alias = xs` between `xs.push(1)` and `xs.push("two")`): the
|
||||
* checker types the alias by the elements written before it, but both names
|
||||
* hold one array, so the alias takes the binding's checked-dynamic type and
|
||||
* sees the later writes. Aliases of such an alias follow it. Null for other
|
||||
* bindings. */
|
||||
function evolvingArrayAliasType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null {
|
||||
let current = decl;
|
||||
for (let depth = 0; depth < 8; depth++) {
|
||||
let initializer = current.initializer;
|
||||
while (initializer !== undefined && ts.isParenthesizedExpression(initializer))
|
||||
initializer = initializer.expression;
|
||||
if (initializer === undefined || !ts.isIdentifier(initializer)) return null;
|
||||
const source = lowerer.checker.getSymbolAtLocation(initializer)?.valueDeclaration;
|
||||
if (source === undefined || !ts.isVariableDeclaration(source) || source.type) return null;
|
||||
const evolved = evolvingArrayType(lowerer, source);
|
||||
if (evolved !== null) return evolved.kind === "dyn" ? evolved : null;
|
||||
current = source;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A single static type for an initializer-less JavaScript `let` whose
|
||||
@@ -5565,7 +5627,7 @@ export function lowerVarDecl(
|
||||
!decl.type &&
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(decl))
|
||||
) {
|
||||
const evolved = evolvingArrayType(lowerer, decl);
|
||||
const evolved = evolvingArrayType(lowerer, decl) ?? evolvingArrayAliasType(lowerer, decl);
|
||||
if (evolved !== null) {
|
||||
type = evolved;
|
||||
init = lowerer.lowerExprExpecting(decl.initializer, evolved);
|
||||
@@ -11484,11 +11546,13 @@ function lowerForIn(lowerer: Lowerer, stmt: ts.ForInStatement): IrStmt {
|
||||
keysT,
|
||||
loc,
|
||||
);
|
||||
// The overflow map's hashed key set answers presence directly; a
|
||||
// fresh key list per visit would make the walk quadratic.
|
||||
const guard = (kRef: IrExpr): IrExpr => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: "includes",
|
||||
receiver: { kind: "recordOvfKeys", obj: recvRef(), shapeId, type: keysT, loc },
|
||||
args: [kRef],
|
||||
kind: "recordOvfHas",
|
||||
obj: recvRef(),
|
||||
shapeId,
|
||||
key: kRef,
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
|
||||
@@ -230,6 +230,7 @@ import {
|
||||
arrayOf,
|
||||
BOOL,
|
||||
canAdaptDynFuncTo,
|
||||
canBoxFuncIntoDyn,
|
||||
canDynCheckTo,
|
||||
canConvertToDyn,
|
||||
DYN,
|
||||
@@ -445,6 +446,7 @@ import {
|
||||
lowerNestedFunctionDecl,
|
||||
lowerLambda,
|
||||
lowerFunction,
|
||||
blockedBodyDiagnostics,
|
||||
} from "./lower-functions.js";
|
||||
import { lowerArrayMethodCall } from "./containers/array-methods.js";
|
||||
import { type ArrayHofInfo, fuseArrayPipelines } from "./containers/array-pipelines.js";
|
||||
@@ -1347,6 +1349,7 @@ export function lowerToIr(
|
||||
// travel with them: a failed statement is never left unexplained.
|
||||
const covered: LowerResult = {
|
||||
...result,
|
||||
diagnostics: [...result.diagnostics, ...remainder.reachedBlockedBodyDiagnostics],
|
||||
stats: { ...result.stats, statementsDynamic },
|
||||
unreached: {
|
||||
diagnostics: rem.diagnostics,
|
||||
@@ -6621,7 +6624,10 @@ export class Lowerer {
|
||||
node.expression.name.text,
|
||||
node.arguments.nodes.length >= 2,
|
||||
);
|
||||
const callback = node.arguments.nodes[0];
|
||||
// `xs.map(((x) => x))`: the parenthesized callback is the arrow.
|
||||
let callback = node.arguments.nodes[0];
|
||||
while (callback !== undefined && ts.isParenthesizedExpression(callback))
|
||||
callback = callback.expression;
|
||||
if (callbackIndices !== null && callback !== undefined && !ts.isSpreadElement(callback)) {
|
||||
const receiverNode = node.expression.expression;
|
||||
const receiverTs = this.typeOf(receiverNode);
|
||||
@@ -6680,13 +6686,26 @@ export class Lowerer {
|
||||
presentSites.set(node, elementsPresent && !reduce);
|
||||
if (elementsPresent && reduce && retypeInlineCallback(callback, callbackIndices))
|
||||
changed = true;
|
||||
if (
|
||||
(method === "reduce" || method === "reduceRight") &&
|
||||
((!elementsPresent && node.arguments.nodes.length < 2) ||
|
||||
callbackReturnsOptional(callback))
|
||||
) {
|
||||
if (promoteHofCallback(callback, [0, 1])) changed = true;
|
||||
const result = this.runtimeOptionalType(this.irTypeOf(node));
|
||||
// The accumulator holds undefined when the callback can return
|
||||
// it, when an initial value can be missing, or when elements can
|
||||
// be absent and there is no initial value. It holds another
|
||||
// primitive when the callback's result can be one at run time
|
||||
// (`missing[0] ?? "fallback"`).
|
||||
const initial = node.arguments.nodes[1];
|
||||
const optional =
|
||||
(!elementsPresent && initial === undefined) ||
|
||||
(initial !== undefined && !ts.isSpreadElement(initial) && mayBeOptional(initial)) ||
|
||||
callbackReturnsOptional(callback);
|
||||
const arithmetic =
|
||||
reduce && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))
|
||||
? lambdaArithmeticReturn(callback)
|
||||
: null;
|
||||
if (reduce && (optional || arithmetic !== null)) {
|
||||
if (optional && promoteHofCallback(callback, [0, 1])) changed = true;
|
||||
let result = this.irTypeOf(node);
|
||||
if (arithmetic !== null)
|
||||
result = joinPrimitiveTypes([result, arithmetic]) ?? result;
|
||||
if (optional) result = this.runtimeOptionalType(result);
|
||||
siteDependencies?.read(node);
|
||||
const previous = this.runtimeOptionalReduceTypes.get(node);
|
||||
if (!previous || !typeEquals(previous, result)) {
|
||||
@@ -7446,7 +7465,10 @@ export class Lowerer {
|
||||
if (sig && !this.wantBody(sig.name)) continue;
|
||||
const fn = this.lowerFunction(decl);
|
||||
if (fn) functions.push(fn);
|
||||
else if (this.countsSkips()) this.countSkippedBody(decl);
|
||||
else if (this.countsSkips()) {
|
||||
this.countSkippedBody(decl);
|
||||
if (this.remainder) this.reportBlockedBody(decl);
|
||||
}
|
||||
}
|
||||
for (const decl of fp.classDecls) {
|
||||
const info = this.classes.get(this.classNamer(decl));
|
||||
@@ -7777,6 +7799,19 @@ export class Lowerer {
|
||||
}
|
||||
}
|
||||
|
||||
/** Coverage remainder: the body diagnostics of a function whose
|
||||
* signature blocked (blockedBodyDiagnostics). A function reachable emit
|
||||
* reached (its signature's diagnostics flushed there) reports them with
|
||||
* the reached diagnostics; any other reports them as unreached. */
|
||||
readonly reachedBlockedBodyDiagnostics: ScrDiagnostic[] = [];
|
||||
private reportBlockedBody(decl: ts.FunctionDeclaration): void {
|
||||
const diags = blockedBodyDiagnostics(this, decl);
|
||||
const symbol = declSymbolOf(this, decl);
|
||||
if (symbol && this.alreadyFlushed.has(symbol))
|
||||
this.reachedBlockedBodyDiagnostics.push(...diags);
|
||||
else for (const d of diags) this.pushDiag(d);
|
||||
}
|
||||
|
||||
/** Whether run() counts a signature-blocked declaration in
|
||||
* stats.functionsSkipped: whole-program passes and the coverage
|
||||
* remainder do; an externally-gated emit pass leaves the counting to the
|
||||
@@ -9300,6 +9335,21 @@ export class Lowerer {
|
||||
* type-mapper.ts's chalk-shape mapping — call paths and func-slot coercions
|
||||
* share it. */
|
||||
hybridCallUnwrap(expr: IrExpr): IrExpr {
|
||||
// A callable CLASS instance: a class whose merged interface carries a
|
||||
// call signature stores its call behavior in a `%call` field (the
|
||||
// chalk package shim's builder). Calls read the field.
|
||||
if (expr.type.kind === "object") {
|
||||
const callType = this.classes.get(expr.type.className)?.fields.get("%call");
|
||||
if (callType === undefined || callType.kind !== "func") return expr;
|
||||
return {
|
||||
kind: "fieldGet",
|
||||
obj: expr,
|
||||
className: expr.type.className,
|
||||
field: "%call",
|
||||
type: callType,
|
||||
loc: expr.loc,
|
||||
};
|
||||
}
|
||||
if (expr.type.kind !== "record") return expr;
|
||||
const shape = this.shapes.get(expr.type.shapeId);
|
||||
const call = shape?.fields.find((f) => f.name === "%call");
|
||||
@@ -9630,9 +9680,27 @@ export class Lowerer {
|
||||
// A HYBRID (function-with-properties) record flowing into a plain
|
||||
// func slot extracts its reserved %call field — the chalk shape's
|
||||
// "the value IS callable" half (type-mapper.ts's hybrid mapping).
|
||||
if (expected.kind === "func" && expr.type.kind === "record") {
|
||||
if (expected.kind === "func" && (expr.type.kind === "record" || expr.type.kind === "object")) {
|
||||
const un = this.hybridCallUnwrap(expr);
|
||||
if (un !== expr && typeEquals(un.type, expected)) return un;
|
||||
// A callable class instance (the chalk shim's variadic builder) in a
|
||||
// narrower function slot: its call closure crosses the checked
|
||||
// function boundary, which adapts the arity.
|
||||
if (
|
||||
un !== expr &&
|
||||
expr.type.kind === "object" &&
|
||||
un.type.kind === "func" &&
|
||||
canBoxFuncIntoDyn(
|
||||
un.type,
|
||||
(id) => this.shapes.get(id),
|
||||
(id) => this.unions.get(id),
|
||||
)
|
||||
) {
|
||||
return this.coerceToExpected(
|
||||
{ kind: "dynFrom", value: un, type: DYN, loc: un.loc },
|
||||
expected,
|
||||
);
|
||||
}
|
||||
}
|
||||
// A zero-param function whose RETURN is a wider record array than the
|
||||
// slot's (`getRoutes: () => cachedRoutes` against `() => Narrow[]`):
|
||||
|
||||
@@ -1590,6 +1590,12 @@ export const BUILTIN_MODULE_FNS: Record<
|
||||
createDiffieHellman: { fn: "crypto.dhNew", params: [DYN, DYN, DYN, DYN], result: DYN },
|
||||
getDiffieHellman: { fn: "crypto.dhGroupNew", params: [DYN], result: DYN },
|
||||
createDiffieHellmanGroup: { fn: "crypto.dhGroupNew", params: [DYN], result: DYN },
|
||||
// Prime checks and generation (lowerPrimeCall): checked-dynamic
|
||||
// arguments, results converted to the call site's type.
|
||||
checkPrimeSync: { fn: "crypto.checkPrimeSync", params: [], result: DYN },
|
||||
checkPrime: { fn: "crypto.checkPrimeCb", params: [], result: VOID },
|
||||
generatePrimeSync: { fn: "crypto.generatePrimeSync", params: [], result: DYN },
|
||||
generatePrime: { fn: "crypto.generatePrimeCb", params: [], result: VOID },
|
||||
},
|
||||
zlib: {
|
||||
// The default-options convenience family is special-cased in
|
||||
@@ -3099,7 +3105,8 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record<
|
||||
["getCipherInfo"].map((m) => [
|
||||
m,
|
||||
"cipher metadata has no lowering — createCipheriv and createDecipheriv support " +
|
||||
"aes-128/192/256 in cbc, ctr, and gcm modes and chacha20-poly1305",
|
||||
"aes-128/192/256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1, and ofb modes, des-ede3 and " +
|
||||
"des-ede, chacha20, and chacha20-poly1305",
|
||||
]),
|
||||
),
|
||||
setFips:
|
||||
|
||||
@@ -62,9 +62,20 @@ interface ImplementationShim {
|
||||
* of it (a __commonJS chunk whose key names the package, its version
|
||||
* and this file) is the package itself. */
|
||||
mainFile: string;
|
||||
/** The package's declaration entry relative to its directory, when it
|
||||
* is not `index.d.ts`. */
|
||||
typesFile?: string;
|
||||
/** An alternative entry for this installation (relative to the shim
|
||||
* root), or undefined for the version's own entry. */
|
||||
variant?: (pkgDir: string) => string | undefined;
|
||||
/** Every variant entry, for the guard test that compiles each one. */
|
||||
variants?: readonly string[];
|
||||
}
|
||||
|
||||
const IMPLEMENTATION_SHIMS: ReadonlyMap<string, ImplementationShim> = new Map([
|
||||
const IMPLEMENTATION_SHIMS: ReadonlyMap<string, ImplementationShim> = new Map<
|
||||
string,
|
||||
ImplementationShim
|
||||
>([
|
||||
[
|
||||
"fs-extra",
|
||||
{
|
||||
@@ -79,6 +90,33 @@ const IMPLEMENTATION_SHIMS: ReadonlyMap<string, ImplementationShim> = new Map([
|
||||
]),
|
||||
},
|
||||
],
|
||||
[
|
||||
"chalk",
|
||||
{
|
||||
name: "chalk",
|
||||
defaultExport: true,
|
||||
mainFile: "source/index.js",
|
||||
typesFile: "source/index.d.ts",
|
||||
versions: new Map([[5, "chalk/v5.ts"]]),
|
||||
},
|
||||
],
|
||||
[
|
||||
"hot-shots",
|
||||
{
|
||||
name: "hot-shots",
|
||||
defaultExport: true,
|
||||
mainFile: "index.js",
|
||||
typesFile: "types.d.ts",
|
||||
versions: new Map([[8, "hot-shots/v8.ts"]]),
|
||||
// hot-shots' uds protocol loads the optional unix-dgram addon: where
|
||||
// it is installed, the entry with the native Unix datagram transport.
|
||||
variant: (pkgDir: string) =>
|
||||
installedPackageDir(join(pkgDir, "index.js"), "unix-dgram") === null
|
||||
? undefined
|
||||
: "hot-shots/v8-unix-dgram.ts",
|
||||
variants: ["hot-shots/v8-unix-dgram.ts"],
|
||||
},
|
||||
],
|
||||
[
|
||||
"undici",
|
||||
{
|
||||
@@ -141,8 +179,10 @@ export function throughBundledShimForwarder(resolved: string): string {
|
||||
* the guard test that compiles each one. */
|
||||
export function packageShimEntries(): string[] {
|
||||
const entries = new Set<string>();
|
||||
for (const shim of IMPLEMENTATION_SHIMS.values())
|
||||
for (const shim of IMPLEMENTATION_SHIMS.values()) {
|
||||
for (const entry of shim.versions.values()) entries.add(join(packageShimRoot(), entry));
|
||||
for (const entry of shim.variants ?? []) entries.add(join(packageShimRoot(), entry));
|
||||
}
|
||||
return [...entries].sort();
|
||||
}
|
||||
|
||||
@@ -240,7 +280,8 @@ function shimEntryForPackageDir(pkgDir: string, expectedName: string | null): st
|
||||
if (shim === undefined) return null;
|
||||
const major = Number(/^(\d+)\./.exec(pkg.version)?.[1] ?? NaN);
|
||||
const entry = shim.versions.get(major);
|
||||
return entry === undefined ? null : join(packageShimRoot(), entry);
|
||||
if (entry === undefined) return null;
|
||||
return join(packageShimRoot(), shim.variant?.(pkgDir) ?? entry);
|
||||
}
|
||||
|
||||
/** The shim module `specifier` resolves to from `fromFile`, or null. Only
|
||||
@@ -280,16 +321,33 @@ export function isShimmedImport(fromFile: string, specifier: string): boolean {
|
||||
return isUnloadedPackage(specifier) || packageShimFor(fromFile, specifier) !== null;
|
||||
}
|
||||
|
||||
/** The shimmed package whose declaration entry `path` is, with the
|
||||
* package directory: `<pkg>/index.d.ts`, or the entry's own `typesFile`. */
|
||||
function shimOfDeclarationPath(path: string): { pkgDir: string; shim: ImplementationShim } | null {
|
||||
if (!path.endsWith(".d.ts")) return null;
|
||||
if (path.endsWith("/index.d.ts")) {
|
||||
const pkgDir = dirname(path);
|
||||
const shim = IMPLEMENTATION_SHIMS.get(pkgDir.slice(pkgDir.lastIndexOf("/") + 1));
|
||||
if (shim !== undefined && shim.typesFile === undefined) return { pkgDir, shim };
|
||||
}
|
||||
for (const shim of IMPLEMENTATION_SHIMS.values()) {
|
||||
if (shim.typesFile === undefined) continue;
|
||||
const suffix = `/${shim.name}/${shim.typesFile}`;
|
||||
if (path.endsWith(suffix))
|
||||
return { pkgDir: path.slice(0, path.length - suffix.length + shim.name.length + 1), shim };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The fs-shadow answer for a shimmed package's declaration entry: the
|
||||
* installed `index.d.ts` reads as a re-export of the version's shim module,
|
||||
* so the checker types each importer against its own version. Undefined
|
||||
* for every other path. */
|
||||
export function packageShimDeclarationText(path: string): string | undefined {
|
||||
if (!path.endsWith("/index.d.ts")) return undefined;
|
||||
const pkgDir = dirname(path);
|
||||
const name = pkgDir.slice(pkgDir.lastIndexOf("/") + 1);
|
||||
const shim = IMPLEMENTATION_SHIMS.get(name);
|
||||
if (shim === undefined) return undefined;
|
||||
const located = shimOfDeclarationPath(path);
|
||||
if (located === null) return undefined;
|
||||
const { pkgDir, shim } = located;
|
||||
const name = shim.name;
|
||||
// A DefinitelyTyped twin (`@types/fs-extra`) types a package that ships no
|
||||
// declarations. Nothing at its path names the installed runtime version,
|
||||
// so it reads as the shim only when every supported version shares one
|
||||
@@ -316,6 +374,12 @@ export function isPackageShimFile(fileName: string): boolean {
|
||||
return fileName.startsWith(root.endsWith("/") ? root : `${root}/`);
|
||||
}
|
||||
|
||||
/** True for a name a shipped shim may declare as a bodiless runtime
|
||||
* intrinsic (lower-calls.ts lowers its calls to runtime calls). */
|
||||
export function isShimIntrinsicName(name: string): boolean {
|
||||
return name.startsWith("scriptcUnixDgram");
|
||||
}
|
||||
|
||||
/** Resets per-compile caches. */
|
||||
export function clearPackageShimCaches(): void {
|
||||
installedCache.clear();
|
||||
|
||||
@@ -169,6 +169,30 @@ function ids(
|
||||
return nodes?.map((n) => (n as OracleNode).index);
|
||||
}
|
||||
|
||||
test("generated child getters select the child their name selects", () => {
|
||||
// These getters adapt their raw child; their slot functions are compared instead.
|
||||
const adapted = ["body", "initializer", "left", "modifiers", "operand", "questionToken"];
|
||||
const names = [...new Set(Object.values(childProperties).flat())];
|
||||
const lists = (value: unknown): unknown => (value instanceof AstNodeList ? value.nodes : value);
|
||||
let compared = 0;
|
||||
for (const { file } of decoded.values()) {
|
||||
walkPreorder(file.root, (node) => {
|
||||
const view = node as unknown as Record<string, unknown>;
|
||||
for (const name of names) {
|
||||
if (adapted.includes(name)) {
|
||||
const slot = (view[`${name}Slot`] as () => number).call(node);
|
||||
expect(slot, `${node.kind}.${name}`).toBe(astChildOrder(node.kind, name));
|
||||
} else {
|
||||
expect(lists(view[name]), `${node.kind}.${name}`).toBe(lists(node.child(name)));
|
||||
}
|
||||
compared++;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
expect(compared).toBeGreaterThan(100_000);
|
||||
});
|
||||
|
||||
test("the writeTargets option answers write targets without their wrappers", () => {
|
||||
const { bytes, file: plain } = decoded.get("targets.ts")!;
|
||||
const file = new AstFile(bytes, undefined, { writeTargets: true });
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -240,12 +240,12 @@ export class AstWireFile {
|
||||
string(index: number): string {
|
||||
if (index >>> 0 !== index || index % 2 !== 0 || index >= this.stringCount * 2)
|
||||
throw new AstDecodeError(`invalid string index ${index}`);
|
||||
const cached = this.stringCache[index / 2];
|
||||
const cached = this.stringCache[index >>> 1];
|
||||
if (cached !== undefined) return cached;
|
||||
const start = astU32(this.bytes, this.stringOffsets + index * 4);
|
||||
const end = astU32(this.bytes, this.stringOffsets + (index + 1) * 4);
|
||||
const text = decodeAstString(this.bytes, this.strings + start, end - start);
|
||||
this.stringCache[index / 2] = text;
|
||||
this.stringCache[index >>> 1] = text;
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
@@ -5844,6 +5844,41 @@ function constAssertedEmptyArrayProp(p: ts.Symbol): boolean {
|
||||
return underConstAssertion(d);
|
||||
}
|
||||
|
||||
/** A TypeScript property assignment `name: []` whose type is `never[]`. */
|
||||
function emptyNeverArrayProp(p: ts.Symbol, fieldTs: ts.Type, checker: ts.TypeChecker): boolean {
|
||||
const d = p.valueDeclaration;
|
||||
if (d === undefined || !ts.isPropertyAssignment(d)) return false;
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(d))) return false;
|
||||
if (!ts.isArrayLiteralExpression(d.initializer) || d.initializer.elements.nodes.length > 0)
|
||||
return false;
|
||||
return (
|
||||
checker.isArrayType(fieldTs) &&
|
||||
(checker.getTypeArguments(fieldTs as ts.TypeReference)[0]!.flags & ts.TypeFlags.Never) !== 0
|
||||
);
|
||||
}
|
||||
|
||||
/** A JavaScript object literal's property (`{ lastComponent: undefined }`,
|
||||
* `{ user: null }`). Nothing checks a JavaScript file's later writes, which
|
||||
* can store any value in it (`path.lastComponent = { ... }`), so a
|
||||
* property whose only initializer is a unit holds the checked-dynamic value
|
||||
* rather than the unit-only union. */
|
||||
function isUnitOnlyIr(t: IrType, ctx: TypeMapperCtx): boolean {
|
||||
if (t.kind !== "union") return false;
|
||||
const def = ctx.unions.get(t.unionId);
|
||||
return def !== undefined && def.arms.every((arm) => isUnitType(arm));
|
||||
}
|
||||
|
||||
function jsObjectLiteralProperty(p: ts.Symbol): boolean {
|
||||
const d = p.valueDeclaration;
|
||||
// `__proto__: null` sets the literal's prototype; it declares no property.
|
||||
return (
|
||||
d !== undefined &&
|
||||
(ts.isPropertyAssignment(d) || ts.isShorthandPropertyAssignment(d)) &&
|
||||
ts.symbolName(p) !== "__proto__" &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(d))
|
||||
);
|
||||
}
|
||||
|
||||
function mapGenericAwaitedAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
const { resolveTypeParam } = ctx;
|
||||
if (!resolveTypeParam) return null;
|
||||
@@ -7880,6 +7915,14 @@ function mapRecordTypeInner(
|
||||
) {
|
||||
pt = arrayOf(unitOnlyUnion(ctx.unions));
|
||||
}
|
||||
// A TypeScript property initialized with an empty literal and typed
|
||||
// `never[]` (`const out = { files: [] }`) holds no element, so it maps
|
||||
// like the empty tuple: the unit-only element array, which a
|
||||
// destination's typed array field (`files: string[]`) receives through
|
||||
// the checked empty-array lift. never's f64 element would make it a
|
||||
// `number[]` field.
|
||||
if (pt?.kind === "array" && emptyNeverArrayProp(p, fieldTs, checker))
|
||||
pt = arrayOf(unitOnlyUnion(ctx.unions));
|
||||
if (pt === null && (fieldTs.flags & ts.TypeFlags.Any) !== 0) pt = DYN;
|
||||
// An `any[]` field (`list: any[]` in an SDK's message type, also
|
||||
// optional or nullable) is the checked-dynamic array, exactly as
|
||||
@@ -7903,6 +7946,7 @@ function mapRecordTypeInner(
|
||||
// omits the undefined arm exactly like an omitted optional.
|
||||
if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam))
|
||||
pt = unitOnlyUnion(ctx.unions);
|
||||
if (pt !== null && isUnitOnlyIr(pt, ctx) && jsObjectLiteralProperty(p)) pt = DYN;
|
||||
// dyn FIELDS map now (`{ v: unknown }`, `[string, unknown]` tuples):
|
||||
// the slot carries a dyn value exactly like an `unknown`-valued
|
||||
// overflow entry — same RC adapters, same dynFrom conversion on the
|
||||
|
||||
@@ -16,6 +16,12 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
// Node's prime, generator, size, encoding and group-name errors.
|
||||
"crypto.dhNew",
|
||||
"crypto.dhGroupNew",
|
||||
// Node's candidate, size, options, add and rem errors, and OpenSSL's
|
||||
// generation errors.
|
||||
"crypto.checkPrimeSync",
|
||||
"crypto.checkPrimeCb",
|
||||
"crypto.generatePrimeSync",
|
||||
"crypto.generatePrimeCb",
|
||||
// node:test's { timeout } and { concurrency } range checks.
|
||||
"test.register",
|
||||
"test.registerEmpty",
|
||||
@@ -1001,3 +1007,25 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"crypto.createSign",
|
||||
"crypto.createVerify",
|
||||
]);
|
||||
|
||||
/** libCall members that always throw. Their result type is the replaced
|
||||
* expression's own and is never materialized: global.undefRead's is the
|
||||
* declared type of the undefined global; the others stand in for a value
|
||||
* of the expression they replace. The fs Chk ladders qualify: every
|
||||
* validation failure throws Node's typed error, and a full pass throws the
|
||||
* trailing compiler-rendered fence. */
|
||||
export const ALWAYS_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"global.undefRead",
|
||||
"error.nodeThrow",
|
||||
"error.argTypeThrow",
|
||||
"error.propTypeThrow",
|
||||
"fs.mkdtempChk",
|
||||
"fs.readFileChk",
|
||||
"fs.opendirChk",
|
||||
"fs.watchFileChk",
|
||||
"fs.lchmodChk",
|
||||
"fs.readChk",
|
||||
"fs.streamOptsChk",
|
||||
"net.connectOptsChk",
|
||||
"tls.caCertsChk",
|
||||
]);
|
||||
|
||||
@@ -1318,6 +1318,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
argTypes: [STRING, arrayOf(STRING), F64, STRING, F64, F64, F64],
|
||||
result: SPAWNRES_T,
|
||||
},
|
||||
/** cp.spawnSyncOpts with the `env` option: the trailing [k,v,...]
|
||||
* pairs replace the child's environment (recordToEnvPairs). */
|
||||
"cp.spawnSyncEnv": {
|
||||
argTypes: [STRING, arrayOf(STRING), F64, STRING, F64, F64, F64, arrayOf(STRING)],
|
||||
result: SPAWNRES_T,
|
||||
},
|
||||
/** cp.spawnSyncOpts with the stdio carried as a RUNTIME string —
|
||||
* "pipe" | "ignore" | "inherit", proven by the call site's TYPE (the
|
||||
* defaultRunner idiom `stdio: options?.stdio ?? "pipe"`); the runtime
|
||||
@@ -1528,6 +1534,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
},
|
||||
"dgram.sendStr": { argTypes: [DGRAMSOCK_T, STRING, F64, STRING], result: VOID },
|
||||
"dgram.sendBytes": { argTypes: [DGRAMSOCK_T, BYTES_U8, F64, STRING], result: VOID },
|
||||
/** Connected AF_UNIX datagram sockets for the hot-shots package shim
|
||||
* (the unix-dgram addon's surface): fd or result >= 0, -errno on
|
||||
* failure; send answers 1 when the socket would block. */
|
||||
"dgram.unixSocket": { argTypes: [], result: F64 },
|
||||
"dgram.unixConnect": { argTypes: [F64, STRING], result: F64 },
|
||||
"dgram.unixSend": { argTypes: [F64, BYTES_U8], result: F64 },
|
||||
"dgram.unixClose": { argTypes: [F64], result: F64 },
|
||||
/** The send argument-validation ladder over dyn arguments (Node's
|
||||
* signature shuffle: slice bounds, list/type contracts, port/address
|
||||
* validation, connected-state errors), then the send: lists, connected
|
||||
@@ -2820,6 +2833,16 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
/** getDiffieHellman(groupName) and createDiffieHellmanGroup(name): a
|
||||
* native DiffieHellmanGroup handle. */
|
||||
"crypto.dhGroupNew": { argTypes: [DYN], result: DYN },
|
||||
/** checkPrimeSync(candidate, options) and generatePrimeSync(size,
|
||||
* options) over checked-dynamic arguments (scr_dh.c): a boolean, and an
|
||||
* ArrayBuffer or bigint, as checked-dynamic results. The Cb forms take
|
||||
* the callback and whether its error parameter takes null for Node's
|
||||
* undefined, and call it in a later macrotask; Node's argument errors
|
||||
* throw (may-throw). */
|
||||
"crypto.checkPrimeSync": { argTypes: [DYN, DYN], result: DYN },
|
||||
"crypto.checkPrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID },
|
||||
"crypto.generatePrimeSync": { argTypes: [DYN, DYN], result: DYN },
|
||||
"crypto.generatePrimeCb": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID },
|
||||
"crypto.hmacNewStr": { argTypes: [STRING, STRING], result: CRYPTOHMAC_T },
|
||||
"crypto.hmacNewBytes": { argTypes: [STRING, BYTES_U8], result: CRYPTOHMAC_T },
|
||||
"crypto.hashUpdateStr": { argTypes: [CRYPTOHASH_T, STRING], result: CRYPTOHASH_T },
|
||||
@@ -3593,7 +3616,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"weakSet.new": { argTypes: [DYN], result: DYN },
|
||||
"dyn.fromEntries": { argTypes: [DYN], result: DYN },
|
||||
"bytes.constructor": { argTypes: [STRING], result: DYN },
|
||||
"bytes.instanceOf": { argTypes: [DYN, DYN], result: BOOL },
|
||||
"bytes.instanceOf": { argTypes: [DYN, DYN, STRING], result: BOOL },
|
||||
"bytes.construct": { argTypes: [DYN, DYN, STRING], result: DYN },
|
||||
/** The ambient receiver — JS `this` in a plain (non-method) function
|
||||
* body: the innermost binding the current firing/dispatch window
|
||||
|
||||
@@ -508,7 +508,10 @@ export function dynamicFieldStores(
|
||||
};
|
||||
const dynWritten = new Set<string>();
|
||||
let dynWritesAll = false;
|
||||
for (const fn of mod.functions)
|
||||
// A store whose key is not a literal names every field: later stores add
|
||||
// nothing, so the walk stops there.
|
||||
for (const fn of mod.functions) {
|
||||
if (dynWritesAll) break;
|
||||
walkFunction(fn, {
|
||||
expr: (e) => {
|
||||
if (dynWritesAll) return;
|
||||
@@ -524,6 +527,7 @@ export function dynamicFieldStores(
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
return { all: dynWritesAll, names: dynWritten };
|
||||
}
|
||||
|
||||
@@ -536,6 +540,22 @@ export function dynamicallyStoredFamilies(
|
||||
slots: Int32Slots,
|
||||
stores?: DynamicFieldStores,
|
||||
): Set<string> {
|
||||
const records = new Map((mod.records ?? []).map((r) => [r.id, r]));
|
||||
const unions = new Map((mod.unions ?? []).map((u) => [u.id, u]));
|
||||
// A live dyn view (typed-ref capsule) of an instance writes back into
|
||||
// its fields only after a runtime store into the capsule; the commit then
|
||||
// writes every field from the view, which holds the exact snapshot value
|
||||
// of any property no store named. So reaching dynamic code disqualifies
|
||||
// exactly the field names some dynamic store in the program can name.
|
||||
const { all: dynWritesAll, names: dynWritten } =
|
||||
stores ?? dynamicFieldStores(mod, records, unions);
|
||||
const storable = (field: { name: string; type: IrType }): boolean =>
|
||||
field.type.kind === "f64" &&
|
||||
!field.name.startsWith("%") &&
|
||||
(dynWritesAll || dynWritten.has(field.name));
|
||||
const families = new Set<string>();
|
||||
// Without a field some store can name, no class is worth exposing.
|
||||
if (!(mod.classes ?? []).some((cls) => cls.fields.some(storable))) return families;
|
||||
const classes = new Map((mod.classes ?? []).map((c) => [c.name, c]));
|
||||
const children = new Map<string, string[]>();
|
||||
for (const cls of classes.values())
|
||||
@@ -554,8 +574,6 @@ export function dynamicallyStoredFamilies(
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const records = new Map((mod.records ?? []).map((r) => [r.id, r]));
|
||||
const unions = new Map((mod.unions ?? []).map((u) => [u.id, u]));
|
||||
const exposedClasses = new Set<string>();
|
||||
const seenNamed = new Set<string>();
|
||||
let exposeAll = false;
|
||||
@@ -596,13 +614,6 @@ export function dynamicallyStoredFamilies(
|
||||
});
|
||||
}
|
||||
};
|
||||
// A live dyn view (typed-ref capsule) of an instance writes back into
|
||||
// its fields only after a runtime store into the capsule; the commit then
|
||||
// writes every field from the view, which holds the exact snapshot value
|
||||
// of any property no store named. So reaching dynamic code disqualifies
|
||||
// exactly the field names some dynamic store in the program can name.
|
||||
const { all: dynWritesAll, names: dynWritten } =
|
||||
stores ?? dynamicFieldStores(mod, records, unions);
|
||||
for (const fn of mod.functions)
|
||||
walkFunction(fn, {
|
||||
expr: (e) => {
|
||||
@@ -628,16 +639,10 @@ export function dynamicallyStoredFamilies(
|
||||
if (s.kind === "throw") expose(s.value.type);
|
||||
},
|
||||
});
|
||||
const families = new Set<string>();
|
||||
for (const cls of classes.values()) {
|
||||
if (!exposeAll && !exposedClasses.has(cls.name)) continue;
|
||||
for (const field of cls.fields)
|
||||
if (
|
||||
field.type.kind === "f64" &&
|
||||
!field.name.startsWith("%") &&
|
||||
(dynWritesAll || dynWritten.has(field.name))
|
||||
)
|
||||
families.add(slots.family(cls.name, field.name)!);
|
||||
if (storable(field)) families.add(slots.family(cls.name, field.name)!);
|
||||
}
|
||||
return families;
|
||||
}
|
||||
@@ -1058,12 +1063,18 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
|
||||
if (params) for (const id of params.keys()) reads(`param:${fn.name}\0${id}`, fn.name);
|
||||
// Range facts are consulted only at writes into candidate slots; slots
|
||||
// are only ever dropped, so a body without such a write needs none.
|
||||
const ranges: IntegerRanges = hasCandidateWrite(fn)
|
||||
const candidate = hasCandidateWrite(fn);
|
||||
const ranges: IntegerRanges = candidate
|
||||
? analyzeIntegerRanges(numeric(fn), slots.facts(fn.name))
|
||||
: new Map();
|
||||
if (ranges.size > 0) lastWrites.set(fn.name, slotWrites(fn, ranges, functions));
|
||||
else lastWrites.delete(fn.name);
|
||||
const proven: IrExpr[] = [];
|
||||
// The walk below acts only at writes into candidate slots.
|
||||
if (!candidate) {
|
||||
provenByFunction.set(fn.name, proven);
|
||||
continue;
|
||||
}
|
||||
const int32 = (e: IrExpr): boolean => withinInt32(ranges.get(e));
|
||||
// An int32 written into a slot still assumed non-negative: widen the
|
||||
// assumption when the value may be negative, and reanalyze its readers.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user