mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-30 07:14:39 +08:00
Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts: # packages/compiler/src/diagnostics/diagnostic.ts # packages/compiler/src/frontend/pipeline.ts
This commit is contained in:
@@ -773,17 +773,22 @@ jobs:
|
||||
run: >-
|
||||
pnpm exec vitest run tests/harness/worktree-files.test.ts
|
||||
--testNamePattern "workspace reset keeps Linux Sandbox paths POSIX"
|
||||
# The Node host checks with tsc-ts, the default, over the named pipe it
|
||||
# serves on Windows, and with tsgo.
|
||||
- name: Windows scriptc run smoke
|
||||
shell: pwsh
|
||||
run: |
|
||||
zig version
|
||||
$output = & node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend llvm
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
$actual = ($output -join "`n").Trim()
|
||||
if ($actual -ne "hello world") {
|
||||
throw "unexpected scriptc output: $actual"
|
||||
foreach ($checker in @("tsc-ts", "tsgo")) {
|
||||
$env:SCRIPTC_CHECKER = $checker
|
||||
$output = & node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend llvm
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
$actual = ($output -join "`n").Trim()
|
||||
if ($actual -ne "hello world") {
|
||||
throw "unexpected scriptc output with ${checker}: $actual"
|
||||
}
|
||||
}
|
||||
- name: Windows PE subsystem selection
|
||||
env:
|
||||
|
||||
@@ -34,6 +34,7 @@ output/
|
||||
/packages/runtime-*/runtime-pack.json
|
||||
/packages/runtime-*/.runtime-pack-*
|
||||
!tests/fixtures/npm/node_modules/*/dist/
|
||||
!tests/fixtures/npm/node_modules/@*/*/dist/
|
||||
!tests/fixtures/npm/workspace/*/dist/
|
||||
!tests/fixtures/npm/cases/workspace-copied/members/*/dist/
|
||||
!tests/fixtures/npm/cases/workspace-copied/node_modules/*/dist/
|
||||
|
||||
@@ -11,6 +11,8 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any |
|
||||
| `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` |
|
||||
| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner |
|
||||
| `dictionary-walks` | cpu | `Record<string, number>` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted |
|
||||
| `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods |
|
||||
| `functional-pipeline` | cpu | `filter`/`map`/`reduce`/`flatMap` chains, closures, small records |
|
||||
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
|
||||
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/dictionary-walks.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// String-keyed dictionaries (`Record<string, number>`) walked with for-in,
|
||||
// Object.keys, Object.values and Object.entries while entries are added,
|
||||
// updated and deleted: per-category totals, a pruning pass and a merge.
|
||||
let seed = 777;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
const categories = 4000;
|
||||
const counts: Record<string, number> = {};
|
||||
for (let i = 0; i < Math.floor(200000 * scale); i++) {
|
||||
const key = "c" + Math.floor(random() * categories);
|
||||
counts[key] = (counts[key] ?? 0) + 1;
|
||||
}
|
||||
|
||||
let walked = 0;
|
||||
let sum = 0;
|
||||
for (let pass = 0; pass < 40; pass++) {
|
||||
for (const key in counts) {
|
||||
walked++;
|
||||
sum += counts[key]!;
|
||||
}
|
||||
}
|
||||
|
||||
// Prune: delete the small categories during the walk, including keys the
|
||||
// walk has not reached yet.
|
||||
let pruned = 0;
|
||||
for (const key in counts) {
|
||||
const n = counts[key]!;
|
||||
if (n < 45) {
|
||||
delete counts[key];
|
||||
pruned++;
|
||||
}
|
||||
const ahead = "c" + ((Number(key.slice(1)) + 1) % categories);
|
||||
if (n > 60 && ahead in counts && counts[ahead]! < 45) {
|
||||
delete counts[ahead];
|
||||
pruned++;
|
||||
}
|
||||
}
|
||||
|
||||
// Merge into a second dictionary and read it back every way.
|
||||
const merged: Record<string, number> = {};
|
||||
for (const key of Object.keys(counts)) merged[key.toUpperCase()] = counts[key]! * 2;
|
||||
let values = 0;
|
||||
for (const v of Object.values(merged)) values += v;
|
||||
let entries = 0;
|
||||
for (const [key, v] of Object.entries(merged)) entries += key.length + v;
|
||||
|
||||
console.log(
|
||||
`walked ${walked}, sum ${sum}, pruned ${pruned}, kept ${Object.keys(counts).length}, values ${values}, entries ${entries}`,
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/dynamic-methods.mjs",
|
||||
"args": [
|
||||
"300000"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Untyped JavaScript calling builtin methods on values it never declares
|
||||
// types for: string slice/indexOf/toUpperCase/startsWith/padStart, array
|
||||
// push/pop/at/join/includes/indexOf, Map and Set get/set/has/add, number
|
||||
// toFixed, and calls of an object's own methods. Argument: the number of
|
||||
// iterations.
|
||||
const rounds = Number(process.argv[2] ?? "300000");
|
||||
const words = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta"];
|
||||
const counts = new Map();
|
||||
const seen = new Set();
|
||||
const stack = [];
|
||||
const recent = [];
|
||||
const tally = {
|
||||
total: 0,
|
||||
add(value) {
|
||||
this.total += value;
|
||||
return this.total;
|
||||
},
|
||||
};
|
||||
let checksum = 0;
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
const word = words[i % words.length];
|
||||
const head = word.slice(0, 3).toUpperCase();
|
||||
const key = head.padStart(4, "-");
|
||||
if (word.indexOf("e") >= 0 && !word.startsWith("z")) checksum += 1;
|
||||
counts.set(key, (counts.get(key) ?? 0) + 1);
|
||||
seen.add(i % 97);
|
||||
if (seen.has(i % 89)) checksum += 2;
|
||||
stack.push(i);
|
||||
if (stack.length > 32) checksum += stack.pop() % 3;
|
||||
recent.push(word);
|
||||
if (recent.length > 4) recent.shift();
|
||||
if (recent.includes("gamma")) checksum += recent.indexOf("gamma");
|
||||
checksum += tally.add(i % 5) % 11;
|
||||
if (i % 1000 === 0) checksum += Number((i / 7).toFixed(2)) | 0;
|
||||
}
|
||||
console.log(checksum, counts.size, seen.size, stack.at(-1), recent.join(","), tally.total);
|
||||
@@ -206,7 +206,7 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
|
||||
<dd>A runtime pack directory to use instead of an installed <code>@scriptc/runtime-<target></code> package.</dd>
|
||||
|
||||
<dt><code>SCRIPTC_CHECKER</code></dt>
|
||||
<dd>The TypeScript 7 API server that checks the program: <code>tsc-ts</code> (the default), which scriptc includes, or <code>tsgo</code>, from the <code>typescript</code> package scriptc also includes. Both give the same diagnostics and generated code. On Windows the default is <code>tsgo</code>, because tsc-ts's API server does not yet accept the named pipe scriptc connects over there.</dd>
|
||||
<dd>The TypeScript 7 API server that checks the program: <code>tsc-ts</code> (the default), which scriptc includes, or <code>tsgo</code>, from the <code>typescript</code> package scriptc also includes. Both give the same diagnostics and generated code.</dd>
|
||||
</dl>
|
||||
|
||||
```console
|
||||
|
||||
@@ -360,7 +360,7 @@ A class can be both `ProgramLifetime` and `Acyclic`. `Acyclic` is still checked
|
||||
|
||||
Instances can be published to other threads with `@scriptc/threads` like other class instances: `publish()` makes them immutable and shares them by reference, writes to a published instance throw the same `TypeError` as under Node.js, and instances that were not published stay writable and are cloned by `postMessage` and `workerData`.
|
||||
|
||||
Static code has no `WeakRef` or `FinalizationRegistry`, and a `WeakMap` or `WeakSet` entry whose key is never freed is never observable as removed, so nothing else can tell the difference.
|
||||
Static code has no `WeakRef`, a `FinalizationRegistry` never runs its cleanup callbacks, and a `WeakMap` or `WeakSet` entry whose key is never freed is never observable as removed, so nothing else can tell the difference.
|
||||
|
||||
### Lifetime regions
|
||||
|
||||
|
||||
@@ -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" />
|
||||
@@ -95,7 +97,7 @@ Coverage and `scriptc build` report the same diagnostic codes. Common ones inclu
|
||||
Some code compiles but can behave differently than under Node.js. Coverage reports the cases it can detect as `SC6xxx` warnings, and `scriptc build` prints the same warnings after a successful build. They never fail a build, but `--fail-on=divergences` can fail a CI job on them.
|
||||
|
||||
- `SC6001`: a record passed to, stored as, asserted as, or stored as an array element of a record type with different properties is copied, and the program then writes through the copy or compares it by identity.
|
||||
- `SC6002`: values of one record type are built in different key orders, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output.
|
||||
- `SC6002`: values of one record type are built in key orders scriptc cannot track per value, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output.
|
||||
- `SC6003`: `localeCompare` compares a string literal outside the built-in Latin-script collation.
|
||||
- `SC6004`: a call passes a class instance to a function that casts it to an unrelated class.
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -92,6 +92,13 @@ scriptc implements a few packages itself and compiles that implementation in pla
|
||||
- graceful-fs's retry queue, which fs-extra uses under Node.js, isn't part of the implementation, so a program that runs out of file descriptors fails at the first `EMFILE` error instead of retrying.
|
||||
- An ES module that imports fs-extra can use any of its exports by name. Node.js only exposes the names its CommonJS lexer finds in fs-extra's source, so a name like `readFile` throws a `SyntaxError` there.
|
||||
|
||||
`@hono/node-server` 2.x is built in. `serve`, `createAdaptorServer`, `getRequestListener` and `RequestError` serve a fetch handler such as Hono's `app.fetch` over `node:http` with native `Request` and `Response` objects, and write each response the way the package does under Node.js: the same status line, header names, header order and content length, for buffered and streamed bodies, including the package's shortcut for a response that is still exactly what its constructor received. Request bodies stream to the handler as it reads them, and a body the handler leaves unread is drained after the response, up to 64 MiB or 500 ms, as the package does. Known differences:
|
||||
|
||||
- Only HTTP/1.1 servers are declared: the `createServer` and `serverOptions` options for `node:https` and `node:http2`, the `websocket` option and `upgradeWebSocket` aren't declared, so using one is a type error at build time. Subpath modules such as `@hono/node-server/serve-static` compile from the package's own files.
|
||||
- The fetch handler must return a `Response` or a promise of one; the handler's second argument holds `incoming` and `outgoing`.
|
||||
- The global `Request` and `Response` stay the built-in classes, so `overrideGlobalObjects` has no effect, and a `Response` reads its default text content type as `text/plain;charset=UTF-8` where the package's replacement reads `text/plain; charset=UTF-8`. Written responses carry the package's spelling.
|
||||
- A streamed response is written without waiting for `drain`, and a failed handler answers 504 only for an error whose `name` is `TimeoutError`.
|
||||
|
||||
Small polyfill packages whose module is exactly a Node.js built-in on Node.js 24 compile as that built-in, at the `require()` that loads them, from your program's CommonJS files, from compiled packages, and from esbuild bundles whose module paths name the package and its version (as pnpm installs do). The package's own files aren't compiled.
|
||||
|
||||
<table>
|
||||
@@ -132,7 +139,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)
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/language-features/async-iteration.mdx</include>
|
||||
|
||||
<include>../limitations/language-features/iterable-values.mdx</include>
|
||||
|
||||
<include>../limitations/language-features/generics.mdx</include>
|
||||
|
||||
<include>../limitations/language-features/function-values.mdx</include>
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
`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, CCM, ECB, CFB, CFB8, CFB1, and OFB modes and as key wrap (the wrap and wrap-pad ciphers and their -inv forms), 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.
|
||||
`getCipherInfo` answers Node's info for every cipher OpenSSL names, including the ones without a native implementation.
|
||||
`getCipherInfo` options whose class defines `keyLength` or `ivLength` throw a coded SC2020 error.
|
||||
The result is Node's object; the SIV ciphers and the SM4 GCM, CCM, and XTS ciphers have no `nid`, and reading it where the declared `CipherInfo` type gives `number` throws a TypeError.
|
||||
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,7 +28,12 @@
|
||||
`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.
|
||||
`generateKeyPairSync` and `generateKeyPair` check their arguments in Node's order, with Node's errors, including the OpenSSL errors for RSA modulus lengths below 512 or above 2147483647 bits and for public exponents of 0, 1, or an even number, which `generateKeyPair` passes to its callback.
|
||||
RSA keys generate with even modulus lengths from 1024 to 8192 bits and public exponents up to 2147483647; other lengths and exponents that Node accepts, the `raw-public`, `raw-private`, and `raw-seed` encodings, and `paramEncoding: 'explicit'` throw a coded SC2020 error.
|
||||
The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1.
|
||||
Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate.
|
||||
They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`.
|
||||
|
||||
@@ -7,4 +7,6 @@
|
||||
They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`.
|
||||
They also support the iterator helper methods, such as `.map()` and `.take()`.
|
||||
Detached collection methods are unsupported.
|
||||
Constructing a Map, Set, WeakMap or WeakSet, or calling `Object.fromEntries`, with a checked-dynamic iterable (a native iterator, a generator, or an object with a `Symbol.iterator` method) takes all of its values before adding the first one.
|
||||
When an entry of a Map seed is not an object, the TypeError comes after the iterable is exhausted; Node closes the iterator at that entry.
|
||||
The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported.
|
||||
|
||||
@@ -3,4 +3,6 @@
|
||||
Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly.
|
||||
Named reads such as `value.valueOf` answer these inherited members; computed-key reads such as `value[key]` do not.
|
||||
`Object.prototype`'s own members read as values; its mutation and other reflection, such as `isPrototypeOf` and `__proto__`, are unsupported.
|
||||
The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called, and `instanceof` and `new` against them remain unsupported.
|
||||
The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called.
|
||||
`new` through a stored `Object`, `Date`, `Map`, `Set`, `WeakMap`, or `WeakSet` builds the same value as the `new` expression.
|
||||
`new` through a stored `RegExp` or `Promise`, `new` of a stored `Object` with a primitive argument, and `instanceof` against these constructors are unsupported.
|
||||
|
||||
@@ -10,19 +10,30 @@
|
||||
The `objectMode` option, and the `readableObjectMode` and `writableObjectMode` options of a `Duplex`, `Transform`, or `PassThrough`, take `true` or `false` literals in an options object literal; options passed as a value are read when the stream is built.
|
||||
In typed code, a `write` or `transform` callback, or a `_write` or `_transform` method, may declare its chunk with any type that converts to a checked-dynamic value: the chunk converts when the callback runs, and a value of another type throws a TypeError there.
|
||||
An object-mode write passes its encoding argument to `write` and `transform` as Node does (`undefined` when absent, `null` when a callback took its place); a parameter declared `string` receives `""` for `undefined` and `null`, and one declared `string | null | undefined` receives Node's value.
|
||||
`readable.read()` on an object-mode stream answers its Buffers; another value throws a not-supported error (read such values with `'data'` listeners, `for await`, or `pipe`).
|
||||
`readable.read()` on an object-mode stream answers any value it holds.
|
||||
`node:stream/consumers` and readline interfaces read object-mode streams of strings and Buffers; other values throw a not-supported error.
|
||||
`pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), and an async function destination whose value resolves the promise form and reaches the callback's second argument.
|
||||
`pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), an async generator function as the destination, and an async function destination whose value resolves the promise form and reaches the callback's second argument.
|
||||
A stage is written inline, or named by a function declaration or a `const` bound to a function expression that nothing reassigns.
|
||||
The source may also be an array, a string, a synchronous generator object, or a synchronous generator function without parameters; their elements reach the next stage on the same ticks as in Node.
|
||||
Stages may take the `{ signal }` argument.
|
||||
An async generator function as the destination, using the PassThrough that `pipeline()` returns for a function destination, and function stages or iterable sources together with pipeline options are unsupported.
|
||||
With a function destination, `pipeline()` returns an object-mode PassThrough that receives an async generator destination's values, or an async function destination's value unless it is `null` or `undefined`.
|
||||
`@types/node` types that result as `NodeJS.WritableStream`, so `for await` over it needs the `PassThrough` type (`pipeline(...) as PassThrough`, or a JSDoc `@type`).
|
||||
In typed code, the callback of a pipeline with an async function destination receives `null` instead of `undefined` when there is no error.
|
||||
`Readable.from` accepts `objectMode` and `highWaterMark` options.
|
||||
The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options; their callbacks take only the chunk, `map` returns strings or Buffers, and `flatMap` returns strings or arrays of them.
|
||||
`readable.compose` is unsupported.
|
||||
`stream.compose` accepts two or more streams, or one `Duplex`; functions, iterables, and web streams as stages are unsupported, and the `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream.
|
||||
`pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags.
|
||||
The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options.
|
||||
Their callbacks may take the `{ signal }` argument, `map` may return any value, and `flatMap` may return any iterable, async iterable, or stream.
|
||||
`Duplex.from` accepts an async generator function, which reads what is written to the Duplex, a `Duplex`, `Transform` or `PassThrough`, which it returns, and the values `Readable.from` accepts, which give a Duplex that is not writable.
|
||||
It also accepts a lone `Readable` or `Writable` and a `{ readable, writable }` pair, which the Duplex reads and writes through, a promise, whose value it reads, and an async function, which reads what is written and must resolve to `null` or `undefined`.
|
||||
A `Duplex.from` body function may take the `{ signal }` argument, which is aborted with the error the Duplex is destroyed with.
|
||||
`stream.compose` accepts streams and async generator functions in any position, an async function as the last stage, and an array, a string, or a generator as the first stage; one stage is `Duplex.from` of it.
|
||||
A web `ReadableStream` is read as an async iterable by `Readable.from` and as the first stage of `compose` or `pipeline`, and through its reader by `Readable.fromWeb` (with `objectMode`, `highWaterMark`, `encoding`, and `signal` in an options object literal) and `Duplex.from`.
|
||||
A web stream in a later compose stage, `Readable.toWeb`, and web `WritableStream` and `TransformStream` interop are unsupported.
|
||||
The `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream.
|
||||
`readable.compose(stage, { signal })` is `stream.compose(readable, stage)` with the signal added; the options are an object literal whose `signal` is a variable or a property read.
|
||||
`pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags.
|
||||
Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream.
|
||||
The `construct` option and `_construct` method, `decodeStrings`, and `defaultEncoding` work on every stream class, and the `writev` option and `_writev` method on Writable and Duplex; `writev` on a Transform or PassThrough, assigning `_construct` after construction, and a non-literal `decodeStrings` or `defaultEncoding` in an options object literal are unsupported.
|
||||
`setDefaultEncoding()` takes any encoding Node knows.
|
||||
A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error.
|
||||
`node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile.
|
||||
`on`, `once`, and `addListener` return the interface, so registrations chain.
|
||||
|
||||
@@ -28,7 +28,8 @@ The `captureRejections` option and `events.errorMonitor` listeners are not suppo
|
||||
The stream calls the object's `_read`, `_write`, `_final`, `_transform`, `_flush`, and `_destroy` with the object as `this`, and the stream prototypes' methods and accessors work on it.
|
||||
Its listeners live in the native stream rather than in `_events`, and `_readableState` and `_writableState` are read-only views that answer the commonly read fields.
|
||||
Typed code that receives such an object as a stream class uses its native stream, and piping a native stream into it works.
|
||||
The options the native streams refuse (`decodeStrings: false`, `signal`, and others) are refused here too, as are `_writev`, `_construct`, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, strings written to a byte-mode stream in encodings other than UTF-8, and pipes into a destination that overrides `write()` or `end()`.
|
||||
The options the native streams refuse (`signal` and others) are refused here too, as are `_writev` on a Transform, `_final` on a Transform, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, and pipes into a destination that overrides `write()` or `end()`.
|
||||
`_construct` and `_writev` are read when `Readable.call(this, options)` or `Writable.call(this, options)` runs, as Node's constructors read them.
|
||||
Other class constructors passed to `util.inherits` or `util.deprecate` are refused.
|
||||
Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported.
|
||||
A stored `util.deprecate` reports a `length` of 4, where Node reports 3.
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
- **Iterable values:** A value typed `Iterable<T>` can hold an array, a Set, a Map (for `Iterable<[K, V]>`), a string (for `Iterable<string>`), a generator, or a Map, Set or array iterator.
|
||||
Class instances with their own `[Symbol.iterator]` method and typed arrays can't be stored in an `Iterable<T>` and are reported at the conversion.
|
||||
@@ -10,9 +10,10 @@ A type whose values are only read field by field, from fields the class does not
|
||||
Object spread of a class instance is refused.
|
||||
Dense array literals created directly in checked storage preserve identity, and supported native handles pass by reference.
|
||||
|
||||
`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in one order per record type rather than per object. scriptc chooses that order from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with.
|
||||
The order matches Node.js when every value of the type is created in the same order.
|
||||
When values of one record type are created in different orders, including values of two types with the same properties and property types, or when a key is deleted and assigned again, the type keeps the declaration order of its first type instead, and `SC6002` reports object literals whose order differs where the program observes it.
|
||||
`Object.keys`, `Object.values`, `Object.entries`, `for...in`, `JSON.stringify`, and `util.inspect` enumerate a record's keys in the order the object created them, as in Node.js.
|
||||
scriptc chooses one order per record type from the order in which the program's object literals create the keys, follows object spread, rest destructuring, and record conversions back to the values they copy, and places an optional key that the program assigns after construction after the keys the object was built with.
|
||||
When the object literals of one record type create its keys in different orders, including literals of two types with the same properties and property types, each value records the order its literal used: copies made by object spread, rest destructuring, and record conversions keep their source's order, and assigning an optional key the object lacks, including after `delete`, moves the key last.
|
||||
A type keeps the declaration order of its first type, and `SC6002` reports object literals whose order differs where the program observes it, when one of its values gets its keys from a spread whose source order is not known, from a spread that also sets an optional key the source lacks, or from a copy of a value scriptc cannot trace, or when the type's values can take more than 16 orders.
|
||||
|
||||
A record converted from a checked value, such as `const rows: { score: number }[] = JSON.parse(text)`, keeps the properties its type does not declare.
|
||||
`JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `util.inspect`, and untyped views of the record see them, together with the declared fields, in the source object's key order.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
- **Local class expressions:** Static fields and static blocks in TypeScript files, decorators, private brands, generic members, and references through the class's internal name are unsupported.
|
||||
- **Local class expressions:** Static fields and static blocks in TypeScript files, decorators, private brands, and generic members are unsupported.
|
||||
Static methods read the enclosing function's bindings and are inherited by subclasses.
|
||||
A named class expression's methods and static members can refer to the class by its name.
|
||||
Supported expressions preserve fresh constructor identity and shared captures across constructors, methods, accessors, and arrow fields.
|
||||
Local inheritance requires a compiled base class.
|
||||
Constructors retain identity, `typeof`, `name`, and `length` through `unknown`; cast back to their exact type before construction.
|
||||
|
||||
@@ -3,6 +3,15 @@ Copied values, other native handles, and symbol keys are unsupported.
|
||||
Constructor seeds support arrays; custom iterators and detached methods are unsupported.
|
||||
Weak collections do not retain their keys, but values that refer back to a key can keep it alive until deletion or collection release.
|
||||
|
||||
`FinalizationRegistry` keeps its registrations: `register` and `unregister` check their arguments and answer as in Node.js.
|
||||
Its cleanup callbacks never run, which the ECMAScript specification allows.
|
||||
Program output matches Node.js, which runs cleanup callbacks only when its garbage collector decides to.
|
||||
Memory that a package frees only from a cleanup callback is not reclaimed until the program exits.
|
||||
For example, a wasm-bindgen package frees the WebAssembly memory behind a wrapper object from its cleanup callback, so a long-running server that drops such wrappers without calling their `free()` method keeps that memory.
|
||||
Call `free()` on wrappers the program no longer needs.
|
||||
Symbol targets and unregister tokens are unsupported.
|
||||
`WeakRef` is unsupported.
|
||||
|
||||
Memory uses reference counting.
|
||||
Acyclic values are freed deterministically, and cycles are collected at defined collection points.
|
||||
Native checked objects, arrays, Sets, and captured closures participate in cycle collection.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -11660,9 +11660,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -19328,6 +19328,14 @@
|
||||
"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/7154-crypto-cipher-info.cjs",
|
||||
"tests/corpus/7155-cipher-aes-ccm.cjs",
|
||||
"tests/corpus/7156-cipher-key-wrap.cjs",
|
||||
"tests/corpus/7157-crypto-cipher-info-typed.ts",
|
||||
"tests/corpus/node24-crypto-kdf-errors.ts",
|
||||
"tests/corpus/node24-crypto-kdf-options.ts",
|
||||
"tests/corpus/node24-crypto-kdf.ts",
|
||||
@@ -19658,8 +19666,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 +19693,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 +19723,10 @@
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.crypto.createCipheriv",
|
||||
"tests": [
|
||||
"tests/corpus/7155-cipher-aes-ccm.cjs",
|
||||
"tests/corpus/7156-cipher-key-wrap.cjs",
|
||||
"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 +19766,10 @@
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.crypto.createDecipheriv",
|
||||
"tests": [
|
||||
"tests/corpus/7155-cipher-aes-ccm.cjs",
|
||||
"tests/corpus/7156-cipher-key-wrap.cjs",
|
||||
"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 +20388,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 +20415,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"
|
||||
},
|
||||
@@ -20410,8 +20442,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.crypto.getCipherInfo"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.crypto.getCipherInfo",
|
||||
"tests": [
|
||||
"tests/corpus/7154-crypto-cipher-info.cjs",
|
||||
"tests/corpus/7157-crypto-cipher-info-typed.ts"
|
||||
]
|
||||
},
|
||||
"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/7155-cipher-aes-ccm.cjs", "tests/corpus/7156-cipher-key-wrap.cjs", "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/7155-cipher-aes-ccm.cjs", "tests/corpus/7156-cipher-key-wrap.cjs", "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,9 @@
|
||||
"node-builtin.os.version": ["tests/corpus/5140-os-host-facts.ts"],
|
||||
"node-builtin.os.constants": ["tests/corpus/5141-os-constants.ts"],
|
||||
"node-builtin.fs.constants": ["tests/corpus/6207-fs-constants-object.ts", "tests/corpus/6208-fs-constants-require.cjs"],
|
||||
"node-builtin.crypto.generatePrime": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"],
|
||||
"node-builtin.crypto.generatePrimeSync": ["tests/corpus/7152-crypto-prime.ts", "tests/corpus/7153-crypto-prime-errors.cjs"],
|
||||
"node-builtin.crypto.getCipherInfo": ["tests/corpus/7154-crypto-cipher-info.cjs", "tests/corpus/7157-crypto-cipher-info-typed.ts"],
|
||||
"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"],
|
||||
|
||||
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
minimum_os: "14.0",
|
||||
},
|
||||
program: { object, entry_symbol: "main" },
|
||||
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
|
||||
runtime_abi: { version: 36, marker: "scr_runtime_abi_v36" },
|
||||
runtime_pack: {
|
||||
kind: "precompiled",
|
||||
package: "@scriptc/runtime-darwin-arm64",
|
||||
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
() => null,
|
||||
(failure: { stderr?: string }) => failure,
|
||||
);
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v27");
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v36");
|
||||
});
|
||||
});
|
||||
|
||||
+369
-37
@@ -3171,12 +3171,17 @@ 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
|
||||
* chacha20-poly1305. Both are Transforms: written chunks go through
|
||||
* update(), and the end pushes final(). */
|
||||
/* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, gcm, ccm, ecb, cfb,
|
||||
* cfb8, cfb1, and ofb modes and their key wraps, des-ede3 and des-ede,
|
||||
* chacha20, and chacha20-poly1305. Both are Transforms: written chunks go
|
||||
* through update(), and the end pushes final(). */
|
||||
export type CipherCCMTypes = "aes-128-ccm" | "aes-192-ccm" | "aes-256-ccm";
|
||||
export type CipherGCMTypes = "aes-128-gcm" | "aes-192-gcm" | "aes-256-gcm";
|
||||
export type CipherChaCha20Poly1305Types = "chacha20-poly1305";
|
||||
export type CipherKey = BinaryLike | KeyObject;
|
||||
export interface CipherCCMOptions {
|
||||
authTagLength: number;
|
||||
}
|
||||
export interface CipherGCMOptions {
|
||||
authTagLength?: number;
|
||||
}
|
||||
@@ -3196,6 +3201,10 @@ declare module "crypto" {
|
||||
final(outputEncoding: BufferEncoding): string;
|
||||
setAutoPadding(autoPadding?: boolean): this;
|
||||
}
|
||||
export interface CipherCCM extends Cipheriv {
|
||||
setAAD(buffer: ArrayBufferView, options: { plaintextLength: number }): this;
|
||||
getAuthTag(): Buffer;
|
||||
}
|
||||
export interface CipherGCM extends Cipheriv {
|
||||
setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this;
|
||||
getAuthTag(): Buffer;
|
||||
@@ -3217,6 +3226,10 @@ declare module "crypto" {
|
||||
final(outputEncoding: BufferEncoding): string;
|
||||
setAutoPadding(autoPadding?: boolean): this;
|
||||
}
|
||||
export interface DecipherCCM extends Decipheriv {
|
||||
setAuthTag(buffer: ArrayBufferView): this;
|
||||
setAAD(buffer: ArrayBufferView, options: { plaintextLength: number }): this;
|
||||
}
|
||||
export interface DecipherGCM extends Decipheriv {
|
||||
setAuthTag(buffer: ArrayBufferView): this;
|
||||
setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this;
|
||||
@@ -3225,6 +3238,12 @@ declare module "crypto" {
|
||||
setAuthTag(buffer: ArrayBufferView): this;
|
||||
setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this;
|
||||
}
|
||||
export function createCipheriv(
|
||||
algorithm: CipherCCMTypes,
|
||||
key: CipherKey,
|
||||
iv: BinaryLike,
|
||||
options: CipherCCMOptions,
|
||||
): CipherCCM;
|
||||
export function createCipheriv(
|
||||
algorithm: CipherGCMTypes,
|
||||
key: CipherKey,
|
||||
@@ -3243,6 +3262,12 @@ declare module "crypto" {
|
||||
iv: BinaryLike | null,
|
||||
options?: { authTagLength?: number },
|
||||
): Cipheriv;
|
||||
export function createDecipheriv(
|
||||
algorithm: CipherCCMTypes,
|
||||
key: CipherKey,
|
||||
iv: BinaryLike,
|
||||
options: CipherCCMOptions,
|
||||
): DecipherCCM;
|
||||
export function createDecipheriv(
|
||||
algorithm: CipherGCMTypes,
|
||||
key: CipherKey,
|
||||
@@ -3351,6 +3376,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;
|
||||
@@ -3412,6 +3492,34 @@ declare module "crypto" {
|
||||
* per member like http2.constants. */
|
||||
export function getFips(): 1 | 0;
|
||||
export function getCiphers(): string[];
|
||||
export type CipherMode =
|
||||
| "cbc"
|
||||
| "ccm"
|
||||
| "cfb"
|
||||
| "ctr"
|
||||
| "ecb"
|
||||
| "gcm"
|
||||
| "ocb"
|
||||
| "ofb"
|
||||
| "stream"
|
||||
| "wrap"
|
||||
| "xts";
|
||||
export interface CipherInfoOptions {
|
||||
keyLength?: number | undefined;
|
||||
ivLength?: number | undefined;
|
||||
}
|
||||
export interface CipherInfo {
|
||||
name: string;
|
||||
nid: number;
|
||||
blockSize?: number | undefined;
|
||||
ivLength?: number | undefined;
|
||||
keyLength: number;
|
||||
mode: CipherMode;
|
||||
}
|
||||
export function getCipherInfo(
|
||||
nameOrNid: string | number,
|
||||
options?: CipherInfoOptions,
|
||||
): CipherInfo | undefined;
|
||||
export function getHashes(): string[];
|
||||
export function getCurves(): string[];
|
||||
/* Node v24 crypto.constants, literal-typed (the Http2Constants
|
||||
@@ -3512,7 +3620,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 +3630,7 @@ declare module "child_process" {
|
||||
timeout?: number;
|
||||
killSignal?: string;
|
||||
stdio?: "pipe" | "ignore" | "inherit" | ("pipe" | "ignore" | "inherit")[];
|
||||
env?: { [k: string]: string | undefined };
|
||||
windowsHide?: boolean;
|
||||
},
|
||||
): SpawnSyncReturns;
|
||||
@@ -3554,20 +3664,20 @@ declare module "child_process" {
|
||||
|
||||
export interface ChildProcess extends Disposable {
|
||||
ref(): void;
|
||||
on(event: "spawn", listener: () => void): void;
|
||||
once(event: "spawn", listener: () => void): void;
|
||||
on(event: "spawn", listener: () => void): this;
|
||||
once(event: "spawn", listener: () => void): this;
|
||||
/* The exit listener may also take Node's second parameter — the
|
||||
* terminating signal's name, null for a normal exit. */
|
||||
on(event: "exit", listener: (code: number | null, signal: string | null) => void): void;
|
||||
on(event: "close", listener: (code: number | null, signal: string | null) => void): void;
|
||||
on(event: "error", listener: (err: Error) => void): void;
|
||||
once(event: "exit", listener: (code: number | null, signal: string | null) => void): void;
|
||||
once(event: "close", listener: (code: number | null, signal: string | null) => void): void;
|
||||
once(event: "error", listener: (err: Error) => void): void;
|
||||
on(event: "message", listener: (message: any) => void): void;
|
||||
on(event: "disconnect", listener: () => void): void;
|
||||
once(event: "message", listener: (message: any) => void): void;
|
||||
once(event: "disconnect", listener: () => void): void;
|
||||
on(event: "exit", listener: (code: number | null, signal: string | null) => void): this;
|
||||
on(event: "close", listener: (code: number | null, signal: string | null) => void): this;
|
||||
on(event: "error", listener: (err: Error) => void): this;
|
||||
once(event: "exit", listener: (code: number | null, signal: string | null) => void): this;
|
||||
once(event: "close", listener: (code: number | null, signal: string | null) => void): this;
|
||||
once(event: "error", listener: (err: Error) => void): this;
|
||||
on(event: "message", listener: (message: any) => void): this;
|
||||
on(event: "disconnect", listener: () => void): this;
|
||||
once(event: "message", listener: (message: any) => void): this;
|
||||
once(event: "disconnect", listener: () => void): this;
|
||||
/* The lifecycle members, Node's exact shapes: pid is undefined exactly
|
||||
* when the spawn failed; exitCode is null while running, the code
|
||||
* after a normal exit, null for a signal death, and -errno once a
|
||||
@@ -3780,9 +3890,8 @@ declare module "util" {
|
||||
export type TextDecoder = globalThis.TextDecoder;
|
||||
export var TextEncoder: typeof globalThis.TextEncoder;
|
||||
export var TextDecoder: typeof globalThis.TextDecoder;
|
||||
/* stream.pipeline's (...streams: unknown[]) => unknown is assignable to
|
||||
* every other target's parameter type, so its overload comes first; no
|
||||
* other target is assignable to it. */
|
||||
/* stream.pipeline's overload comes first: its __promisify__ member keeps
|
||||
* every other target out of it. */
|
||||
export function promisify(
|
||||
fn: typeof import("node:stream").pipeline,
|
||||
): typeof import("node:stream/promises").pipeline;
|
||||
@@ -5504,6 +5613,7 @@ declare module "http" {
|
||||
readonly trailersDistinct: { [name: string]: string[] | undefined };
|
||||
readonly rawTrailers: string[];
|
||||
resume(): void;
|
||||
pause(): void;
|
||||
destroy(): void;
|
||||
setTimeout(msecs: number, callback?: () => void): this;
|
||||
/* setEncoding('utf8'): 'data' delivers strings (other real encodings
|
||||
@@ -5929,6 +6039,7 @@ declare module "http2" {
|
||||
export interface Http2Stream {
|
||||
on(event: "error" | "close", listener: (err: Error) => void): void;
|
||||
once(event: "error" | "close", listener: (err: Error) => void): void;
|
||||
addListener(event: "error" | "close", listener: (err: Error) => void): void;
|
||||
destroy(): void;
|
||||
}
|
||||
export interface Http2ServerRequest {
|
||||
@@ -6029,11 +6140,6 @@ declare module "http2" {
|
||||
listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void,
|
||||
): void;
|
||||
on(event: "session", listener: (session: ServerHttp2Session) => void): void;
|
||||
once(
|
||||
event: "stream",
|
||||
listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void,
|
||||
): void;
|
||||
once(event: "session", listener: (session: ServerHttp2Session) => void): void;
|
||||
/* HTTP CONNECT (and, in Node, the h2 extended CONNECT — which the
|
||||
* allowHTTP1 lowering never sees): the second argument is the raw
|
||||
* socket on HTTP/1.1 connections; narrow with instanceof. */
|
||||
@@ -6051,6 +6157,42 @@ declare module "http2" {
|
||||
): void;
|
||||
once(event: "error", listener: (err: Error) => void): void;
|
||||
once(event: "close", listener: () => void): void;
|
||||
once(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void;
|
||||
once(
|
||||
event: "stream",
|
||||
listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void,
|
||||
): void;
|
||||
once(event: "session", listener: (session: ServerHttp2Session) => void): void;
|
||||
once(
|
||||
event: "connect",
|
||||
listener: (
|
||||
req: Http2ServerRequest | import("http").IncomingMessage,
|
||||
resOrSocket: Http2ServerResponse | Socket,
|
||||
) => void,
|
||||
): void;
|
||||
addListener(event: "connection", listener: (socket: Socket) => void): void;
|
||||
addListener(
|
||||
event: "request",
|
||||
listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void,
|
||||
): void;
|
||||
addListener(event: "error", listener: (err: Error) => void): void;
|
||||
addListener(event: "close", listener: () => void): void;
|
||||
addListener(
|
||||
event: "sessionError",
|
||||
listener: (err: Error, session: ServerHttp2Session) => void,
|
||||
): void;
|
||||
addListener(
|
||||
event: "stream",
|
||||
listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void,
|
||||
): void;
|
||||
addListener(event: "session", listener: (session: ServerHttp2Session) => void): void;
|
||||
addListener(
|
||||
event: "connect",
|
||||
listener: (
|
||||
req: Http2ServerRequest | import("http").IncomingMessage,
|
||||
resOrSocket: Http2ServerResponse | Socket,
|
||||
) => void,
|
||||
): void;
|
||||
}
|
||||
export interface SecureServerOptions {
|
||||
allowHTTP1?: boolean;
|
||||
@@ -6392,6 +6534,28 @@ declare module "http2" {
|
||||
listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void,
|
||||
): void;
|
||||
once(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void;
|
||||
once(event: "frameError", listener: (type: number, code: number, id: number) => void): void;
|
||||
once(event: "ping", listener: (payload: Uint8Array) => void): void;
|
||||
addListener(event: "close" | "timeout", listener: () => void): void;
|
||||
addListener(event: "error", listener: (err: Error) => void): void;
|
||||
addListener(
|
||||
event: "connect",
|
||||
listener: (session: Http2Session, socket: import("net").Socket) => void,
|
||||
): void;
|
||||
addListener(
|
||||
event: "stream",
|
||||
listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void,
|
||||
): void;
|
||||
addListener(
|
||||
event: "goaway",
|
||||
listener: (errorCode: number, lastStreamID: number, opaqueData?: Uint8Array) => void,
|
||||
): void;
|
||||
addListener(event: "localSettings" | "remoteSettings", listener: (settings: any) => void): void;
|
||||
addListener(
|
||||
event: "frameError",
|
||||
listener: (type: number, code: number, id: number) => void,
|
||||
): void;
|
||||
addListener(event: "ping", listener: (payload: Uint8Array) => void): void;
|
||||
}
|
||||
|
||||
export interface ClientHttp2Session extends Http2Session {
|
||||
@@ -6469,7 +6633,30 @@ declare module "http2" {
|
||||
listener: () => void,
|
||||
): void;
|
||||
once(event: "error", listener: (err: Error) => void): void;
|
||||
once(event: "frameError", listener: (type: number, code: number, id: number) => void): void;
|
||||
once(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void;
|
||||
addListener(event: "data", listener: (chunk: any) => void): void;
|
||||
addListener(
|
||||
event:
|
||||
| "end"
|
||||
| "close"
|
||||
| "aborted"
|
||||
| "ready"
|
||||
| "timeout"
|
||||
| "drain"
|
||||
| "finish"
|
||||
| "wantTrailers",
|
||||
listener: () => void,
|
||||
): void;
|
||||
addListener(event: "error", listener: (err: Error) => void): void;
|
||||
addListener(
|
||||
event: "frameError",
|
||||
listener: (type: number, code: number, id: number) => void,
|
||||
): void;
|
||||
addListener(
|
||||
event: "trailers",
|
||||
listener: (trailers: IncomingHttpHeaders, flags: number) => void,
|
||||
): void;
|
||||
respond(
|
||||
headers?: OutgoingHttpHeaders,
|
||||
options?: { endStream?: boolean; waitForTrailers?: boolean },
|
||||
@@ -6519,6 +6706,7 @@ declare module "http2" {
|
||||
on(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void;
|
||||
once(event: "response", listener: (headers: IncomingHttpHeaders, flags: number) => void): void;
|
||||
once(event: "push", listener: (headers: IncomingHttpHeaders, flags: number) => void): void;
|
||||
once(event: "headers", listener: (headers: IncomingHttpHeaders, flags: number) => void): void;
|
||||
once(event: "data", listener: (chunk: any) => void): void;
|
||||
once(
|
||||
event:
|
||||
@@ -6533,6 +6721,42 @@ declare module "http2" {
|
||||
listener: () => void,
|
||||
): void;
|
||||
once(event: "error", listener: (err: Error) => void): void;
|
||||
once(event: "frameError", listener: (type: number, code: number, id: number) => void): void;
|
||||
once(event: "trailers", listener: (trailers: IncomingHttpHeaders, flags: number) => void): void;
|
||||
addListener(
|
||||
event: "response",
|
||||
listener: (headers: IncomingHttpHeaders, flags: number) => void,
|
||||
): void;
|
||||
addListener(
|
||||
event: "push",
|
||||
listener: (headers: IncomingHttpHeaders, flags: number) => void,
|
||||
): void;
|
||||
addListener(
|
||||
event: "headers",
|
||||
listener: (headers: IncomingHttpHeaders, flags: number) => void,
|
||||
): void;
|
||||
addListener(event: "data", listener: (chunk: any) => void): void;
|
||||
addListener(
|
||||
event:
|
||||
| "end"
|
||||
| "close"
|
||||
| "aborted"
|
||||
| "ready"
|
||||
| "timeout"
|
||||
| "drain"
|
||||
| "finish"
|
||||
| "wantTrailers",
|
||||
listener: () => void,
|
||||
): void;
|
||||
addListener(event: "error", listener: (err: Error) => void): void;
|
||||
addListener(
|
||||
event: "frameError",
|
||||
listener: (type: number, code: number, id: number) => void,
|
||||
): void;
|
||||
addListener(
|
||||
event: "trailers",
|
||||
listener: (trailers: IncomingHttpHeaders, flags: number) => void,
|
||||
): void;
|
||||
}
|
||||
|
||||
export interface Http2Server {
|
||||
@@ -6567,6 +6791,24 @@ declare module "http2" {
|
||||
): void;
|
||||
once(event: "error", listener: (err: Error) => void): void;
|
||||
once(event: "close" | "listening" | "timeout", listener: () => void): void;
|
||||
once(event: "connection", listener: (socket: import("net").Socket) => void): void;
|
||||
once(event: "sessionError", listener: (err: Error, session: ServerHttp2Session) => void): void;
|
||||
addListener(
|
||||
event: "stream",
|
||||
listener: (stream: ServerHttp2Stream, headers: IncomingHttpHeaders, flags: number) => void,
|
||||
): void;
|
||||
addListener(event: "session", listener: (session: ServerHttp2Session) => void): void;
|
||||
addListener(
|
||||
event: "request",
|
||||
listener: (req: Http2ServerRequest, res: Http2ServerResponse) => void,
|
||||
): void;
|
||||
addListener(event: "error", listener: (err: Error) => void): void;
|
||||
addListener(event: "close" | "listening" | "timeout", listener: () => void): void;
|
||||
addListener(event: "connection", listener: (socket: import("net").Socket) => void): void;
|
||||
addListener(
|
||||
event: "sessionError",
|
||||
listener: (err: Error, session: ServerHttp2Session) => void,
|
||||
): void;
|
||||
}
|
||||
|
||||
export interface ServerOptions {
|
||||
@@ -7090,7 +7332,11 @@ declare module "stream" {
|
||||
encoding: string,
|
||||
callback: (error?: Error | null) => void,
|
||||
): void;
|
||||
writev?(this: Writable, chunks: unknown[], callback: (error?: Error | null) => void): void;
|
||||
writev?(
|
||||
this: Writable,
|
||||
chunks: Array<{ chunk: any; encoding: string }>,
|
||||
callback: (error?: Error | null) => void,
|
||||
): void;
|
||||
final?(this: Writable, callback: (error?: Error | null) => void): void;
|
||||
destroy?(this: Writable, error: Error | null, callback: (error?: Error | null) => void): void;
|
||||
construct?(this: Writable, callback: (error?: Error | null) => void): void;
|
||||
@@ -7125,7 +7371,11 @@ declare module "stream" {
|
||||
readable?: boolean;
|
||||
writable?: boolean;
|
||||
defaultEncoding?: string;
|
||||
writev?(this: Duplex, chunks: unknown[], callback: (error?: Error | null) => void): void;
|
||||
writev?(
|
||||
this: Duplex,
|
||||
chunks: Array<{ chunk: any; encoding: string }>,
|
||||
callback: (error?: Error | null) => void,
|
||||
): void;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
interface TransformOptions {
|
||||
@@ -7210,6 +7460,36 @@ declare module "stream" {
|
||||
): void;
|
||||
} & ObjectModeFlag;
|
||||
|
||||
/* decodeStrings: false — a string write reaches write/transform as the
|
||||
* string with its encoding (Buffers keep 'buffer'). */
|
||||
interface StringWritableOptions extends Omit<WritableOptions, "decodeStrings" | "write"> {
|
||||
decodeStrings: false;
|
||||
write?(
|
||||
this: Writable,
|
||||
chunk: string | Buffer,
|
||||
encoding: string,
|
||||
callback: (error?: Error | null) => void,
|
||||
): void;
|
||||
}
|
||||
type StringDuplexOptions = Omit<DuplexOptions, "decodeStrings" | "write"> & {
|
||||
decodeStrings: false;
|
||||
write?(
|
||||
this: Duplex,
|
||||
chunk: string | Buffer,
|
||||
encoding: string,
|
||||
callback: (error?: Error | null) => void,
|
||||
): void;
|
||||
};
|
||||
type StringTransformOptions = Omit<TransformOptions, "decodeStrings" | "transform"> & {
|
||||
decodeStrings: false;
|
||||
transform?(
|
||||
this: Transform,
|
||||
chunk: string | Buffer,
|
||||
encoding: string,
|
||||
callback: (error?: Error | null, data?: Buffer | string) => void,
|
||||
): void;
|
||||
};
|
||||
|
||||
/* The operators' options: concurrency and highWaterMark bound map's
|
||||
* in-flight callbacks and queued results. */
|
||||
interface ArrayOptions {
|
||||
@@ -7228,12 +7508,16 @@ declare module "stream" {
|
||||
_destroy(error: Error | null, callback: (error?: Error | null) => void): void;
|
||||
[Symbol.asyncIterator](): AsyncIterableIterator<any>;
|
||||
iterator(options?: { destroyOnReturn?: boolean }): AsyncIterableIterator<any>;
|
||||
/* compose(this, stage), destroyed when options.signal aborts. */
|
||||
compose(stage: ComposeStage, options?: { signal: AbortSignal }): Duplex;
|
||||
push(chunk: Buffer | string | null, encoding?: string): boolean;
|
||||
/* An object-mode stream's values. */
|
||||
push(chunk: any, encoding?: string): boolean;
|
||||
unshift(chunk: Buffer | string, encoding?: string): void;
|
||||
unshift(chunk: any, encoding?: string): void;
|
||||
read(size?: number): Buffer | null;
|
||||
/* A Buffer, a string once encoded, an object-mode value, or null (as
|
||||
* @types/node types it). */
|
||||
read(size?: number): any;
|
||||
pause(): this;
|
||||
resume(): this;
|
||||
isPaused(): boolean;
|
||||
@@ -7251,6 +7535,16 @@ declare module "stream" {
|
||||
readonly destroyed: boolean;
|
||||
readonly errored: Error | null;
|
||||
static from(iterable: unknown, options?: ReadableOptions): Readable;
|
||||
/* A Readable over a web ReadableStream's reader. */
|
||||
static fromWeb(
|
||||
readableStream: globalThis.ReadableStream,
|
||||
options?: {
|
||||
encoding?: string;
|
||||
highWaterMark?: number;
|
||||
objectMode?: boolean;
|
||||
signal?: AbortSignal;
|
||||
},
|
||||
): Readable;
|
||||
/* The Readable operators: chunks are strings or Buffers, map and
|
||||
* flatMap callbacks return strings or Buffers (flatMap: arrays of
|
||||
* them, or strings), and callbacks that declare the options argument
|
||||
@@ -7340,6 +7634,7 @@ declare module "stream" {
|
||||
|
||||
class Writable extends EventEmitter {
|
||||
constructor(opts: ObjectWritableOptions);
|
||||
constructor(opts: StringWritableOptions);
|
||||
constructor(opts?: WritableOptions);
|
||||
/* The underscore-method surface (see Readable). */
|
||||
_write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void;
|
||||
@@ -7417,7 +7712,11 @@ declare module "stream" {
|
||||
* mirrors @types/node: extends Readable, the writable half declared
|
||||
* again. The lowering treats both halves as first-class. */
|
||||
class Duplex extends Readable {
|
||||
/* Duplex.from: an async generator function, an iterable, a string or
|
||||
* a stream (duplexify.js). */
|
||||
static from(body: unknown): Duplex;
|
||||
constructor(opts: ObjectDuplexOptions);
|
||||
constructor(opts: StringDuplexOptions);
|
||||
constructor(opts?: DuplexOptions);
|
||||
/* The underscore-method surface (see Readable). */
|
||||
_write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void;
|
||||
@@ -7517,6 +7816,7 @@ declare module "stream" {
|
||||
|
||||
class Transform extends Duplex {
|
||||
constructor(opts: ObjectTransformOptions);
|
||||
constructor(opts: StringTransformOptions);
|
||||
constructor(opts?: TransformOptions);
|
||||
/* The underscore-method surface (see Readable). */
|
||||
_transform(
|
||||
@@ -7528,17 +7828,43 @@ declare module "stream" {
|
||||
}
|
||||
class PassThrough extends Transform {
|
||||
constructor(opts: ObjectTransformOptions);
|
||||
constructor(opts: StringTransformOptions);
|
||||
constructor(opts?: TransformOptions);
|
||||
}
|
||||
|
||||
/* addAbortSignal(signal, stream): the stream, destroyed with an
|
||||
* AbortError (cause: signal.reason) when the signal aborts. */
|
||||
function addAbortSignal<T extends Readable | Writable>(signal: AbortSignal, stream: T): T;
|
||||
/* Declared so requires typecheck; each fences at its use site. */
|
||||
function pipeline(...streams: unknown[]): unknown;
|
||||
/* compose(...streams): a Duplex that writes to the first stream and
|
||||
* reads from the last, with the streams running as a pipeline. */
|
||||
function compose(...streams: Array<Readable | Writable>): Duplex;
|
||||
/* pipeline(...stages, callback): the last stream, or the object-mode
|
||||
* PassThrough that a function destination feeds. */
|
||||
type PipelineResult<T extends unknown[]> = T extends [
|
||||
...unknown[],
|
||||
infer D,
|
||||
(...args: any[]) => unknown,
|
||||
]
|
||||
? D extends (...args: any[]) => unknown
|
||||
? PassThrough
|
||||
: D
|
||||
: unknown;
|
||||
function pipeline<T extends unknown[]>(...streams: T): PipelineResult<T>;
|
||||
/* util.promisify's pipeline overload matches only pipeline itself: a
|
||||
* generic signature alone admits every function. */
|
||||
namespace pipeline {
|
||||
const __promisify__: typeof promises.pipeline;
|
||||
}
|
||||
/* A compose stage: a stream, an async generator function over the
|
||||
* previous stage, or (first) an iterable, async iterable or web
|
||||
* ReadableStream. */
|
||||
type ComposeStage =
|
||||
| Readable
|
||||
| Writable
|
||||
| Iterable<unknown>
|
||||
| AsyncIterable<unknown>
|
||||
| globalThis.ReadableStream
|
||||
| ((source: any, options?: { signal: AbortSignal }) => unknown);
|
||||
/* compose(...streams): a Duplex that writes to the first stage and
|
||||
* reads from the last, with the stages running as a pipeline. */
|
||||
function compose(...streams: ComposeStage[]): Duplex;
|
||||
/* finished's options: readable/writable choose which ends count,
|
||||
* error: false ignores 'error' events, signal aborts the wait. */
|
||||
interface FinishedOptions {
|
||||
@@ -7557,13 +7883,20 @@ declare module "stream" {
|
||||
* else 65536 — Node's own state.js split); literal argument only. */
|
||||
function getDefaultHighWaterMark(objectMode: boolean): number;
|
||||
/* The promise pipeline's result: a last stage that is an async
|
||||
* function resolves the promise with its value. */
|
||||
* function resolves the promise with its value (before the options
|
||||
* argument, when one follows). */
|
||||
type PipelinePromise<T extends unknown[]> = T extends [
|
||||
...unknown[],
|
||||
(...args: any[]) => Promise<infer P>,
|
||||
]
|
||||
? Promise<P>
|
||||
: Promise<void>;
|
||||
: T extends [
|
||||
...unknown[],
|
||||
(...args: any[]) => Promise<infer P>,
|
||||
{ signal?: AbortSignal; end?: boolean },
|
||||
]
|
||||
? Promise<P>
|
||||
: Promise<void>;
|
||||
namespace promises {
|
||||
function pipeline<T extends unknown[]>(...streams: T): PipelinePromise<T>;
|
||||
function finished(
|
||||
@@ -7593,6 +7926,7 @@ declare module "stream" {
|
||||
DuplexOptions,
|
||||
TransformOptions,
|
||||
FinishedOptions,
|
||||
PipelinePromise,
|
||||
};
|
||||
}
|
||||
declare module "node:stream" {
|
||||
@@ -7607,9 +7941,7 @@ declare module "stream/promises" {
|
||||
* pipeline's trailing { signal, end } object and finished's options
|
||||
* (readable, writable, error, cleanup, signal) are supported. A last
|
||||
* stage that is an async function resolves the promise with its value. */
|
||||
function pipeline<T extends unknown[]>(
|
||||
...streams: T
|
||||
): T extends [...unknown[], (...args: any[]) => Promise<infer P>] ? Promise<P> : Promise<void>;
|
||||
function pipeline<T extends unknown[]>(...streams: T): import("stream").PipelinePromise<T>;
|
||||
function finished(
|
||||
stream: unknown,
|
||||
options?: {
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,10 @@
|
||||
#ifndef _WIN32
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#endif
|
||||
/* flock and LOCK_EX/LOCK_UN, which Darwin hides under _POSIX_C_SOURCE. */
|
||||
#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE)
|
||||
#define _DARWIN_C_SOURCE 1
|
||||
#endif
|
||||
#include <stdint.h>
|
||||
#include <limits.h>
|
||||
#include <stddef.h>
|
||||
@@ -56,3 +60,78 @@ void scr_collect_cycles_phase(void);
|
||||
void scriptc_native_collect_cycles(void) {
|
||||
scr_collect_cycles_phase();
|
||||
}
|
||||
|
||||
/* Forked lowering workers (frontend/process-fork.ts). A worker shares the
|
||||
* parent's heap copy-on-write, writes its results to a file and leaves with
|
||||
* _exit, so no buffered output or exit handler of the parent runs twice.
|
||||
* Checker requests from workers hold an exclusive flock on a file each
|
||||
* worker opens after the fork, so every worker has its own open file
|
||||
* description and the lock excludes the other workers. */
|
||||
#ifdef _WIN32
|
||||
int32_t scriptc_native_fork(void) { return -1; }
|
||||
void scriptc_native_exit(int32_t code) { ExitProcess((UINT)code); }
|
||||
int32_t scriptc_native_wait(int32_t pid) { (void)pid; return -1; }
|
||||
uint8_t scriptc_native_lock_open(const uint8_t *bytes, size_t size) { (void)bytes; (void)size; return 0; }
|
||||
void scriptc_native_lock(void) {}
|
||||
void scriptc_native_unlock(void) {}
|
||||
#else
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
int32_t scriptc_native_fork(void) { return (int32_t)fork(); }
|
||||
|
||||
void scriptc_native_exit(int32_t code) { _exit(code); }
|
||||
|
||||
/* The worker's exit status, 128 plus the signal for a worker a signal
|
||||
* ended, or -1 when waitpid fails. */
|
||||
int32_t scriptc_native_wait(int32_t pid) {
|
||||
int status = 0;
|
||||
while (waitpid((pid_t)pid, &status, 0) < 0) {
|
||||
if (errno != EINTR) return -1;
|
||||
}
|
||||
if (WIFEXITED(status)) return WEXITSTATUS(status);
|
||||
if (WIFSIGNALED(status)) return 128 + WTERMSIG(status);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int scriptc_native_lock_fd = -1;
|
||||
|
||||
uint8_t scriptc_native_lock_open(const uint8_t *bytes, size_t size) {
|
||||
if (size == 0 || memchr(bytes, 0, size) != NULL) return 0;
|
||||
char *path = malloc(size + 1);
|
||||
if (!path) return 0;
|
||||
memcpy(path, bytes, size);
|
||||
path[size] = 0;
|
||||
if (scriptc_native_lock_fd >= 0) close(scriptc_native_lock_fd);
|
||||
scriptc_native_lock_fd = open(path, O_CREAT | O_RDWR | O_CLOEXEC, 0600);
|
||||
free(path);
|
||||
return scriptc_native_lock_fd >= 0;
|
||||
}
|
||||
|
||||
void scriptc_native_lock(void) {
|
||||
if (scriptc_native_lock_fd < 0) return;
|
||||
while (flock(scriptc_native_lock_fd, LOCK_EX) < 0 && errno == EINTR) {
|
||||
}
|
||||
}
|
||||
|
||||
void scriptc_native_unlock(void) {
|
||||
if (scriptc_native_lock_fd >= 0) flock(scriptc_native_lock_fd, LOCK_UN);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* The runtime's growth cap for full cycle passes (scr_rc.h). */
|
||||
void scr_cyc_set_growth_cap(long cap);
|
||||
|
||||
/* Bounds the cyclic garbage a build can hold to twice its live heap.
|
||||
* Lowering a large program leaves hundreds of megabytes of cyclic garbage,
|
||||
* and whether a scheduled full pass reclaims it before the build's peak
|
||||
* depends on where the growth rule fires: with the default cap of 16, small
|
||||
* changes to the compiler moved the peak of one build by 390 MB. With a cap
|
||||
* of 2 a full pass runs once the heap has grown to three times its size
|
||||
* after the last one; a cap of 1 bounds the garbage further but runs more
|
||||
* passes over the live heap. SCR_CYCLE_GROWTH_CAP still overrides it. */
|
||||
void scriptc_native_bound_cycle_growth(void) {
|
||||
scr_cyc_set_growth_cap(2);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
"ffi_format": 6,
|
||||
"functions": [
|
||||
{ "name": "compilerNativePrivateDirectory", "symbol": "scriptc_native_private_directory", "params": ["string", "bool"], "returns": "bool" },
|
||||
{ "name": "compilerNativeCollectCycles", "symbol": "scriptc_native_collect_cycles", "params": [], "returns": "void" }
|
||||
{ "name": "compilerNativeCollectCycles", "symbol": "scriptc_native_collect_cycles", "params": [], "returns": "void" },
|
||||
{ "name": "compilerNativeFork", "symbol": "scriptc_native_fork", "params": [], "returns": "i32" },
|
||||
{ "name": "compilerNativeExit", "symbol": "scriptc_native_exit", "params": ["i32"], "returns": "void" },
|
||||
{ "name": "compilerNativeWait", "symbol": "scriptc_native_wait", "params": ["i32"], "returns": "i32" },
|
||||
{ "name": "compilerNativeLockOpen", "symbol": "scriptc_native_lock_open", "params": ["string"], "returns": "bool" },
|
||||
{ "name": "compilerNativeLock", "symbol": "scriptc_native_lock", "params": [], "returns": "void" },
|
||||
{ "name": "compilerNativeUnlock", "symbol": "scriptc_native_unlock", "params": [], "returns": "void" },
|
||||
{ "name": "compilerNativeBoundCycleGrowth", "symbol": "scriptc_native_bound_cycle_growth", "params": [], "returns": "void" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@scriptc/runtime": "workspace:*",
|
||||
"tsc-ts": "0.0.7",
|
||||
"tsc-ts": "0.0.8",
|
||||
"typescript": "7.0.2",
|
||||
"typescript5": "npm:typescript@5.9.3"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,645 @@
|
||||
// scriptc's built-in implementation of `@hono/node-server` 2.x: the Node.js
|
||||
// adapter that serves a fetch-style handler (Hono's `app.fetch`) over
|
||||
// node:http. The published package swaps the global Request and Response
|
||||
// for lightweight objects and reparents prototypes, which only pays off
|
||||
// under V8; this port builds native Request objects from the incoming
|
||||
// message, with a body streamed from it as the app reads it, and writes
|
||||
// the handler's Response the way the package's listener does (header
|
||||
// record, content length for bodies that complete within the first reads,
|
||||
// streamed bodies, error responses), so a client sees the same status
|
||||
// lines, headers and bodies. A body the app leaves unread is drained after
|
||||
// the response, within the package's limits.
|
||||
//
|
||||
// HTTP/1.1 only: the http2/https server options and WebSocket upgrades are
|
||||
// not declared. A Response that is still exactly its constructor arguments
|
||||
// takes the package's cached-response path (its lightweight Response), read
|
||||
// from the runtime through scriptcResponseLight; every other Response takes
|
||||
// the general path.
|
||||
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
|
||||
export type HttpBindings = {
|
||||
incoming: IncomingMessage;
|
||||
outgoing: ServerResponse;
|
||||
};
|
||||
|
||||
export type ServerType = Server;
|
||||
|
||||
export type FetchCallback = (request: Request, env: HttpBindings) => Response | Promise<Response>;
|
||||
|
||||
export type CustomErrorHandler = (err: unknown) => void | Response | Promise<void | Response>;
|
||||
|
||||
export type Options = {
|
||||
fetch: FetchCallback;
|
||||
overrideGlobalObjects?: boolean;
|
||||
autoCleanupIncoming?: boolean;
|
||||
port?: number;
|
||||
hostname?: string;
|
||||
};
|
||||
|
||||
export interface RequestListenerOptions {
|
||||
hostname?: string;
|
||||
errorHandler?: CustomErrorHandler;
|
||||
overrideGlobalObjects?: boolean;
|
||||
autoCleanupIncoming?: boolean;
|
||||
}
|
||||
|
||||
export class RequestError extends Error {
|
||||
constructor(message: string, options?: { cause?: unknown }) {
|
||||
super(message, options);
|
||||
this.name = "RequestError";
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_CONTENT_TYPE = "text/plain; charset=UTF-8";
|
||||
const X_ALREADY_SENT = "x-hono-already-sent";
|
||||
|
||||
const reValidRequestUrl = /^\/[!#$&-;=?-[\]_a-z~]*$/;
|
||||
const reDotSegment = /\/\.\.?(?:[/?#]|$)/;
|
||||
const reValidHost = /^[a-z0-9._-]+(?::(?:[1-5]\d{3,4}|[6-9]\d{3}))?$/;
|
||||
|
||||
function buildUrl(scheme: string, host: string, incomingUrl: string): string {
|
||||
const url = `${scheme}://${host}${incomingUrl}`;
|
||||
if (!reValidHost.test(host)) {
|
||||
const urlObj = new URL(url);
|
||||
if (
|
||||
urlObj.hostname.length !== host.length &&
|
||||
urlObj.hostname !== (host.includes(":") ? host.replace(/:\d+$/, "") : host).toLowerCase()
|
||||
)
|
||||
throw new RequestError("Invalid host header");
|
||||
return urlObj.href;
|
||||
}
|
||||
if (incomingUrl.length === 0) return url + "/";
|
||||
if (incomingUrl.charCodeAt(0) !== 47) throw new RequestError("Invalid URL");
|
||||
if (!reValidRequestUrl.test(incomingUrl) || reDotSegment.test(incomingUrl))
|
||||
return new URL(url).href;
|
||||
return url;
|
||||
}
|
||||
|
||||
function normalizeIncomingMethod(method: string | undefined): string {
|
||||
if (method === undefined || method.length === 0) return "GET";
|
||||
switch (method) {
|
||||
case "DELETE":
|
||||
case "GET":
|
||||
case "HEAD":
|
||||
case "OPTIONS":
|
||||
case "PATCH":
|
||||
case "POST":
|
||||
case "PUT":
|
||||
case "QUERY":
|
||||
return method;
|
||||
}
|
||||
const upper = method.toUpperCase();
|
||||
switch (upper) {
|
||||
case "DELETE":
|
||||
case "GET":
|
||||
case "HEAD":
|
||||
case "OPTIONS":
|
||||
case "POST":
|
||||
case "PUT":
|
||||
return upper;
|
||||
default:
|
||||
return method;
|
||||
}
|
||||
}
|
||||
|
||||
/** The request URL the package builds: an absolute request target as is,
|
||||
* otherwise the Host header (or the configured hostname) over http. */
|
||||
function requestUrl(incoming: IncomingMessage, defaultHostname: string | undefined): string {
|
||||
const incomingUrl = incoming.url ?? "";
|
||||
if (
|
||||
incomingUrl[0] !== "/" &&
|
||||
(incomingUrl.startsWith("http://") || incomingUrl.startsWith("https://"))
|
||||
) {
|
||||
try {
|
||||
return new URL(incomingUrl).href;
|
||||
} catch (e) {
|
||||
throw new RequestError("Invalid absolute URL", { cause: e });
|
||||
}
|
||||
}
|
||||
const hostHeader = incoming.headers.host;
|
||||
const host = hostHeader !== undefined && hostHeader !== "" ? hostHeader : (defaultHostname ?? "");
|
||||
if (host === "") throw new RequestError("Missing host header");
|
||||
try {
|
||||
return buildUrl("http", host, incomingUrl);
|
||||
} catch (e) {
|
||||
if (e instanceof RequestError) throw e;
|
||||
throw new RequestError("Invalid URL", { cause: e });
|
||||
}
|
||||
}
|
||||
|
||||
function requestHeaders(incoming: IncomingMessage): Headers {
|
||||
const headers = new Headers();
|
||||
const raw = incoming.rawHeaders;
|
||||
for (let i = 0; i < raw.length; i += 2) {
|
||||
const name = raw[i]!;
|
||||
if (!name.startsWith(":")) headers.append(name, raw[i + 1]!);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
/** What the listener knows about the incoming body: whether it ended
|
||||
* (the package's `readableEnded`) and whether a drain started. */
|
||||
type IncomingState = { ended: boolean; draining: boolean };
|
||||
|
||||
/** The incoming message's readableHighWaterMark: Readable.toWeb queues
|
||||
* this many bytes before it pauses the message. */
|
||||
const BODY_HIGH_WATER_MARK = 64 * 1024;
|
||||
|
||||
/** The request body as the package streams it: Readable.toWeb over the
|
||||
* incoming message, read one chunk per pull. Chunks queue until
|
||||
* BODY_HIGH_WATER_MARK bytes wait unread, then the message pauses until a
|
||||
* pull takes them, so the socket holds the rest. The stream pulls once on
|
||||
* its own when it is constructed; the package builds its stream only when
|
||||
* the app reads the body, so until the app's first read a drain discards
|
||||
* the chunks instead of queueing them. */
|
||||
function incomingBody(incoming: IncomingMessage, state: IncomingState): ReadableStream<Uint8Array> {
|
||||
let pulls = 0;
|
||||
let listening = false;
|
||||
let ended = false;
|
||||
let failure: Error | undefined = undefined;
|
||||
const queue: Buffer[] = [];
|
||||
let queued = 0;
|
||||
let wake: (() => void) | undefined = undefined;
|
||||
const woken = (): void => {
|
||||
const resolve = wake;
|
||||
wake = undefined;
|
||||
if (resolve !== undefined) resolve();
|
||||
};
|
||||
const listen = (): void => {
|
||||
listening = true;
|
||||
incoming.on("data", (chunk: Buffer) => {
|
||||
if (state.draining && pulls === 1) return;
|
||||
queue.push(chunk);
|
||||
queued += chunk.length;
|
||||
if (queued >= BODY_HIGH_WATER_MARK) incoming.pause();
|
||||
woken();
|
||||
});
|
||||
incoming.on("end", () => {
|
||||
ended = true;
|
||||
woken();
|
||||
});
|
||||
incoming.on("error", (err: Error) => {
|
||||
failure = err;
|
||||
woken();
|
||||
});
|
||||
};
|
||||
return new ReadableStream<Uint8Array>({
|
||||
async pull(controller) {
|
||||
pulls++;
|
||||
if (!listening) listen();
|
||||
for (;;) {
|
||||
const chunk = queue.shift();
|
||||
if (chunk !== undefined) {
|
||||
queued -= chunk.length;
|
||||
if (queued < BODY_HIGH_WATER_MARK) incoming.resume();
|
||||
controller.enqueue(chunk);
|
||||
return;
|
||||
}
|
||||
if (failure !== undefined) {
|
||||
controller.error(failure);
|
||||
return;
|
||||
}
|
||||
if (ended) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
await new Promise<void>((resolve) => {
|
||||
wake = resolve;
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function newRequest(
|
||||
incoming: IncomingMessage,
|
||||
defaultHostname: string | undefined,
|
||||
state: IncomingState,
|
||||
): Request {
|
||||
const method = normalizeIncomingMethod(incoming.method);
|
||||
const url = requestUrl(incoming, defaultHostname);
|
||||
const headers = requestHeaders(incoming);
|
||||
if (method === "GET" || method === "HEAD") return new Request(url, { method, headers });
|
||||
return new Request(url, {
|
||||
method,
|
||||
headers,
|
||||
body: incomingBody(incoming, state),
|
||||
duplex: "half",
|
||||
});
|
||||
}
|
||||
|
||||
const DRAIN_TIMEOUT_MS = 500;
|
||||
const MAX_DRAIN_BYTES = 64 * 1024 * 1024;
|
||||
|
||||
/** The package's drain of a body the app left unread: read and discard
|
||||
* up to MAX_DRAIN_BYTES or DRAIN_TIMEOUT_MS, then close the connection
|
||||
* once its writes are out. */
|
||||
function drainIncoming(incoming: IncomingMessage, state: IncomingState): void {
|
||||
const socket = incoming.socket;
|
||||
if (socket.destroyed || state.draining) return;
|
||||
state.draining = true;
|
||||
let bytesRead = 0;
|
||||
let done = false;
|
||||
const cleanup = (): void => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
const forceClose = (): void => {
|
||||
if (done) return;
|
||||
cleanup();
|
||||
if (!socket.destroyed) socket.destroySoon();
|
||||
};
|
||||
const timer = setTimeout(forceClose, DRAIN_TIMEOUT_MS);
|
||||
timer.unref();
|
||||
incoming.on("data", (chunk: Buffer) => {
|
||||
if (done) return;
|
||||
bytesRead += chunk.length;
|
||||
if (bytesRead > MAX_DRAIN_BYTES) forceClose();
|
||||
});
|
||||
incoming.on("end", cleanup);
|
||||
incoming.on("error", cleanup);
|
||||
incoming.resume();
|
||||
}
|
||||
|
||||
/** The package's close handler cleanup: an unread body drains two timer
|
||||
* turns after the response closes. */
|
||||
function drainAfterClose(incoming: IncomingMessage, state: IncomingState): void {
|
||||
if (state.ended) return;
|
||||
setTimeout(() => {
|
||||
if (!state.ended)
|
||||
setTimeout(() => {
|
||||
drainIncoming(incoming, state);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** The package's outgoing header record: Headers order (lowercase,
|
||||
* sorted), Set-Cookie values gathered into one array entry at the end,
|
||||
* and the default content type for a response with a body. */
|
||||
function buildOutgoingHttpHeaders(
|
||||
headers: Headers,
|
||||
hasBody: boolean,
|
||||
): Record<string, string | string[]> {
|
||||
const res: Record<string, string | string[]> = {};
|
||||
if (headers.has("set-cookie")) {
|
||||
const cookies: string[] = [];
|
||||
headers.forEach((v, k) => {
|
||||
if (k === "set-cookie") cookies.push(v);
|
||||
else res[k] = v;
|
||||
});
|
||||
// Headers.forEach joins nothing but set-cookie, which getSetCookie
|
||||
// lists value by value, as the package's iteration does.
|
||||
if (cookies.length > 0) res["set-cookie"] = headers.getSetCookie();
|
||||
} else {
|
||||
headers.forEach((v, k) => {
|
||||
res[k] = v;
|
||||
});
|
||||
}
|
||||
if (hasBody && res["content-type"] === undefined) res["content-type"] = DEFAULT_CONTENT_TYPE;
|
||||
return res;
|
||||
}
|
||||
|
||||
/* Spelled out rather than ReadableStreamReadResult<Uint8Array>, whose
|
||||
* declaration depends on the program's lib and @types/node settings: the
|
||||
* shim compiles inside every program that imports the package. */
|
||||
type ReadResult = { done: boolean; value?: Uint8Array };
|
||||
|
||||
function readWithoutBlocking(read: Promise<ReadResult>): Promise<ReadResult | undefined> {
|
||||
return Promise.race([read, Promise.resolve().then(() => Promise.resolve(undefined))]);
|
||||
}
|
||||
|
||||
function nextTick(): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
/** Writes the rest of a body stream. As in the package, a response that
|
||||
* closes before the stream ends (the client disconnected) cancels the
|
||||
* stream, so its producer stops; the pending read then answers done. A
|
||||
* response that fails closes too, and its stream is cancelled the same way
|
||||
* (the package passes the error as the reason). The listener stays attached
|
||||
* and does nothing once the stream has ended. */
|
||||
async function pump(
|
||||
reader: ReadableStreamDefaultReader<Uint8Array>,
|
||||
outgoing: ServerResponse,
|
||||
pending: Promise<ReadResult> | undefined,
|
||||
): Promise<void> {
|
||||
let finished = false;
|
||||
outgoing.on("close", () => {
|
||||
if (!finished) reader.cancel().catch(() => {});
|
||||
});
|
||||
try {
|
||||
let read = pending ?? reader.read();
|
||||
for (;;) {
|
||||
const { done, value } = await read;
|
||||
if (done) {
|
||||
outgoing.end();
|
||||
return;
|
||||
}
|
||||
outgoing.write(value!);
|
||||
read = reader.read();
|
||||
}
|
||||
} finally {
|
||||
finished = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleResponseError(e: unknown, outgoing: ServerResponse): void {
|
||||
const err = e instanceof Error ? e : new Error("unknown error", { cause: e });
|
||||
console.error(e);
|
||||
if (!outgoing.headersSent) outgoing.writeHead(500, { "Content-Type": "text/plain" });
|
||||
outgoing.end(`Error: ${err.message}`);
|
||||
outgoing.destroy();
|
||||
}
|
||||
|
||||
function emptyResponse(status: number): Response {
|
||||
return new Response(null, { status });
|
||||
}
|
||||
|
||||
/** The lightweight state of a constructed Response (the runtime's
|
||||
* scr_fetch_response_light): [form, flags, byte length, ...record pairs]. */
|
||||
declare function scriptcResponseLight(res: Response): string[];
|
||||
|
||||
const UNDICI_TEXT_TYPE = "text/plain;charset=UTF-8";
|
||||
|
||||
/** Content-Length as Node's implicit header: set the headers one by one and
|
||||
* let end(body) compute the length, which Node then writes after its own
|
||||
* Date/Connection headers, as the package's preset length does. */
|
||||
function endWithImplicitLength(
|
||||
outgoing: ServerResponse,
|
||||
status: number,
|
||||
header: Record<string, string | string[]>,
|
||||
body: Uint8Array,
|
||||
): void {
|
||||
outgoing.statusCode = status;
|
||||
for (const name of Object.keys(header)) outgoing.setHeader(name, header[name]!);
|
||||
outgoing.end(body);
|
||||
}
|
||||
|
||||
/** The package's cached-response path: a Response that is still exactly
|
||||
* its constructor arguments (no body or metadata read since) is written
|
||||
* from them. Form 1 has no init headers, form 2 Headers, form 3 a plain
|
||||
* record kept verbatim. */
|
||||
async function writeLightResponse(
|
||||
res: Response,
|
||||
outgoing: ServerResponse,
|
||||
light: string[],
|
||||
head: boolean,
|
||||
): Promise<void> {
|
||||
const form = light[0]!;
|
||||
const flags = light[1]!;
|
||||
const length = Number(light[2]!);
|
||||
const status = res.status;
|
||||
const sized = length >= 0;
|
||||
if (form === "1") {
|
||||
if (length === -1) {
|
||||
outgoing.writeHead(status);
|
||||
outgoing.end();
|
||||
} else if (sized) {
|
||||
const body = await res.bytes();
|
||||
if (head) {
|
||||
outgoing.writeHead(status, {
|
||||
"Content-Type": DEFAULT_CONTENT_TYPE,
|
||||
"Content-Length": String(length),
|
||||
});
|
||||
outgoing.end(body);
|
||||
} else
|
||||
endWithImplicitLength(outgoing, status, { "Content-Type": DEFAULT_CONTENT_TYPE }, body);
|
||||
} else {
|
||||
outgoing.writeHead(status, { "Content-Type": DEFAULT_CONTENT_TYPE });
|
||||
outgoing.flushHeaders();
|
||||
await pump(res.body!.getReader(), outgoing, undefined);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let header: Record<string, string | string[]>;
|
||||
let hasContentLength = false;
|
||||
let plain = false;
|
||||
let canAutoLength = true;
|
||||
if (form === "2") {
|
||||
const headers = new Headers(res.headers);
|
||||
if (flags.includes("a") && headers.get("content-type") === UNDICI_TEXT_TYPE) {
|
||||
if (flags.includes("d")) headers.set("content-type", DEFAULT_CONTENT_TYPE);
|
||||
else headers.delete("content-type");
|
||||
} else if (flags.includes("d") && !headers.has("content-type")) {
|
||||
headers.set("content-type", DEFAULT_CONTENT_TYPE);
|
||||
}
|
||||
hasContentLength = headers.has("content-length");
|
||||
header = buildOutgoingHttpHeaders(headers, length !== -1);
|
||||
} else {
|
||||
plain = true;
|
||||
header = {};
|
||||
for (let i = 3; i + 1 < light.length; i += 2) {
|
||||
const name = light[i]!;
|
||||
const value = light[i + 1]!;
|
||||
const previous = header[name];
|
||||
if (previous === undefined) header[name] = value;
|
||||
else if (typeof previous === "string") header[name] = [previous, value];
|
||||
else previous.push(value);
|
||||
const lower = name.toLowerCase();
|
||||
if (lower === "content-length") hasContentLength = true;
|
||||
else if (lower === "transfer-encoding" || lower === "trailer") canAutoLength = false;
|
||||
}
|
||||
}
|
||||
const body = sized ? await res.bytes() : undefined;
|
||||
if (!hasContentLength && body !== undefined) {
|
||||
if (plain && canAutoLength && !head && status !== 204 && status !== 304) {
|
||||
endWithImplicitLength(outgoing, status, header, body);
|
||||
return;
|
||||
}
|
||||
header["Content-Length"] = String(length);
|
||||
}
|
||||
outgoing.writeHead(status, header);
|
||||
if (length === -1) outgoing.end();
|
||||
else if (body !== undefined) outgoing.end(body);
|
||||
else {
|
||||
outgoing.flushHeaders();
|
||||
await pump(res.body!.getReader(), outgoing, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
async function writeResponse(
|
||||
res: Response,
|
||||
outgoing: ServerResponse,
|
||||
head: boolean,
|
||||
): Promise<void> {
|
||||
const light = scriptcResponseLight(res);
|
||||
if (light[0] !== "0") return writeLightResponse(res, outgoing, light, head);
|
||||
const body = res.body;
|
||||
const record = buildOutgoingHttpHeaders(res.headers, body !== null);
|
||||
if (body !== null) {
|
||||
const reader: ReadableStreamDefaultReader<Uint8Array> = body.getReader();
|
||||
const values: Uint8Array[] = [];
|
||||
let done = false;
|
||||
let pending: Promise<ReadResult> | undefined = undefined;
|
||||
if (record["transfer-encoding"] !== "chunked") {
|
||||
let maxReadCount = 2;
|
||||
for (let i = 0; i < maxReadCount; i++) {
|
||||
pending ??= reader.read();
|
||||
let chunk: ReadResult | undefined = undefined;
|
||||
try {
|
||||
chunk = await readWithoutBlocking(pending);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
done = true;
|
||||
}
|
||||
if (chunk === undefined) {
|
||||
if (i === 1 && !done) {
|
||||
await nextTick();
|
||||
maxReadCount = 3;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
pending = undefined;
|
||||
if (chunk.value !== undefined) values.push(chunk.value);
|
||||
if (chunk.done) {
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (done && record["content-length"] === undefined) {
|
||||
let length = 0;
|
||||
for (const value of values) length += value.length;
|
||||
record["content-length"] = String(length);
|
||||
}
|
||||
}
|
||||
outgoing.writeHead(res.status, record);
|
||||
for (const value of values) outgoing.write(value);
|
||||
if (done) outgoing.end();
|
||||
else {
|
||||
if (values.length === 0) outgoing.flushHeaders();
|
||||
await pump(reader, outgoing, pending);
|
||||
}
|
||||
} else if (record[X_ALREADY_SENT] === undefined) {
|
||||
outgoing.writeHead(res.status, record);
|
||||
outgoing.end();
|
||||
}
|
||||
}
|
||||
|
||||
async function respond(
|
||||
pending: Response | Promise<Response>,
|
||||
outgoing: ServerResponse,
|
||||
errorHandler: CustomErrorHandler | undefined,
|
||||
head: boolean,
|
||||
): Promise<void> {
|
||||
let res: Response;
|
||||
if (pending instanceof Promise) {
|
||||
try {
|
||||
res = await pending;
|
||||
} catch (err) {
|
||||
if (errorHandler === undefined) {
|
||||
res = handleFetchError(err);
|
||||
} else {
|
||||
const errRes = await errorHandler(err);
|
||||
if (errRes === undefined) return;
|
||||
res = errRes;
|
||||
}
|
||||
}
|
||||
} else res = pending;
|
||||
await writeResponse(res, outgoing, head);
|
||||
}
|
||||
|
||||
/** A Response the package writes from its cache without waiting: still
|
||||
* exactly its constructor arguments, with no body or a string or bytes
|
||||
* body. */
|
||||
function immediateCacheable(res: Response): boolean {
|
||||
const light = scriptcResponseLight(res);
|
||||
return light[0] !== "0" && Number(light[2]!) >= -1;
|
||||
}
|
||||
|
||||
function handleFetchError(e: unknown): Response {
|
||||
return emptyResponse(e instanceof Error && e.name === "TimeoutError" ? 504 : 500);
|
||||
}
|
||||
|
||||
async function handleRequest(
|
||||
fetchCallback: FetchCallback,
|
||||
options: RequestListenerOptions,
|
||||
incoming: IncomingMessage,
|
||||
outgoing: ServerResponse,
|
||||
): Promise<void> {
|
||||
let req: Request | undefined = undefined;
|
||||
let res: Response | Promise<Response> | undefined = undefined;
|
||||
const state: IncomingState = { ended: false, draining: false };
|
||||
let needsBodyCleanup = false;
|
||||
let closeHandlerAttached = false;
|
||||
const ensureCloseHandler = (): void => {
|
||||
if (req === undefined || closeHandlerAttached) return;
|
||||
closeHandlerAttached = true;
|
||||
if (needsBodyCleanup)
|
||||
outgoing.on("close", () => {
|
||||
drainAfterClose(incoming, state);
|
||||
});
|
||||
};
|
||||
try {
|
||||
req = newRequest(incoming, options.hostname, state);
|
||||
needsBodyCleanup =
|
||||
options.autoCleanupIncoming !== false &&
|
||||
!(incoming.method === "GET" || incoming.method === "HEAD");
|
||||
if (needsBodyCleanup)
|
||||
incoming.on("end", () => {
|
||||
state.ended = true;
|
||||
});
|
||||
res = fetchCallback(req, { incoming, outgoing });
|
||||
if (!(res instanceof Promise) && immediateCacheable(res)) {
|
||||
if (needsBodyCleanup)
|
||||
outgoing.once("finish", () => {
|
||||
if (!state.ended) drainIncoming(incoming, state);
|
||||
});
|
||||
} else ensureCloseHandler();
|
||||
} catch (e) {
|
||||
if (res !== undefined) return handleResponseError(e, outgoing);
|
||||
const errorHandler = options.errorHandler;
|
||||
if (errorHandler !== undefined) {
|
||||
ensureCloseHandler();
|
||||
const handled = await errorHandler(
|
||||
req !== undefined ? e : e instanceof RequestError ? e : toRequestError(e),
|
||||
);
|
||||
if (handled === undefined) return;
|
||||
res = handled;
|
||||
} else if (req === undefined) res = emptyResponse(400);
|
||||
else res = handleFetchError(e);
|
||||
}
|
||||
try {
|
||||
await respond(res, outgoing, options.errorHandler, incoming.method === "HEAD");
|
||||
} catch (e) {
|
||||
handleResponseError(e, outgoing);
|
||||
}
|
||||
}
|
||||
|
||||
function toRequestError(e: unknown): RequestError {
|
||||
return new RequestError(e instanceof Error ? e.message : String(e), { cause: e });
|
||||
}
|
||||
|
||||
export function getRequestListener(
|
||||
fetchCallback: FetchCallback,
|
||||
options: RequestListenerOptions = {},
|
||||
): (incoming: IncomingMessage, outgoing: ServerResponse) => Promise<void> {
|
||||
return (incoming, outgoing) => handleRequest(fetchCallback, options, incoming, outgoing);
|
||||
}
|
||||
|
||||
export function createAdaptorServer(options: Options): ServerType {
|
||||
const listener = getRequestListener(options.fetch, {
|
||||
hostname: options.hostname,
|
||||
overrideGlobalObjects: options.overrideGlobalObjects,
|
||||
autoCleanupIncoming: options.autoCleanupIncoming,
|
||||
});
|
||||
return createServer((incoming, outgoing) => {
|
||||
void listener(incoming, outgoing);
|
||||
});
|
||||
}
|
||||
|
||||
export function serve(
|
||||
options: Options,
|
||||
listeningListener?: (info: AddressInfo) => void,
|
||||
): ServerType {
|
||||
const server = createAdaptorServer(options);
|
||||
server.listen(options.port ?? 3000, options.hostname, () => {
|
||||
const info = server.address() as AddressInfo;
|
||||
if (listeningListener !== undefined) listeningListener(info);
|
||||
});
|
||||
return server;
|
||||
}
|
||||
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}`);
|
||||
|
||||
@@ -28,6 +28,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
...(features.dynTier ? { dynTier: true } : {}),
|
||||
...(features.streamDyn ? { streamDyn: true } : {}),
|
||||
...(features.handleProps ? { handleProps: true } : {}),
|
||||
...(features.declAdopt ? { declAdopt: true } : {}),
|
||||
...(features.dynForAwait ? { dynForAwait: true } : {}),
|
||||
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
|
||||
...(features.dynBuffer ? { dynBuffer: true } : {}),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { DynamicFieldStores } from "../ir/int32-slots.js";
|
||||
import type { IrExpr, IrModule, IrStmt } from "../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../ir/traverse.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../ir/node-index.js";
|
||||
import {
|
||||
computeCycleMutability,
|
||||
constructorEscapePoint,
|
||||
@@ -48,6 +49,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 +65,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);
|
||||
}
|
||||
|
||||
@@ -95,7 +98,10 @@ export interface ConstructionFacts {
|
||||
privateStores: Set<IrStmt>;
|
||||
}
|
||||
|
||||
export function computeConstructionFacts(mod: IrModule): ConstructionFacts {
|
||||
/** The nodes that can reach a constructor with a reused receiver. */
|
||||
const CONSTRUCTOR_USE_KINDS: readonly IrNodeKind[] = ["call", "closure"];
|
||||
|
||||
export function computeConstructionFacts(mod: IrModule, nodes?: IrNodeIndex): ConstructionFacts {
|
||||
const facts: ConstructionFacts = { privateStores: new Set() };
|
||||
if (mod.lib !== undefined) return facts;
|
||||
const classes = new Map((mod.classes ?? []).map((c) => [c.name, c] as const));
|
||||
@@ -120,6 +126,7 @@ export function computeConstructionFacts(mod: IrModule): ConstructionFacts {
|
||||
c = classes.get(c)!.base;
|
||||
}
|
||||
};
|
||||
const scan = nodes?.scan(CONSTRUCTOR_USE_KINDS);
|
||||
for (const fn of mod.functions) {
|
||||
const own = ctorClass.get(fn.name);
|
||||
const thisId = own !== undefined ? thisOf(own) : null;
|
||||
@@ -127,22 +134,19 @@ export function computeConstructionFacts(mod: IrModule): ConstructionFacts {
|
||||
own !== undefined && classes.get(own)!.base !== undefined
|
||||
? `%${classes.get(own)!.base}.constructor`
|
||||
: null;
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "call") {
|
||||
const target = ctorClass.get(e.callee);
|
||||
eachNodeOfKinds(fn, scan, (node) => {
|
||||
if (node.kind === "call") {
|
||||
const target = ctorClass.get(node.callee);
|
||||
if (
|
||||
target !== undefined &&
|
||||
!(e.callee === baseCtor && e.args.length > 0 && isThisRef(e.args[0]!, thisId))
|
||||
!(node.callee === baseCtor && node.args.length > 0 && isThisRef(node.args[0]!, thisId))
|
||||
)
|
||||
markReused(target);
|
||||
} else if (e.kind === "closure") {
|
||||
const target = ctorClass.get(e.fnName);
|
||||
} else if (node.kind === "closure") {
|
||||
const target = ctorClass.get(node.fnName);
|
||||
if (target !== undefined) markReused(target);
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => everyStmtChild(s, expr, stmt);
|
||||
fn.body.every(stmt);
|
||||
});
|
||||
}
|
||||
|
||||
// The first escaping statement of each constructor (body length: none).
|
||||
@@ -190,29 +194,32 @@ export function computeConstructionFacts(mod: IrModule): ConstructionFacts {
|
||||
return facts;
|
||||
}
|
||||
|
||||
/** The nodes that write a binding: statement and expression assignments. */
|
||||
const ASSIGNMENT_KINDS: readonly IrNodeKind[] = ["assign", "assignExpr"];
|
||||
|
||||
/** Module constants whose value never changes once initialized: immutable,
|
||||
* not TDZ-checked (every read follows the initialization), no import-cycle
|
||||
* init flag, and at most one assignment in the whole module. Such a global
|
||||
* owns its value for the rest of the program, so a read of it can be
|
||||
* borrowed across any later operand or call. */
|
||||
export function stableGlobals(mod: IrModule): Set<string> {
|
||||
export function stableGlobals(mod: IrModule, nodes?: IrNodeIndex): Set<string> {
|
||||
const candidates = new Set(
|
||||
(mod.globals ?? [])
|
||||
.filter((g) => !g.mutable && g.tdz !== true && g.initFlag === undefined)
|
||||
.map((g) => g.id),
|
||||
);
|
||||
const assigned = new Set<string>();
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "assignExpr" && candidates.has(e.localId)) candidates.delete(e.localId);
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "assign" && candidates.has(s.localId)) {
|
||||
if (assigned.has(s.localId)) candidates.delete(s.localId);
|
||||
assigned.add(s.localId);
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
for (const fn of mod.functions) fn.body.every(stmt);
|
||||
const scan = nodes?.scan(ASSIGNMENT_KINDS);
|
||||
for (const fn of mod.functions) {
|
||||
if (candidates.size === 0) break;
|
||||
eachNodeOfKinds(fn, scan, (node) => {
|
||||
if (node.kind === "assignExpr" && candidates.has(node.localId))
|
||||
candidates.delete(node.localId);
|
||||
else if (node.kind === "assign" && candidates.has(node.localId)) {
|
||||
if (assigned.has(node.localId)) candidates.delete(node.localId);
|
||||
assigned.add(node.localId);
|
||||
}
|
||||
});
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
@@ -57,10 +57,11 @@ test("only empty literals stored where the module appends construct small", () =
|
||||
expect(found.has(idle)).toBe(false);
|
||||
expect(found.has(returned)).toBe(false);
|
||||
|
||||
// Only `fill` builds arrays.
|
||||
// Only `fill` builds arrays. They may hold int32 slots (element kind 7),
|
||||
// which does not change the choice of constructor.
|
||||
const llvm = emitLlvmModule(module(body, ["xs", "ys"]));
|
||||
expect(llvm.match(/call ptr @scr_arr_new_small\(i32 0\)/g)).toHaveLength(2);
|
||||
expect(llvm.match(/call ptr @scr_arr_new\(i32 0, i64 0\)/g)).toHaveLength(2);
|
||||
expect(llvm.match(/call ptr @scr_arr_new_small\(i32 [07]\)/g)).toHaveLength(2);
|
||||
expect(llvm.match(/call ptr @scr_arr_new\(i32 [07], i64 0\)/g)).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("a binding with the same id in another function does not count", () => {
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
* one directly or through a binding read from it. The choice never changes
|
||||
* behavior. */
|
||||
|
||||
import type { IrExpr, IrModule, IrStmt } from "../../ir/ir.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import type { IrExpr, IrModule } from "../../ir/ir.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
|
||||
const APPENDING_METHODS: ReadonlySet<string> = new Set([
|
||||
"push",
|
||||
@@ -30,7 +30,27 @@ function isEmptyLiteral(e: IrExpr): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function findAppendedArrayLiterals(module: IrModule): ReadonlySet<IrExpr> {
|
||||
/** The nodes that name an appended binding or field, and the stores that
|
||||
* initialize one. */
|
||||
const TARGET_KINDS: readonly IrNodeKind[] = [
|
||||
"arrIntrinsic",
|
||||
"assignExpr",
|
||||
"arraySet",
|
||||
"varDecl",
|
||||
"assign",
|
||||
];
|
||||
const STORE_KINDS: readonly IrNodeKind[] = [
|
||||
"assignExpr",
|
||||
"varDecl",
|
||||
"assign",
|
||||
"fieldSet",
|
||||
"recordSet",
|
||||
];
|
||||
|
||||
export function findAppendedArrayLiterals(
|
||||
module: IrModule,
|
||||
index?: IrNodeIndex,
|
||||
): ReadonlySet<IrExpr> {
|
||||
// Bindings are function-scoped by id; module globals are shared.
|
||||
const localsOf = new Map<string, ReadonlySet<string>>();
|
||||
for (const fn of module.functions) {
|
||||
@@ -65,46 +85,37 @@ export function findAppendedArrayLiterals(module: IrModule): ReadonlySet<IrExpr>
|
||||
const field = fieldOf(target);
|
||||
if (field !== null) appendedFields.add(field);
|
||||
};
|
||||
const targets = index?.scan(TARGET_KINDS);
|
||||
for (const fn of module.functions) {
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "arrIntrinsic" && APPENDING_METHODS.has(e.method))
|
||||
noteTarget(fn.name, e.receiver);
|
||||
else if (e.kind === "assignExpr") noteAlias(fn.name, e.localId, e.value);
|
||||
return true;
|
||||
},
|
||||
stmt: (s) => {
|
||||
if (s.kind === "arraySet") noteTarget(fn.name, s.arr);
|
||||
else if (s.kind === "varDecl") noteAlias(fn.name, s.localId, s.init);
|
||||
else if (s.kind === "assign") noteAlias(fn.name, s.localId, s.value);
|
||||
return true;
|
||||
},
|
||||
eachNodeOfKinds(fn, targets, (node) => {
|
||||
if (node.kind === "arrIntrinsic" && APPENDING_METHODS.has(node.method))
|
||||
noteTarget(fn.name, node.receiver);
|
||||
else if (node.kind === "assignExpr") noteAlias(fn.name, node.localId, node.value);
|
||||
else if (node.kind === "arraySet") noteTarget(fn.name, node.arr);
|
||||
else if (node.kind === "varDecl") noteAlias(fn.name, node.localId, node.init);
|
||||
else if (node.kind === "assign") noteAlias(fn.name, node.localId, node.value);
|
||||
});
|
||||
}
|
||||
for (const binding of appendedBindings)
|
||||
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);
|
||||
};
|
||||
const stores = index?.scan(STORE_KINDS);
|
||||
for (const fn of module.functions) {
|
||||
const binding = (localId: string): boolean =>
|
||||
appendedBindings.has(bindingKey(fn.name, localId));
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "assignExpr") consider(e.value, binding(e.localId));
|
||||
return true;
|
||||
},
|
||||
stmt: (s: IrStmt) => {
|
||||
if (s.kind === "varDecl") consider(s.init, binding(s.localId));
|
||||
else if (s.kind === "assign") consider(s.value, binding(s.localId));
|
||||
else if (s.kind === "fieldSet")
|
||||
consider(s.value, appendedFields.has(fieldKey(s.className, s.field)));
|
||||
else if (s.kind === "recordSet")
|
||||
consider(s.value, appendedFields.has(fieldKey(s.shapeId, s.field)));
|
||||
return true;
|
||||
},
|
||||
eachNodeOfKinds(fn, stores, (node) => {
|
||||
if (node.kind === "assignExpr") consider(node.value, binding(node.localId));
|
||||
else if (node.kind === "varDecl") consider(node.init, binding(node.localId));
|
||||
else if (node.kind === "assign") consider(node.value, binding(node.localId));
|
||||
else if (node.kind === "fieldSet")
|
||||
consider(node.value, appendedFields.has(fieldKey(node.className, node.field)));
|
||||
else if (node.kind === "recordSet")
|
||||
consider(node.value, appendedFields.has(fieldKey(node.shapeId, node.field)));
|
||||
});
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -99,6 +99,7 @@ function match(
|
||||
f: IrFunction,
|
||||
loop: IrStmt & { kind: "for" },
|
||||
captured: string[] = [],
|
||||
scalarStrings: string[] = [],
|
||||
): BytesLoopVersion | null {
|
||||
const ranges = analyzeIntegerRanges(f);
|
||||
const locals = new Map(f.locals.map((l) => [l.id, l]));
|
||||
@@ -111,6 +112,7 @@ function match(
|
||||
proven: new Set(),
|
||||
workers: false,
|
||||
owned: new Set(),
|
||||
scalarStrings: new Set(scalarStrings),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -388,6 +390,7 @@ test("while loop counters below a bound version their accesses", () => {
|
||||
ranges,
|
||||
initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(),
|
||||
proven: new Set(),
|
||||
scalarStrings: new Set(),
|
||||
workers: false,
|
||||
owned: new Set(),
|
||||
});
|
||||
@@ -487,3 +490,131 @@ test("bodies compiled without optimization keep one checked loop", () => {
|
||||
expect(body).not.toContain("bytes.version");
|
||||
expect(body).toContain("bytes.index.invalid");
|
||||
});
|
||||
|
||||
const STR: IrType = { kind: "string" };
|
||||
const charCode = (receiver: IrExpr, index: IrExpr): IrExpr => ({
|
||||
kind: "strIntrinsic",
|
||||
method: "charCodeAt",
|
||||
receiver,
|
||||
args: [index],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
|
||||
/** The kernel with a string parameter `text`. */
|
||||
function textKernel(body: IrStmt[], mutable = false): IrFunction {
|
||||
const kernel = fn(body, [local("text", STR, mutable)]);
|
||||
kernel.params.push({ localId: "text", name: "text", type: STR });
|
||||
return kernel;
|
||||
}
|
||||
|
||||
test("charCodeAt reads of a string binding version with the typed-array reads", () => {
|
||||
const read = charCode(ref("text", STR), bin("+", ref("i"), num(1)));
|
||||
const loop = counted([set(ref("out", f64s), ref("i"), read)]);
|
||||
const version = match(textKernel([loop]), loop)!;
|
||||
expect(version.bounds.has(read)).toBe(true);
|
||||
expect(
|
||||
version.receivers.map((r) => [r.ref.kind === "varRef" && r.ref.localId, r.string]),
|
||||
).toEqual([
|
||||
["out", false],
|
||||
["text", true],
|
||||
]);
|
||||
expect(version.affine.map((c) => [c.receiver, c.minOffset, c.maxEnd])).toEqual([
|
||||
[0, 0, 0],
|
||||
[1, 1, 1],
|
||||
]);
|
||||
// A string the loop rebinds keeps its checked reads.
|
||||
const rebinding = counted([
|
||||
set(ref("out", f64s), ref("i"), charCode(ref("text", STR), ref("i"))),
|
||||
{ kind: "assign", localId: "text", value: ref("text", STR), loc },
|
||||
]);
|
||||
const rebound = match(textKernel([rebinding], true), rebinding)!;
|
||||
expect(rebound.receivers.map((r) => r.string)).toEqual([false]);
|
||||
// A string held as a slice of another string has no header to test.
|
||||
const sliced = match(textKernel([loop]), loop, [], ["text"])!;
|
||||
expect(sliced.receivers.map((r) => r.string)).toEqual([false]);
|
||||
expect(sliced.bounds.has(read)).toBe(false);
|
||||
});
|
||||
|
||||
test("charAt reads of a string binding version as one-unit reads", () => {
|
||||
const read: IrExpr = {
|
||||
kind: "strIntrinsic",
|
||||
method: "charAt",
|
||||
receiver: ref("text", STR),
|
||||
args: [bin("-", ref("i"), num(1))],
|
||||
type: STR,
|
||||
loc,
|
||||
};
|
||||
const loop = counted([{ kind: "exprStmt", expr: read, loc }]);
|
||||
const version = match(textKernel([loop]), loop)!;
|
||||
expect(version.bounds.has(read)).toBe(true);
|
||||
expect(version.receivers.map((r) => r.string)).toEqual([true]);
|
||||
expect(version.affine.map((c) => [c.receiver, c.minOffset, c.maxEnd])).toEqual([[0, -1, -1]]);
|
||||
});
|
||||
|
||||
test("the guard tests a string's ASCII bit and the fast copy reads bytes", () => {
|
||||
const loop = counted([set(ref("out", f64s), ref("i"), charCode(ref("text", STR), ref("i")))]);
|
||||
const kernel = textKernel([loop]);
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "__main",
|
||||
functions: [
|
||||
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
|
||||
kernel,
|
||||
],
|
||||
};
|
||||
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
|
||||
const guard = body.slice(0, body.search(/^bytes\.version\.slow\d*:/m));
|
||||
expect(guard).toMatch(/load i32, ptr %\S+\n\s+%\S+ = icmp slt i32 %\S+, 0/);
|
||||
const fast = body.slice(body.lastIndexOf("bytes.version.fast"));
|
||||
expect(fast).toMatch(/load i8, ptr %\S+\n\s+%\S+ = uitofp i8/);
|
||||
expect(fast).not.toContain("@sc_str_char_code_at_int");
|
||||
// The checked copy keeps the inline helper and its fallback.
|
||||
expect(body).toContain("@sc_str_char_code_at_int");
|
||||
});
|
||||
|
||||
test("while loop counters below a string's length version its reads", () => {
|
||||
// while (i < text.length && text.charCodeAt(i) === 32) i = i + 1
|
||||
const text = ref("text", STR);
|
||||
const read = charCode(text, ref("i"));
|
||||
const length: IrExpr = {
|
||||
kind: "strIntrinsic",
|
||||
method: "length",
|
||||
receiver: text,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const loop: IrStmt & { kind: "while" } = {
|
||||
kind: "while",
|
||||
cond: {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: { kind: "bin", op: "<", left: ref("i"), right: length, type: BOOL, loc },
|
||||
right: { kind: "bin", op: "===", left: read, right: num(32), type: BOOL, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
body: [{ kind: "assign", localId: "i", value: bin("+", ref("i"), num(1)), loc }],
|
||||
loc,
|
||||
};
|
||||
const f = textKernel([{ kind: "varDecl", localId: "i", init: ref("k"), loc }, loop]);
|
||||
const version = matchBytesLoopVersion(loop, null, {
|
||||
locals: new Map(f.locals.map((l) => [l.id, l])),
|
||||
invariantGlobals: new Set(),
|
||||
captured: new Set(),
|
||||
ranges: analyzeIntegerRanges(f),
|
||||
initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(),
|
||||
proven: new Set(),
|
||||
scalarStrings: new Set(),
|
||||
workers: false,
|
||||
owned: new Set(),
|
||||
})!;
|
||||
// The read is bounded by the counter's test against the length, not by
|
||||
// a static range the guard could not meet.
|
||||
expect(version.bounds.has(read)).toBe(true);
|
||||
expect(version.receivers.map((r) => r.string)).toEqual([true]);
|
||||
expect(version.bounded.map((c) => c.slack)).toEqual([0]);
|
||||
expect(version.fixed).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js";
|
||||
import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { emitStringAsciiFlag, emitStringByteLength } from "./string-reads.js";
|
||||
|
||||
type Node = IrExpr | IrStmt;
|
||||
type VarRef = IrExpr & { kind: "varRef" };
|
||||
@@ -63,8 +64,9 @@ export interface BytesLoopVersion {
|
||||
/** Worker programs: typed-array accesses whose views the guard proves
|
||||
* thread-owned (no shared storage). */
|
||||
owned: ReadonlySet<Node>;
|
||||
/** Root receiver bindings, read once before the loop. */
|
||||
receivers: { ref: VarRef; global: boolean }[];
|
||||
/** Root receiver bindings, read once before the loop; `string` marks a
|
||||
* string whose `charCodeAt` and `charAt` reads the version covers. */
|
||||
receivers: { ref: VarRef; global: boolean; string: boolean }[];
|
||||
/** Receivers the guard requires to have no shared storage. */
|
||||
ownedReceivers: number[];
|
||||
fixed: FixedCheck[];
|
||||
@@ -101,6 +103,9 @@ export interface BytesLoopFacts {
|
||||
workers: boolean;
|
||||
/** Accesses already proved thread-owned by an enclosing version. */
|
||||
owned: ReadonlySet<Node>;
|
||||
/** String bindings held as a slice of another string (string-slices.ts):
|
||||
* they have no string header for the guard to read. */
|
||||
scalarStrings: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
const MAX_COST = 400;
|
||||
@@ -127,6 +132,14 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"])
|
||||
* such as `base` for `const base = r * cols`, must also be an exact
|
||||
* integer between -2^32 and 2^32, which the guard tests.
|
||||
*
|
||||
* `s.charCodeAt(index)` and `s.charAt(index)` (so `s[index]`) of a string
|
||||
* binding qualify the same way. A string is immutable: its bytes, its
|
||||
* length and the capacity-word bit that proves it ASCII (string-reads.ts)
|
||||
* are fixed when it is built. The guard also requires that bit, so each
|
||||
* UTF-16 unit is one byte and the length is the byte length. The fast copy
|
||||
* reads each unit with one byte load, and each character as the immortal
|
||||
* one-character string for its byte.
|
||||
*
|
||||
* In worker programs, every access of a view that may be shared tests its
|
||||
* shared storage. A view's storage is also fixed at construction, so the
|
||||
* same guard tests each invariant receiver once, and the fast copy reads
|
||||
@@ -217,7 +230,8 @@ export function matchBytesLoopVersion(
|
||||
};
|
||||
const resolving = new Set<string>();
|
||||
const root = (e: IrExpr): VarRef | null => {
|
||||
if (e.kind !== "varRef" || e.type.kind !== "bytes") return null;
|
||||
if (e.kind !== "varRef" || (e.type.kind !== "bytes" && e.type.kind !== "string")) return null;
|
||||
if (facts.scalarStrings.has(e.localId)) return null;
|
||||
const init = temp(e.localId);
|
||||
if (init) {
|
||||
if (resolving.has(e.localId)) return null;
|
||||
@@ -396,7 +410,7 @@ export function matchBytesLoopVersion(
|
||||
},
|
||||
});
|
||||
}
|
||||
const receivers: { ref: VarRef; global: boolean }[] = [];
|
||||
const receivers: { ref: VarRef; global: boolean; string: boolean }[] = [];
|
||||
const receiverIndex = new Map<string, number>();
|
||||
const fixed = new Map<number, { check: FixedCheck; nodes: Node[] }>();
|
||||
const affineChecks = new Map<
|
||||
@@ -408,7 +422,11 @@ export function matchBytesLoopVersion(
|
||||
if (index === undefined) {
|
||||
index = receivers.length;
|
||||
receiverIndex.set(ref.localId, index);
|
||||
receivers.push({ ref, global: !facts.locals.has(ref.localId) });
|
||||
receivers.push({
|
||||
ref,
|
||||
global: !facts.locals.has(ref.localId),
|
||||
string: ref.type.kind === "string",
|
||||
});
|
||||
}
|
||||
return index;
|
||||
};
|
||||
@@ -423,7 +441,7 @@ export function matchBytesLoopVersion(
|
||||
const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => {
|
||||
const ref = root(array);
|
||||
if (!ref) return;
|
||||
if (facts.workers && !facts.owned.has(node)) {
|
||||
if (facts.workers && ref.type.kind === "bytes" && !facts.owned.has(node)) {
|
||||
const receiver = receiverOf(ref);
|
||||
const nodes = owned.get(receiver);
|
||||
if (nodes) nodes.push(node);
|
||||
@@ -491,7 +509,12 @@ export function matchBytesLoopVersion(
|
||||
const numeric = byteNumberAccess(e);
|
||||
if (numeric) access(e, e.receiver, e.args[numeric.offsetArg]!, numeric.width);
|
||||
else if (e.method === "get" && e.args[0]) access(e, e.receiver, e.args[0], 1);
|
||||
}
|
||||
} else if (
|
||||
e.kind === "strIntrinsic" &&
|
||||
(e.method === "charCodeAt" || e.method === "charAt") &&
|
||||
e.args[0]
|
||||
)
|
||||
access(e, e.receiver, e.args[0], 1);
|
||||
return everyExprChild(e, visitExpr, visitStmt);
|
||||
};
|
||||
const visitStmt = (s: IrStmt): boolean => {
|
||||
@@ -694,17 +717,18 @@ function counterStep(s: IrStmt): { id: string; k: number; wraps: boolean } | nul
|
||||
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 `<=`). */
|
||||
/** Typed-array and string accesses of a while loop that happen at a
|
||||
* counter below a loop-invariant bound. A counter is a number local that
|
||||
* the loop changes only by positive integer increments, written as
|
||||
* statements of the body, of its blocks and branches, or of nested while
|
||||
* loops. The condition of the loop, an `if` or a `&&` chain that compares
|
||||
* the counter below a bound (`while (pos < end && bytes[pos] === 32)
|
||||
* pos++`, or `s.charCodeAt(pos)` of a string) makes it below that bound
|
||||
* until the next increment on the path. The counter starts at its entry
|
||||
* value and only grows (a `| 0` increment never wraps when every bound is
|
||||
* at most 2^31 minus the increments), so an access at `counter + d` is in
|
||||
* bounds for the whole loop when the entry value is at least `-d` and
|
||||
* `bound + d < length` (`+ 1` for `<=`). */
|
||||
function findWhileCounterAccesses(
|
||||
loop: IrStmt & { kind: "while" },
|
||||
host: {
|
||||
@@ -814,6 +838,12 @@ function findWhileCounterAccesses(
|
||||
: undefined;
|
||||
if (index) record(e, index, clean);
|
||||
}
|
||||
if (
|
||||
e.kind === "strIntrinsic" &&
|
||||
(e.method === "charCodeAt" || e.method === "charAt") &&
|
||||
e.args[0]
|
||||
)
|
||||
record(e, e.args[0], clean);
|
||||
if (e.kind === "logical" && e.op === "&&") {
|
||||
expr(e.left, clean);
|
||||
expr(e.right, withFacts(clean, e.left));
|
||||
@@ -969,14 +999,20 @@ export function emitBytesLoopVersionGuard(
|
||||
}
|
||||
return value;
|
||||
});
|
||||
const lengths = receivers.map((receiver) => {
|
||||
const len = emitBytesHeaderLength(host, receiver);
|
||||
const lengths = receivers.map((receiver, index) => {
|
||||
const len = version.receivers[index]!.string
|
||||
? emitStringByteLength(host, receiver)
|
||||
: emitBytesHeaderLength(host, receiver);
|
||||
if (host.sizeType === "i64") return len;
|
||||
const wide = B.tmp();
|
||||
B.line(`${wide} = zext ${host.sizeType} ${len} to i64`);
|
||||
return wide;
|
||||
});
|
||||
const conditions: string[] = [];
|
||||
// A string's byte length is its UTF-16 length when it is ASCII.
|
||||
version.receivers.forEach(({ string }, index) => {
|
||||
if (string) conditions.push(emitStringAsciiFlag(host, receivers[index]!));
|
||||
});
|
||||
for (const index of version.ownedReceivers) {
|
||||
const owned = B.tmp();
|
||||
B.line(`${owned} = icmp eq ptr ${emitBytesSharedStorage(host, receivers[index]!)}, null`);
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
export function preservesDynTest(test: string): boolean {
|
||||
switch (test) {
|
||||
case "truthy":
|
||||
case "classCapsule":
|
||||
case "funcKind":
|
||||
case "nullish":
|
||||
case "promise":
|
||||
case "buffer":
|
||||
|
||||
@@ -168,6 +168,43 @@ test("ProgramLifetime hierarchies are never freed and take no reference counts",
|
||||
expect(body(mangleClassRelease("Owner"))).toContain("load i32, ptr %bufp");
|
||||
});
|
||||
|
||||
test("arrays of ProgramLifetime instances tell the runtime their teardown releases nothing", () => {
|
||||
const kids = { kind: "array" as const, elem: { kind: "object" as const, className: "Node" } };
|
||||
const owners = { kind: "array" as const, elem: { kind: "object" as const, className: "Owner" } };
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
functions: [
|
||||
{
|
||||
name: "main",
|
||||
params: [],
|
||||
returnType: VOID,
|
||||
locals: [],
|
||||
body: [
|
||||
{ kind: "exprStmt", expr: { kind: "arrayLit", elems: [], type: kids, loc }, loc },
|
||||
{ kind: "exprStmt", expr: { kind: "arrayLit", elems: [], type: owners, loc }, loc },
|
||||
],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
classes: [
|
||||
{ name: "Node", fields: [], methods: [], loc },
|
||||
{ name: "Owner", fields: [{ name: "nodes", type: kids }], methods: [], loc },
|
||||
],
|
||||
contracts: { programLifetime: ["Node"] },
|
||||
};
|
||||
const llvm = emitLlvmModule(mod, { pointerBits: 64 });
|
||||
const ops = (cls: string): string =>
|
||||
new RegExp(`^@sc_arrops\\.[^\\n]*${mangleClassRelease(cls)}[^\\n]*$`, "m").exec(llvm)?.[0] ??
|
||||
"";
|
||||
// SCR_ARR_OPS_RELEASE_NOTHING for Node; Owner's elements are released.
|
||||
expect(ops("Node")).toMatch(
|
||||
/= internal global \{ ptr, ptr, ptr, i32, i32 \} .*, i32 0, i32 2 \}$/,
|
||||
);
|
||||
expect(ops("Owner")).toMatch(/= internal global %ScrArrOps .*, i32 0 \}$/);
|
||||
});
|
||||
|
||||
test("classes whose trace visits nothing start acyclic in a traced hierarchy", () => {
|
||||
const mapper = { kind: "object" as const, className: "Mapper" };
|
||||
const mod: IrModule = {
|
||||
|
||||
@@ -10,9 +10,10 @@ import type {
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
|
||||
/** Field numbers of %ScrArr, which mirrors scr_runtime.h's ScrArr field for
|
||||
* field (its static assertions pin `len`, `cap`, `cyc`, `filled` and
|
||||
* `data`). `len` and `cap` are i32 on every target; the dense states end
|
||||
* the allocation after the `cap` value slots at `data` (scrArrStates). */
|
||||
* field (its static assertions pin `len`, `cap`, `cyc`, `spare`, `filled`
|
||||
* and `data`). `len` and `cap` are i32 on every target; the dense states of
|
||||
* an array with states (SCR_ARR_STATES in `spare`) end the allocation after
|
||||
* the `cap` value slots at `data` (scrArrStates). */
|
||||
export const SCR_ARR_FIELD = {
|
||||
rc: 0,
|
||||
len: 1,
|
||||
@@ -24,6 +25,10 @@ export const SCR_ARR_FIELD = {
|
||||
data: 7,
|
||||
} as const;
|
||||
|
||||
/** The `spare` flag of a %ScrArr whose dense slots carry state bytes
|
||||
* (SCR_ARR_STATES in scr_array.h). */
|
||||
export const SCR_ARR_STATES = 16;
|
||||
|
||||
/** Loads the i32 `len` or `cap` of a %ScrArr at `ptr` into `dest` as a
|
||||
* size-type count. */
|
||||
export function loadArrCount(
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
import type { IrExpr, IrFunction, IrModule } from "../../ir/ir.js";
|
||||
import { exprFunctionReference, moduleFunctionReferences } from "../../ir/function-references.js";
|
||||
import {
|
||||
exprFunctionReference,
|
||||
FUNCTION_REFERENCE_KINDS,
|
||||
moduleFunctionReferences,
|
||||
} from "../../ir/function-references.js";
|
||||
import { writtenLocals } from "../../ir/callback-specialization.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
|
||||
/** The nodes the body census below reads. */
|
||||
const CENSUS_KINDS: readonly IrNodeKind[] = [
|
||||
"varDecl",
|
||||
"assign",
|
||||
"forOf",
|
||||
"rethrow",
|
||||
"tryCatch",
|
||||
"call",
|
||||
"callValue",
|
||||
"assignExpr",
|
||||
"incDec",
|
||||
...FUNCTION_REFERENCE_KINDS,
|
||||
];
|
||||
|
||||
/** Closed direct-call consumers can devirtualize a callback when every
|
||||
* incoming call supplies the same function. Keep its evaluated environment,
|
||||
* arguments and ordinary ownership; only the loaded code pointer changes.
|
||||
@@ -15,6 +34,7 @@ import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
export function findConstantCallbacks(
|
||||
mod: IrModule,
|
||||
lifetimes: CallLifetimes,
|
||||
index?: IrNodeIndex,
|
||||
): Map<string, Map<string, string>> {
|
||||
const functions = new Map(mod.functions.map((fn) => [fn.name, fn]));
|
||||
const definitions = new Map<string, Map<string, IrExpr>>();
|
||||
@@ -50,7 +70,7 @@ export function findConstantCallbacks(
|
||||
const writes = new Set<string>();
|
||||
// Parameters and captures each function (or a closure sharing the
|
||||
// binding) writes after entry.
|
||||
const rebinds = writtenLocals(mod);
|
||||
const rebinds = writtenLocals(mod, index);
|
||||
// A function named anywhere but as a direct call's callee can be entered
|
||||
// by a generated adapter or the runtime, which a direct-call census cannot
|
||||
// specialize: the entry, exports, class helpers and methods here, and
|
||||
@@ -66,6 +86,7 @@ export function findConstantCallbacks(
|
||||
// The initializer of each immutable function-typed local declared once:
|
||||
// its only write, even when a capture box (not a stable slot) holds it.
|
||||
const constDefinitions = new Map<string, Map<string, IrExpr | null>>();
|
||||
const scan = index?.scan(CENSUS_KINDS);
|
||||
for (const fn of mod.functions) {
|
||||
const defs = new Map<string, IrExpr>();
|
||||
const consts = new Map<string, IrExpr | null>();
|
||||
@@ -85,51 +106,64 @@ export function findConstantCallbacks(
|
||||
const emptyDecls = new Map<string, number>();
|
||||
const assigned = new Map<string, IrExpr[]>();
|
||||
const rebound = new Set<string>();
|
||||
everyStmtList(fn.body, {
|
||||
stmt: (stmt) => {
|
||||
if (stmt.kind === "varDecl" && stmt.init?.type.kind === "func" && stable?.has(stmt.localId))
|
||||
defs.set(stmt.localId, stmt.init);
|
||||
if (stmt.kind === "varDecl" && immutable.has(stmt.localId))
|
||||
consts.set(stmt.localId, consts.has(stmt.localId) ? null : stmt.init);
|
||||
if (stmt.kind === "varDecl" && hoisted.has(stmt.localId)) {
|
||||
if (stmt.init !== null) rebound.add(stmt.localId);
|
||||
emptyDecls.set(stmt.localId, (emptyDecls.get(stmt.localId) ?? 0) + 1);
|
||||
}
|
||||
if (stmt.kind === "assign" && hoisted.has(stmt.localId)) {
|
||||
const values = assigned.get(stmt.localId) ?? [];
|
||||
values.push(stmt.value);
|
||||
assigned.set(stmt.localId, values);
|
||||
}
|
||||
if (stmt.kind === "forOf" || stmt.kind === "rethrow") rebound.add(stmt.localId);
|
||||
if (stmt.kind === "tryCatch" && stmt.catchLocalId !== null) rebound.add(stmt.catchLocalId);
|
||||
if (stmt.kind === "assign") {
|
||||
if (globalIds.has(stmt.localId)) {
|
||||
if (globals.has(stmt.localId)) writes.add(stmt.localId);
|
||||
else globals.set(stmt.localId, stmt.value);
|
||||
eachNodeOfKinds(fn, scan, (node) => {
|
||||
switch (node.kind) {
|
||||
case "varDecl":
|
||||
if (node.init?.type.kind === "func" && stable?.has(node.localId))
|
||||
defs.set(node.localId, node.init);
|
||||
if (immutable.has(node.localId))
|
||||
consts.set(node.localId, consts.has(node.localId) ? null : node.init);
|
||||
if (hoisted.has(node.localId)) {
|
||||
if (node.init !== null) rebound.add(node.localId);
|
||||
emptyDecls.set(node.localId, (emptyDecls.get(node.localId) ?? 0) + 1);
|
||||
}
|
||||
return;
|
||||
case "assign":
|
||||
if (hoisted.has(node.localId)) {
|
||||
const values = assigned.get(node.localId) ?? [];
|
||||
values.push(node.value);
|
||||
assigned.set(node.localId, values);
|
||||
}
|
||||
if (globalIds.has(node.localId)) {
|
||||
if (globals.has(node.localId)) writes.add(node.localId);
|
||||
else globals.set(node.localId, node.value);
|
||||
}
|
||||
return;
|
||||
case "forOf":
|
||||
case "rethrow":
|
||||
rebound.add(node.localId);
|
||||
return;
|
||||
case "tryCatch":
|
||||
if (node.catchLocalId !== null) rebound.add(node.catchLocalId);
|
||||
return;
|
||||
case "call":
|
||||
if (consumers.has(node.callee)) {
|
||||
const calls = incoming.get(node.callee) ?? [];
|
||||
calls.push({ owner: fn.name, args: node.args });
|
||||
incoming.set(node.callee, calls);
|
||||
}
|
||||
return;
|
||||
case "callValue":
|
||||
if (node.callee.kind === "varRef") callees.add(node.callee.localId);
|
||||
return;
|
||||
case "closure": {
|
||||
escaped.add(node.fnName);
|
||||
const sites = closureSites.get(node.fnName) ?? [];
|
||||
sites.push({ owner: fn.name, captures: node.captures });
|
||||
closureSites.set(node.fnName, sites);
|
||||
return;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
expr: (expr) => {
|
||||
if (expr.kind === "call" && consumers.has(expr.callee)) {
|
||||
const calls = incoming.get(expr.callee) ?? [];
|
||||
calls.push({ owner: fn.name, args: expr.args });
|
||||
incoming.set(expr.callee, calls);
|
||||
case "orDefault": {
|
||||
const referenced = exprFunctionReference(node);
|
||||
if (referenced !== undefined) escaped.add(referenced);
|
||||
return;
|
||||
}
|
||||
if (expr.kind === "callValue" && expr.callee.kind === "varRef")
|
||||
callees.add(expr.callee.localId);
|
||||
const referenced = exprFunctionReference(expr);
|
||||
if (referenced !== undefined) escaped.add(referenced);
|
||||
if (expr.kind === "closure") {
|
||||
const sites = closureSites.get(expr.fnName) ?? [];
|
||||
sites.push({ owner: fn.name, captures: expr.captures });
|
||||
closureSites.set(expr.fnName, sites);
|
||||
}
|
||||
if ((expr.kind === "assignExpr" || expr.kind === "incDec") && globalIds.has(expr.localId))
|
||||
writes.add(expr.localId);
|
||||
if (expr.kind === "assignExpr" || expr.kind === "incDec") rebound.add(expr.localId);
|
||||
return true;
|
||||
},
|
||||
case "assignExpr":
|
||||
case "incDec":
|
||||
if (globalIds.has(node.localId)) writes.add(node.localId);
|
||||
rebound.add(node.localId);
|
||||
return;
|
||||
}
|
||||
});
|
||||
for (const [id, values] of assigned)
|
||||
if (emptyDecls.get(id) === 1 && values.length === 1 && !rebound.has(id))
|
||||
@@ -252,11 +286,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;
|
||||
|
||||
@@ -316,6 +316,55 @@ test.each([32, 64] as const)(
|
||||
},
|
||||
);
|
||||
|
||||
test("arrays without state bytes skip the state stores", () => {
|
||||
const read: IrExpr = { kind: "arrayGet", arr: ref("s", words), index, type: STRING, loc };
|
||||
const write: IrStmt = {
|
||||
kind: "arraySet",
|
||||
arr: ref("s", words),
|
||||
index,
|
||||
value: { kind: "strLit", value: "x", type: STRING, loc },
|
||||
loc,
|
||||
};
|
||||
const push: IrStmt = {
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: ref("a", nums),
|
||||
args: [{ kind: "numLit", value: 2.5, type: F64, loc }],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
};
|
||||
const llvm = emitLlvmModule(
|
||||
module("plain", STRING, [push, write, { kind: "return", value: read, loc }]),
|
||||
);
|
||||
const fn = body(llvm, "plain");
|
||||
// SCR_ARR_STATES (16) in `spare`, field 5.
|
||||
const flag = String.raw`getelementptr inbounds %ScrArr, ptr %t\d+, i32 0, i32 5\n %t\d+ = load i16, ptr %t\d+, !tbaa !\d+\n %t\d+ = and i16 %t\d+, 16\n %t\d+ = icmp ne i16 %t\d+, 0\n`;
|
||||
// A push marks the state only when the array has states.
|
||||
expect(fn).toMatch(
|
||||
new RegExp(
|
||||
`${flag} br i1 %t\\d+, label %arr\\.push\\.state\\d+, label %arr\\.push\\.marked\\d+\\narr\\.push\\.state\\d+:\\n store i8 1, ptr `,
|
||||
),
|
||||
);
|
||||
// Above the prefix, an array without states has no elements (its prefix
|
||||
// is its length): a store there appends at the length and replaces no
|
||||
// value.
|
||||
expect(fn).toMatch(
|
||||
new RegExp(
|
||||
`${flag} br i1 %t\\d+, label %arr\\.set\\.states\\d+, label %arr\\.set\\.plain\\d+`,
|
||||
),
|
||||
);
|
||||
expect(fn).toMatch(
|
||||
/^arr\.set\.plain\d+:\n %t\d+ = icmp eq i64 .*\n br i1 %t\d+, label %arr\.set\.values\d+, label %arr\.set\.slow\d+$/m,
|
||||
);
|
||||
expect(fn).toMatch(/phi i1 \[ %t\d+, %arr\.set\.states\d+ \], \[ false, %arr\.set\.values\d+ \]/);
|
||||
// Reads test no flag: they reach the state check only above the prefix.
|
||||
expect(fn).not.toMatch(/label %arr\.get\.state\d+/);
|
||||
});
|
||||
|
||||
test("state and presence queries answer dense slots inline", () => {
|
||||
const state: IrExpr = { kind: "arrayState", arr: ref("b", flags), index, type: F64, loc };
|
||||
const has: IrExpr = { kind: "arrayHas", arr: ref("s", words), index, type: BOOL, loc };
|
||||
|
||||
@@ -14,13 +14,17 @@
|
||||
* state bytes altogether: the runtime keeps every state below it a present
|
||||
* value, so reads load the slot directly and writes replace it without
|
||||
* touching its state. Other dense indices take the state-checked path,
|
||||
* whose writes extend the bound when they continue it. */
|
||||
* whose writes extend the bound when they continue it. An array without
|
||||
* state bytes (SCR_ARR_STATES clear in `spare`) keeps its bound at its
|
||||
* length, so reads never reach the state check for it; a write past the
|
||||
* bound tests the flag and, without states, appends at the length. */
|
||||
|
||||
import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js";
|
||||
import {
|
||||
f64Lit,
|
||||
loadArrCount,
|
||||
SCR_ARR_FIELD,
|
||||
SCR_ARR_STATES,
|
||||
scrArrStatePtr,
|
||||
scrArrStates,
|
||||
sizeToDouble,
|
||||
@@ -29,10 +33,29 @@ import {
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { compactRefLoadLines, compactRefStoreLines } from "./classes.js";
|
||||
import { elemAccess } from "./shapes.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { indexInteger } from "./integer-values.js";
|
||||
import { integerOrNaNIndex } from "./wrapping-sums.js";
|
||||
|
||||
const SCR_ARR_VALUE = 1;
|
||||
|
||||
/** Whether the array at `array` has state bytes, as an i1. `spare` changes
|
||||
* only in runtime calls, so its loads take their own alias tag. */
|
||||
export function emitArrayHasStates(host: LlvmEmitterContext, array: string): string {
|
||||
const B = host.B;
|
||||
const sparePtr = B.tmp(),
|
||||
spare = B.tmp(),
|
||||
bit = B.tmp(),
|
||||
has = B.tmp();
|
||||
B.line(
|
||||
`${sparePtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.spare}`,
|
||||
);
|
||||
host.markMemoryPointer(sparePtr, "array:spare");
|
||||
B.line(`${spare} = load i16, ptr ${sparePtr}${host.fieldAliasAttachment(sparePtr)}`);
|
||||
B.line(`${bit} = and i16 ${spare}, ${SCR_ARR_STATES}`);
|
||||
B.line(`${has} = icmp ne i16 ${bit}, 0`);
|
||||
return has;
|
||||
}
|
||||
|
||||
/** Call-site effects of the non-retaining read fallbacks: they only read
|
||||
* module-visible memory, or trap without returning to the caller (the trap
|
||||
* writes runtime-private stderr/TLS state). Declaring this lets LLVM keep
|
||||
@@ -76,13 +99,21 @@ function emitDenseGuard(
|
||||
): DenseGuard {
|
||||
const B = host.B;
|
||||
const size = host.sizeType;
|
||||
const integer = exactInteger(host, index, indexExpr);
|
||||
const integer = indexInteger(host, index, indexExpr);
|
||||
const nanIndex = integer === null ? integerOrNaNIndex(host, index, indexExpr) : null;
|
||||
let wide: string;
|
||||
let integral: string | null = null;
|
||||
if (integer) {
|
||||
// A proven integer needs no double round-trip. Negative signed values
|
||||
// compare above any length or capacity as unsigned 64-bit integers.
|
||||
wide = widenInteger(host, integer);
|
||||
if (integer !== null) {
|
||||
// A proven integer, or a wide view that equals the double at every
|
||||
// index below a length, needs no double round-trip. Negative signed
|
||||
// values compare above any length or capacity as unsigned 64-bit
|
||||
// integers, and every index that fails the comparisons takes the
|
||||
// double's path.
|
||||
wide = integer;
|
||||
} else if (nanIndex) {
|
||||
// An exact integer or NaN is an index exactly when it is ordered.
|
||||
wide = nanIndex.wide;
|
||||
integral = nanIndex.ordered;
|
||||
} else {
|
||||
// freeze keeps NaN/out-of-range conversions from producing poison; the
|
||||
// exact round-trip comparison then rejects every value that is not an
|
||||
@@ -211,9 +242,10 @@ function statePointer(
|
||||
return statePtr;
|
||||
}
|
||||
|
||||
/** 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). */
|
||||
/** How an array's dense slots hold its elements: the access kind (`i32`
|
||||
* for int32 arrays, ir/int32-arrays.ts), or `cref` for references to compact
|
||||
* class instances (SCR_ELEM_CREF: a 4-byte slot holding the address over 8;
|
||||
* LlEmitter.compactArrayElem). */
|
||||
type SlotKind = ArrayAccess | "cref";
|
||||
|
||||
function slotKind(host: LlvmEmitterContext, acc: ArrayAccess, element: IrType): SlotKind {
|
||||
@@ -221,14 +253,14 @@ function slotKind(host: LlvmEmitterContext, acc: ArrayAccess, element: IrType):
|
||||
}
|
||||
|
||||
/** The dense slot type: boolean arrays store one byte per element, compact
|
||||
* references four, every other element kind an 8-byte slot. */
|
||||
* references and int32 arrays four, every other element kind an 8-byte slot. */
|
||||
function slotType(kind: SlotKind): "i8" | "i32" | "i64" {
|
||||
return kind === "bool" ? "i8" : kind === "cref" ? "i32" : "i64";
|
||||
return kind === "bool" ? "i8" : kind === "cref" || kind === "i32" ? "i32" : "i64";
|
||||
}
|
||||
|
||||
/** Bytes per dense slot (scr_arr_slot_width). */
|
||||
function slotWidth(kind: SlotKind): number {
|
||||
return kind === "bool" ? 1 : kind === "cref" ? 4 : 8;
|
||||
export function slotWidth(kind: SlotKind): number {
|
||||
return kind === "bool" ? 1 : kind === "cref" || kind === "i32" ? 4 : 8;
|
||||
}
|
||||
|
||||
/** Bytes per dense slot of an array of `element` (scr_arr_slot_width). */
|
||||
@@ -236,6 +268,17 @@ export function denseSlotWidth(host: LlvmEmitterContext, element: IrType): numbe
|
||||
return slotWidth(slotKind(host, elemAccess(element), element));
|
||||
}
|
||||
|
||||
/** The runtime accessor family of an access: int32 arrays answer through
|
||||
* the double-valued entry points, which convert their slots. */
|
||||
export function runtimeAccess(acc: ArrayAccess): "f64" | "bool" | "ref" {
|
||||
return acc === "i32" ? "f64" : acc;
|
||||
}
|
||||
|
||||
/** The LLVM type of an access's values at the emitter's interface. */
|
||||
function valueType(acc: ArrayAccess): "double" | "i1" | "ptr" {
|
||||
return acc === "f64" || acc === "i32" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
}
|
||||
|
||||
function valuePointer(
|
||||
host: LlvmEmitterContext,
|
||||
guard: Pick<DenseGuard, "offset" | "data">,
|
||||
@@ -257,7 +300,13 @@ function loadState(host: LlvmEmitterContext, guard: DenseGuard): string {
|
||||
return state;
|
||||
}
|
||||
|
||||
export type ArrayAccess = "f64" | "bool" | "ref";
|
||||
/** The ScrElemKind of int32 arrays (scr_array.h). */
|
||||
export const SCR_ELEM_I32 = 7;
|
||||
|
||||
/** How a dense array's slots are read and written: doubles, booleans,
|
||||
* references, or the int32 slots of SCR_ELEM_I32 arrays (ir/int32-arrays.ts),
|
||||
* whose values are doubles at the emitter's interface. */
|
||||
export type ArrayAccess = "f64" | "bool" | "ref" | "i32";
|
||||
|
||||
/** Before a self-append (`a[i] = a[i] + suffix`) replaces a reference
|
||||
* element: when `index` lies in the hole-free prefix and the slot still
|
||||
@@ -313,15 +362,33 @@ export function emitDenseArrayGet(
|
||||
retain: boolean,
|
||||
proven = false,
|
||||
): string {
|
||||
return emitDenseArrayRead(host, array, index, indexExpr, acc, element, getter, retain, proven)
|
||||
.value;
|
||||
}
|
||||
|
||||
/** emitDenseArrayGet, with the int32 of an int32 array's element: the
|
||||
* slot itself on the dense path, the runtime getter's double converted
|
||||
* (exactly: the element is an int32) on the other. */
|
||||
export function emitDenseArrayRead(
|
||||
host: LlvmEmitterContext,
|
||||
array: string,
|
||||
index: LlValue,
|
||||
indexExpr: IrExpr | undefined,
|
||||
acc: ArrayAccess,
|
||||
element: IrType,
|
||||
getter: string,
|
||||
retain: boolean,
|
||||
proven = false,
|
||||
): { value: string; int32: string | null } {
|
||||
const B = host.B;
|
||||
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
const ty = valueType(acc);
|
||||
const ret = acc === "bool" ? "zeroext i1" : ty;
|
||||
const kind = slotKind(host, acc, element);
|
||||
host.declare(`declare ${ret} @${getter}(ptr, double)`);
|
||||
if (acc === "ref" && retain && !isRefCounted(element)) {
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call ${ty} @${getter}(ptr ${array}, double ${index.name})`);
|
||||
return t;
|
||||
return { value: t, int32: null };
|
||||
}
|
||||
const slow = B.newLabel("arr.get.slow"),
|
||||
join = B.newLabel("arr.get.join"),
|
||||
@@ -352,10 +419,16 @@ export function emitDenseArrayGet(
|
||||
B.startBlock(valueLabel);
|
||||
const ptr = valuePointer(host, guard, kind);
|
||||
let fast: string;
|
||||
let fastInt: string | null = null;
|
||||
let fastLabel = valueLabel;
|
||||
if (acc === "f64") {
|
||||
fast = B.tmp();
|
||||
B.line(`${fast} = load double, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
|
||||
} else if (acc === "i32") {
|
||||
fastInt = B.tmp();
|
||||
fast = B.tmp();
|
||||
B.line(`${fastInt} = load i32, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
|
||||
B.line(`${fast} = sitofp i32 ${fastInt} to double`);
|
||||
} else if (acc === "bool") {
|
||||
const raw = B.tmp();
|
||||
fast = B.tmp();
|
||||
@@ -382,11 +455,21 @@ export function emitDenseArrayGet(
|
||||
const slowValue = B.tmp();
|
||||
const attrs = acc === "ref" && retain ? " cold" : READ_FALLBACK;
|
||||
B.line(`${slowValue} = call ${ty} @${getter}(ptr ${array}, double ${index.name})${attrs}`);
|
||||
let slowInt: string | null = null;
|
||||
if (fastInt !== null) {
|
||||
slowInt = B.tmp();
|
||||
B.line(`${slowInt} = fptosi double ${slowValue} to i32`);
|
||||
}
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = phi ${ty} [ ${fast}, %${fastLabel} ], [ ${slowValue}, %${slow} ]`);
|
||||
return result;
|
||||
let int32: string | null = null;
|
||||
if (fastInt !== null && slowInt !== null) {
|
||||
int32 = B.tmp();
|
||||
B.line(`${int32} = phi i32 [ ${fastInt}, %${fastLabel} ], [ ${slowInt}, %${slow} ]`);
|
||||
}
|
||||
return { value: result, int32 };
|
||||
}
|
||||
|
||||
/** `scr_arr_get_number`: a numeric element, or NaN for every missing,
|
||||
@@ -397,13 +480,14 @@ export function emitDenseArrayNumber(
|
||||
index: LlValue,
|
||||
indexExpr: IrExpr | undefined,
|
||||
proven = false,
|
||||
acc: "f64" | "i32" = "f64",
|
||||
): string {
|
||||
return emitDenseArrayGet(
|
||||
host,
|
||||
array,
|
||||
index,
|
||||
indexExpr,
|
||||
"f64",
|
||||
acc,
|
||||
{ kind: "f64" },
|
||||
"scr_arr_get_number",
|
||||
false,
|
||||
@@ -458,7 +542,9 @@ export function emitDenseArrayState(
|
||||
* value's reference. A dense index stores the slot, marks it present, and
|
||||
* extends the length when it lands at or past it; a replaced present
|
||||
* reference is released after the new edge is published (a release can
|
||||
* collect cycles). Every other index takes the unchanged runtime call. */
|
||||
* collect cycles). Every other index takes the unchanged runtime call.
|
||||
* `int32`, for an int32 array, is the value as an i32 when the caller has
|
||||
* it at hand. */
|
||||
export function emitDenseArraySet(
|
||||
host: LlvmEmitterContext,
|
||||
array: string,
|
||||
@@ -468,11 +554,12 @@ export function emitDenseArraySet(
|
||||
element: IrType,
|
||||
value: string,
|
||||
proven = false,
|
||||
int32: string | null = null,
|
||||
): void {
|
||||
const B = host.B;
|
||||
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
const ty = valueType(acc);
|
||||
const kind = slotKind(host, acc, element);
|
||||
const setter = `scr_arr_set_${acc}`;
|
||||
const setter = `scr_arr_set_${runtimeAccess(acc)}`;
|
||||
host.declare(`declare void @${setter}(ptr, double, ${acc === "bool" ? "i1 zeroext" : ty})`);
|
||||
const call = (attrs: string): void =>
|
||||
B.line(`call void @${setter}(ptr ${array}, double ${index.name}, ${ty} ${value})${attrs}`);
|
||||
@@ -480,6 +567,13 @@ export function emitDenseArraySet(
|
||||
call("");
|
||||
return;
|
||||
}
|
||||
// An int32 array's stores are proven int32s (ir/int32-arrays.ts); both
|
||||
// dense paths below store the converted value.
|
||||
let int32Value: string | null = int32;
|
||||
if (acc === "i32" && int32Value === null) {
|
||||
int32Value = B.tmp();
|
||||
B.line(`${int32Value} = fptosi double ${value} to i32`);
|
||||
}
|
||||
const slow = B.newLabel("arr.set.slow"),
|
||||
join = B.newLabel("arr.set.join"),
|
||||
filledLabel = B.newLabel("arr.set.filled");
|
||||
@@ -498,6 +592,8 @@ export function emitDenseArraySet(
|
||||
const storeValue = (valuePtr: string): void => {
|
||||
if (acc === "f64") {
|
||||
B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
} else if (acc === "i32") {
|
||||
B.line(`store i32 ${int32Value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
} else if (acc === "bool") {
|
||||
const bits = B.tmp();
|
||||
B.line(`${bits} = zext i1 ${value} to i8`);
|
||||
@@ -510,19 +606,49 @@ export function emitDenseArraySet(
|
||||
B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
}
|
||||
};
|
||||
const stated = B.newLabel("arr.set.states"),
|
||||
plain = B.newLabel("arr.set.plain"),
|
||||
plainStore = B.newLabel("arr.set.values"),
|
||||
stored = B.newLabel("arr.set.stored.slot");
|
||||
B.condBr(emitArrayHasStates(host, array), stated, plain);
|
||||
B.startBlock(stated);
|
||||
const statePtr = statePointer(host, guard);
|
||||
const valuePtr = valuePointer(host, guard, kind);
|
||||
let oldState: string | null = null;
|
||||
let old: string | null = null;
|
||||
let statedOld: string | null = null;
|
||||
let statedHad: string | null = null;
|
||||
// Compact instances are never released: a replaced one needs nothing.
|
||||
if (kind === "ref") {
|
||||
oldState = B.tmp();
|
||||
old = B.tmp();
|
||||
const oldState = B.tmp();
|
||||
statedOld = B.tmp();
|
||||
statedHad = B.tmp();
|
||||
B.line(`${oldState} = load i8, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
|
||||
B.line(`${old} = load ptr, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
B.line(`${statedOld} = load ptr, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
B.line(`${statedHad} = icmp eq i8 ${oldState}, ${SCR_ARR_VALUE}`);
|
||||
}
|
||||
storeValue(valuePtr);
|
||||
B.line(`store i8 ${SCR_ARR_VALUE}, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
|
||||
const statedEnd = B.currentLabel();
|
||||
B.br(stored);
|
||||
// Without states the bound is the length, so a store here appends at
|
||||
// the length (it replaces no value) or leaves holes, which the runtime
|
||||
// gives states.
|
||||
B.startBlock(plain);
|
||||
const appends = B.tmp();
|
||||
B.line(`${appends} = icmp eq ${host.sizeType} ${offset}, ${len}`);
|
||||
B.condBr(appends, plainStore, slow);
|
||||
B.startBlock(plainStore);
|
||||
storeValue(valuePointer(host, guard, kind));
|
||||
const plainEnd = B.currentLabel();
|
||||
B.br(stored);
|
||||
B.startBlock(stored);
|
||||
let old: string | null = null;
|
||||
let hadValue: string | null = null;
|
||||
if (kind === "ref") {
|
||||
old = B.tmp();
|
||||
hadValue = B.tmp();
|
||||
B.line(`${old} = phi ptr [ ${statedOld}, %${statedEnd} ], [ null, %${plainEnd} ]`);
|
||||
B.line(`${hadValue} = phi i1 [ ${statedHad}, %${statedEnd} ], [ false, %${plainEnd} ]`);
|
||||
}
|
||||
// A store at the bound continues the hole-free prefix.
|
||||
const filledWide = sizedFilled(host, guard.filled),
|
||||
continues = B.tmp();
|
||||
@@ -566,9 +692,7 @@ export function emitDenseArraySet(
|
||||
storeArrCount(host, nextLen, lenPtr, host.fieldAliasAttachment(lenPtr));
|
||||
B.br(storedLabel);
|
||||
B.startBlock(storedLabel);
|
||||
if (kind === "ref" && oldState && old) {
|
||||
const hadValue = B.tmp();
|
||||
B.line(`${hadValue} = icmp eq i8 ${oldState}, ${SCR_ARR_VALUE}`);
|
||||
if (kind === "ref" && hadValue && old) {
|
||||
const releaseLabel = B.newLabel("arr.set.release");
|
||||
B.condBr(hadValue, releaseLabel, join);
|
||||
B.startBlock(releaseLabel);
|
||||
@@ -657,15 +781,31 @@ export function emitDenseArrayPop(
|
||||
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;
|
||||
if (kind === "cref") {
|
||||
// The 8-byte slot of a compact reference is its address.
|
||||
const wide = B.tmp(),
|
||||
address = B.tmp();
|
||||
B.line(`${wide} = zext i32 ${raw} to i64`);
|
||||
B.line(`${address} = shl nuw nsw i64 ${wide}, 3`);
|
||||
raw = address;
|
||||
} else {
|
||||
// The slot bits of an int32 element's double, as the f64 path returns them.
|
||||
const number = B.tmp(),
|
||||
bits = B.tmp();
|
||||
B.line(`${number} = sitofp i32 ${raw} to double`);
|
||||
B.line(`${bits} = bitcast double ${number} to i64`);
|
||||
raw = bits;
|
||||
}
|
||||
}
|
||||
B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
// Only an array with states has a state byte to clear.
|
||||
const clear = B.newLabel("arr.pop.state"),
|
||||
cleared = B.newLabel("arr.pop.cleared");
|
||||
B.condBr(emitArrayHasStates(host, array), clear, cleared);
|
||||
B.startBlock(clear);
|
||||
B.line(`store i8 0, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
|
||||
B.br(cleared);
|
||||
B.startBlock(cleared);
|
||||
let last32 = last;
|
||||
if (size === "i64") {
|
||||
last32 = B.tmp();
|
||||
@@ -692,8 +832,8 @@ export function emitDenseArrayPush(
|
||||
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}`;
|
||||
const ty = valueType(acc);
|
||||
const pusher = `scr_arr_push_${runtimeAccess(acc)}`;
|
||||
host.declare(`declare double @${pusher}(ptr, ${acc === "bool" ? "i1 zeroext" : ty})`);
|
||||
const header = (field: number): string => {
|
||||
const ptr = B.tmp();
|
||||
@@ -731,6 +871,10 @@ export function emitDenseArrayPush(
|
||||
const valuePtr = valuePointer(host, { offset: len, data }, kind);
|
||||
if (acc === "f64") {
|
||||
B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
} else if (acc === "i32") {
|
||||
const int32 = B.tmp();
|
||||
B.line(`${int32} = fptosi double ${value} to i32`);
|
||||
B.line(`store i32 ${int32}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
} else if (acc === "bool") {
|
||||
const bits = B.tmp();
|
||||
B.line(`${bits} = zext i1 ${value} to i8`);
|
||||
@@ -742,7 +886,14 @@ export function emitDenseArrayPush(
|
||||
} else {
|
||||
B.line(`store ptr ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
}
|
||||
// Only an array with states has a state byte to mark.
|
||||
const mark = B.newLabel("arr.push.state"),
|
||||
marked = B.newLabel("arr.push.marked");
|
||||
B.condBr(emitArrayHasStates(host, array), mark, marked);
|
||||
B.startBlock(mark);
|
||||
B.line(`store i8 ${SCR_ARR_VALUE}, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
|
||||
B.br(marked);
|
||||
B.startBlock(marked);
|
||||
const next = B.tmp(),
|
||||
fastValue = B.tmp();
|
||||
B.line(`${next} = add nuw ${size} ${len}, 1`);
|
||||
@@ -760,6 +911,7 @@ export function emitDenseArrayPush(
|
||||
B.line(`${bumped} = select i1 ${packed}, i32 ${nextFilled}, i32 ${filled}`);
|
||||
B.line(`store i32 ${bumped}, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
|
||||
B.line(`${fastValue} = ${sizeToDouble(size)} ${size} ${next} to double`);
|
||||
const fastEnd = B.currentLabel();
|
||||
B.br(join);
|
||||
B.startBlock(slow);
|
||||
const slowValue = B.tmp();
|
||||
@@ -767,6 +919,6 @@ export function emitDenseArrayPush(
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = phi double [ ${fastValue}, %${fast} ], [ ${slowValue}, %${slow} ]`);
|
||||
B.line(`${result} = phi double [ ${fastValue}, %${fastEnd} ], [ ${slowValue}, %${slow} ]`);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { emitPerKeyOrder } from "./key-orders.js";
|
||||
import { typedRefConstructor } from "./shapes.js";
|
||||
import { bindLiveArrayOps } from "./expr-stream-bridges.js";
|
||||
import { BYTES_ELEM_NUM, SCR_ARR_FIELD, f64Lit, sizeToDouble } from "./common.js";
|
||||
import { BYTES_ELEM_NUM, SCR_ARR_FIELD, SCR_ARR_STATES, f64Lit, sizeToDouble } from "./common.js";
|
||||
import { emitUnionPeek, emitUnionTag } from "./union-repr.js";
|
||||
import {
|
||||
INLINE_UNION_FIELD,
|
||||
@@ -37,6 +38,8 @@ import {
|
||||
accessorSlotProp,
|
||||
DYN_HANDLE_KINDS,
|
||||
isDirentShape,
|
||||
KEY_ORDER_FIELD,
|
||||
shapeKeyOrders,
|
||||
isDynTypedRefType,
|
||||
isRefCounted,
|
||||
isUnitType,
|
||||
@@ -153,6 +156,8 @@ export interface DynHost extends WalkerHost {
|
||||
/** See moduleUsesDynBuiltinStream. */
|
||||
usesDynBuiltinStream?(): boolean;
|
||||
liveDynUnionRefAdapter(t: IrType & { kind: "union" }): string;
|
||||
/** The module has declaration-typed boundary casts (IrModule.declBoundary). */
|
||||
declBoundary?(): boolean;
|
||||
dynPromiseAdapter(t: IrType): string;
|
||||
isErrorClass(className: string): boolean;
|
||||
classSubtypes(className: string): readonly string[];
|
||||
@@ -196,6 +201,55 @@ export class LlDyn {
|
||||
|
||||
constructor(private readonly host: DynHost) {}
|
||||
|
||||
/** Journal replay (emit-journal.ts): builds or finds the helper a chunk
|
||||
* emitter recorded, and answers its name. */
|
||||
replayHelper(op: string, args: readonly unknown[]): string {
|
||||
switch (op) {
|
||||
case "dyn.collectionDynOps":
|
||||
return this.collectionDynOps(args[0] as IrType & { kind: "map" | "set" });
|
||||
case "dyn.dynMatchHelper":
|
||||
return this.dynMatchHelper(args[0] as IrType, args[1] as boolean);
|
||||
case "dyn.jsonSchema":
|
||||
return this.jsonSchema(args[0] as IrType);
|
||||
case "dyn.recordNameTable": {
|
||||
const shapeId = args[0] as string;
|
||||
return this.recordNameTable(shapeId, this.host.recordsById.get(shapeId)!).names;
|
||||
}
|
||||
case "dyn.jsonParseHelper":
|
||||
return this.jsonParseHelper(args[0] as IrType)!;
|
||||
case "dyn.dynCheckHelper":
|
||||
return this.dynCheckHelper(args[0] as IrType, args[1] as boolean);
|
||||
case "dyn.toDynHelper":
|
||||
return this.toDynHelper(args[0] as IrType);
|
||||
case "dyn.promiseDynAdapterHelper":
|
||||
return this.promiseDynAdapterHelper(args[0] as IrType);
|
||||
case "dyn.dynFuncThunkHelper":
|
||||
return this.dynFuncThunkHelper(args[0] as IrType & { kind: "func" });
|
||||
case "dyn.dynFuncBoxHelper":
|
||||
return this.dynFuncBoxHelper(args[0] as IrType & { kind: "func" });
|
||||
case "dyn.dynFuncAdapterHelper":
|
||||
return this.dynFuncAdapterHelper(args[0] as IrType & { kind: "func" });
|
||||
case "dyn.dynToStrHelper":
|
||||
return this.dynToStrHelper();
|
||||
case "dyn.dynIterNHelper":
|
||||
return this.dynIterNHelper();
|
||||
case "dyn.dynDestrCheckHelper":
|
||||
return this.dynDestrCheckHelper();
|
||||
case "dyn.dynKeyGetHelper":
|
||||
return this.dynKeyGetHelper();
|
||||
case "dyn.dynComputedKeyGetHelper":
|
||||
return this.dynComputedKeyGetHelper();
|
||||
case "dyn.caughtToDynHelper":
|
||||
return this.caughtToDynHelper();
|
||||
case "dyn.messageListenerHelper":
|
||||
return this.messageListenerHelper(args[0] as IrType & { kind: "func" }) ?? "";
|
||||
case "dyn.messageReaderHelper":
|
||||
return this.messageReaderHelper(args[0] as IrType);
|
||||
default:
|
||||
throw new InternalCompilerError(`llvm emitter bug: no dyn helper replay ${op}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** The types a dyn matcher or builder for `t` checks structurally: record
|
||||
* fields and index values, array elements and union arms. */
|
||||
private structuralChildren(t: IrType): readonly IrType[] {
|
||||
@@ -316,6 +370,36 @@ export class LlDyn {
|
||||
B.line(`call void @scr_dyn_build_add(ptr %d, ptr @${name}, ptr ${value})`);
|
||||
}
|
||||
|
||||
/** The order a dyn value tries a union's arms in: the canonical order,
|
||||
* except that among the record arms, those with more required fields go
|
||||
* first, then those with more fields. Records are width-tolerant, so an
|
||||
* arm with fewer fields (`{}`, or one whose fields are all optional,
|
||||
* matches every object) would otherwise take an object that a wider arm
|
||||
* also matches and drop the fields only the wider arm has. */
|
||||
private unionArmOrder(def: IrUnionDef): number[] {
|
||||
const width = (i: number): [number, number] | null => {
|
||||
const arm = def.arms[i]!;
|
||||
if (arm.kind !== "record") return null;
|
||||
const shape = this.host.recordsById.get(arm.shapeId);
|
||||
if (!shape || shape.tuple) return null;
|
||||
const required = shape.fields.filter(
|
||||
(field) =>
|
||||
field.type.kind !== "union" ||
|
||||
!this.host.unionsById.get(field.type.unionId)?.arms.some((x) => x.kind === "undefinedT"),
|
||||
).length;
|
||||
return [required, shape.fields.length];
|
||||
};
|
||||
const order = def.arms.map((_, i) => i);
|
||||
const records = order.filter((i) => width(i) !== null);
|
||||
const widest = [...records].sort((x, y) => {
|
||||
const [rx, fx] = width(x)!;
|
||||
const [ry, fy] = width(y)!;
|
||||
return ry - rx || fy - fx || x - y;
|
||||
});
|
||||
records.forEach((slot, k) => (order[slot] = widest[k]!));
|
||||
return order;
|
||||
}
|
||||
|
||||
/** Branch to a variant only after its literal discriminator and complete
|
||||
* structure match. Use the same path for predicates and builders. */
|
||||
private unionArmMatch(
|
||||
@@ -385,6 +469,7 @@ export class LlDyn {
|
||||
if (prior) return prior;
|
||||
const name = `sc_map_dyn_${this.mapDynOps.size}`;
|
||||
this.mapDynOps.set(key, name);
|
||||
this.host.journalAllocated?.(name, "dyn.collectionDynOps", [t]);
|
||||
const keyType = t.kind === "map" ? t.key : t.elem;
|
||||
const valueType: IrType = t.kind === "map" ? t.value : { kind: "f64" };
|
||||
const emitSlot = (type: IrType, role: string): void => {
|
||||
@@ -442,6 +527,7 @@ export class LlDyn {
|
||||
`@${name} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr } { ptr ${this.host.cstr(key)}, ptr @${name}_key_box, ptr @${name}_key_unbox, ptr @${this.dynMatchHelper(keyType)}, ptr @${name}_value_box, ptr @${name}_value_unbox }`,
|
||||
"",
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -840,6 +926,7 @@ export class LlDyn {
|
||||
if (existing) return existing;
|
||||
const name = `sc_dm_${this.dynMatchers.size}`;
|
||||
this.dynMatchers.set(matcherKey, name);
|
||||
this.host.journalAllocated?.(name, "dyn.dynMatchHelper", [t, preserveRefs]);
|
||||
const recursive = this.isRecursiveShape(t);
|
||||
const wrapped = recursive || this.holdsRecursiveShape(t);
|
||||
const B = new BlockBuilder();
|
||||
@@ -1055,9 +1142,10 @@ export class LlDyn {
|
||||
}
|
||||
B.condBr(readable, l0, fail);
|
||||
B.startBlock(l0);
|
||||
// dyn ('unknown') fields match ANY value, present or missing.
|
||||
// dyn ('unknown') fields match ANY value, present or missing. The
|
||||
// order index is no key.
|
||||
for (const f of shape.fields) {
|
||||
if (f.type.kind === "dyn") continue;
|
||||
if (f.type.kind === "dyn" || f.name === KEY_ORDER_FIELD) continue;
|
||||
const { value: m, release } = this.memberRead(B, f.name, f.type);
|
||||
const lNext = B.newLabel("dm.n");
|
||||
this.pendingBail(
|
||||
@@ -1200,6 +1288,7 @@ export class LlDyn {
|
||||
``,
|
||||
);
|
||||
if (wrapped) this.emitVisitWrapper(name, t, recursive ? "match" : "visit");
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -1239,9 +1328,10 @@ export class LlDyn {
|
||||
return (
|
||||
shape.fields.every(
|
||||
(f) =>
|
||||
!f.name.startsWith("%") &&
|
||||
f.name !== "__proto__" &&
|
||||
this.jsonSchemaSupported(f.type, visiting, true),
|
||||
f.name === KEY_ORDER_FIELD ||
|
||||
(!f.name.startsWith("%") &&
|
||||
f.name !== "__proto__" &&
|
||||
this.jsonSchemaSupported(f.type, visiting, true)),
|
||||
) &&
|
||||
(!index || this.jsonSchemaSupported(index, visiting))
|
||||
);
|
||||
@@ -1293,6 +1383,7 @@ export class LlDyn {
|
||||
const S = host.sizeType;
|
||||
const name = `sc_jsch_${this.jsonSchemas.size}`;
|
||||
this.jsonSchemas.set(key, name);
|
||||
this.host.journalAllocated?.(name, "dyn.jsonSchema", [t]);
|
||||
let body: string;
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
@@ -1326,7 +1417,9 @@ export class LlDyn {
|
||||
const base = host.nullableUnions.fieldBase(t.shapeId);
|
||||
const index = new Map(shape.fields.map((f, i) => [f.name, i + base]));
|
||||
// The runtime names kept-source keys by this index (record-sources.ts).
|
||||
const order = recordSourceFieldOrder(shape);
|
||||
// The order index is no key: parsed values keep the table's order
|
||||
// and their source's own order.
|
||||
const order = recordSourceFieldOrder(shape).filter((name) => name !== KEY_ORDER_FIELD);
|
||||
if (recordMayKeepSource(shape)) this.keepsRecordSources = true;
|
||||
const fieldTy = `{ ptr, ${S}, ${S}, ptr }`;
|
||||
const entries = order.map((fieldName) => {
|
||||
@@ -1364,6 +1457,7 @@ export class LlDyn {
|
||||
`@${name} = internal constant { ${S}, ${S}, ptr, ptr, ptr, ptr, ptr, ptr, ${S} } { ${body} }`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -1453,6 +1547,7 @@ export class LlDyn {
|
||||
const base = `sc_rnames_${this.recordNameTables.size}`;
|
||||
const table = { names: `@${base}`, lens: `@${base}_lens` };
|
||||
this.recordNameTables.set(shapeId, table);
|
||||
this.host.journalAllocated?.(base, "dyn.recordNameTable", [shapeId]);
|
||||
const n = shape.fields.length;
|
||||
const names = recordSourceFieldOrder(shape);
|
||||
this.defs.push(
|
||||
@@ -1460,6 +1555,7 @@ export class LlDyn {
|
||||
`@${base}_lens = internal constant [${n} x ${S}] [${names.map((name) => `${S} ${Buffer.byteLength(name, "utf8")}`).join(", ")}]`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return table;
|
||||
}
|
||||
|
||||
@@ -1478,6 +1574,7 @@ export class LlDyn {
|
||||
const host = this.host;
|
||||
const name = `sc_jp_${this.jsonParsers.size}`;
|
||||
this.jsonParsers.set(key, name);
|
||||
this.host.journalAllocated?.(name, "dyn.jsonParseHelper", [t]);
|
||||
const schema = this.jsonSchema(t);
|
||||
const check = this.dynCheckHelper(t);
|
||||
host.declare(`declare ptr @scr_json_parse_schema(ptr, ptr)`);
|
||||
@@ -1504,6 +1601,7 @@ export class LlDyn {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -1519,6 +1617,7 @@ export class LlDyn {
|
||||
if (existing) return existing;
|
||||
const name = `sc_dc_${this.dynBuilders.size}`;
|
||||
this.dynBuilders.set(builderKey, name);
|
||||
this.host.journalAllocated?.(name, "dyn.dynCheckHelper", [t, preserveRefs]);
|
||||
const recursive = this.isRecursiveShape(t);
|
||||
const memo = !recursive && this.holdsRecursiveShape(t) && this.valTy(t) === "ptr";
|
||||
const host = this.host;
|
||||
@@ -1541,6 +1640,7 @@ export class LlDyn {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
if (isRefCounted(t) && t.kind !== "dyn") {
|
||||
@@ -1719,6 +1819,32 @@ export class LlDyn {
|
||||
B.line(`store ${member.type} ${oldValue}, ptr ${checkedPtr}`);
|
||||
});
|
||||
if (t.kind !== "record") {
|
||||
// Whether the dense slots carry states (SCR_ARR_STATES in
|
||||
// `spare`) moves with them; the rest of `spare` stays.
|
||||
const cachedSpare = B.tmp(),
|
||||
checkedSpare = B.tmp();
|
||||
const sparePtr = (array: string): string => {
|
||||
const ptr = B.tmp();
|
||||
B.line(
|
||||
`${ptr} = getelementptr inbounds %ScrArr, ptr ${array}, i64 0, i32 ${SCR_ARR_FIELD.spare}`,
|
||||
);
|
||||
return ptr;
|
||||
};
|
||||
const cachedSparePtr = sparePtr(cached),
|
||||
checkedSparePtr = sparePtr(checked);
|
||||
B.line(`${cachedSpare} = load i16, ptr ${cachedSparePtr}`);
|
||||
B.line(`${checkedSpare} = load i16, ptr ${checkedSparePtr}`);
|
||||
const exchange = (own: string, other: string, ptr: string): void => {
|
||||
const kept = B.tmp(),
|
||||
moved = B.tmp(),
|
||||
next = B.tmp();
|
||||
B.line(`${kept} = and i16 ${own}, ${~SCR_ARR_STATES}`);
|
||||
B.line(`${moved} = and i16 ${other}, ${SCR_ARR_STATES}`);
|
||||
B.line(`${next} = or i16 ${kept}, ${moved}`);
|
||||
B.line(`store i16 ${next}, ptr ${ptr}`);
|
||||
};
|
||||
exchange(cachedSpare, checkedSpare, cachedSparePtr);
|
||||
exchange(checkedSpare, cachedSpare, checkedSparePtr);
|
||||
// Sparse slots and numeric properties live out of line; each
|
||||
// array keeps its own match metadata.
|
||||
host.declare("declare void @scr_arr_swap_indexed_ext(ptr, ptr)");
|
||||
@@ -2069,7 +2195,10 @@ export class LlDyn {
|
||||
}
|
||||
B.line(`%r0 = call ptr @${mangleRecordNew(t.shapeId)}()`);
|
||||
if (recursive) this.recordBuilt(B, name, "%r0");
|
||||
// The order index stays 0: the source's own order is kept below
|
||||
// when it differs from the table.
|
||||
for (const f of shape.fields) {
|
||||
if (f.name === KEY_ORDER_FIELD) continue;
|
||||
const { value: m, release } = this.memberRead(B, f.name, f.type);
|
||||
this.pendingBail(
|
||||
B,
|
||||
@@ -2269,9 +2398,10 @@ export class LlDyn {
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: dynCheck of unknown union ${t.unionId}`,
|
||||
);
|
||||
// Arms in CANONICAL order, first FULL match wins. The matched
|
||||
// arm's builder can no longer fail.
|
||||
def.arms.forEach((arm, i) => {
|
||||
// Arms in unionArmOrder, first FULL match wins. The matched arm's
|
||||
// builder can no longer fail.
|
||||
for (const i of this.unionArmOrder(def)) {
|
||||
const arm = def.arms[i]!;
|
||||
const lHit = B.newLabel("dcu.h");
|
||||
const lNext = B.newLabel("dcu.n");
|
||||
this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs);
|
||||
@@ -2315,7 +2445,7 @@ export class LlDyn {
|
||||
B.terminate(`ret ptr ${u}`);
|
||||
}
|
||||
B.startBlock(lNext);
|
||||
});
|
||||
}
|
||||
if (preserveRefs) {
|
||||
B.line(
|
||||
`call void @scr_dyn_check_fail(ptr %path, ptr ${host.cstr("reference with its original layout")}, ptr %d)`,
|
||||
@@ -2537,6 +2667,7 @@ export class LlDyn {
|
||||
``,
|
||||
);
|
||||
if (recursive || memo) this.emitVisitWrapper(name, t, recursive ? "build" : "memo");
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -2551,6 +2682,7 @@ export class LlDyn {
|
||||
if (existing) return existing;
|
||||
const name = `sc_td_${this.toDynFns.size}`;
|
||||
this.toDynFns.set(key, name);
|
||||
this.host.journalAllocated?.(name, "dyn.toDynHelper", [t]);
|
||||
const host = this.host;
|
||||
const B = new BlockBuilder();
|
||||
let sourceAccessor: { name: string; release: string } | null = null;
|
||||
@@ -2800,70 +2932,79 @@ export class LlDyn {
|
||||
host.declare(`declare ptr @scr_dyn_new_obj()`);
|
||||
B.line(`${d} = call ptr @scr_dyn_new_obj()`);
|
||||
}
|
||||
// Keys insert in DECLARED order (JS insertion order); internal
|
||||
// '%'-fields follow so a record→dyn→record round trip keeps them.
|
||||
// Keys insert in DECLARED order (JS insertion order), or in the
|
||||
// value's own order for a shape with orderVariants; internal
|
||||
// '%'-fields follow so a record→dyn→record round trip keeps them
|
||||
// (all but the order index, which is no key).
|
||||
const byName = new Map(shape.fields.map((f) => [f.name, f]));
|
||||
// An accessor property is one key whose getter and setter are the
|
||||
// slot closures (see accessorSlotProp), in accessorOrder's place.
|
||||
const order = shape.accessorOrder ?? shape.declaredOrder ?? shape.fields.map((f) => f.name);
|
||||
const inOrder = new Set(order);
|
||||
const accessors = new Set(
|
||||
shape.accessorOrder === undefined
|
||||
? []
|
||||
: shape.fields.flatMap((f) => {
|
||||
const slot = accessorSlotProp(f.name);
|
||||
return slot === null ? [] : [slot.prop];
|
||||
}),
|
||||
);
|
||||
type DynField = { name: string; type: IrType } | { accessor: string };
|
||||
const dynFields: DynField[] = [
|
||||
...order.flatMap((n) => {
|
||||
if (accessors.has(n)) return [{ accessor: n } as DynField];
|
||||
const f = byName.get(n);
|
||||
return f === undefined ? [] : [f as DynField];
|
||||
}),
|
||||
...shape.fields.filter(
|
||||
(f) =>
|
||||
!inOrder.has(f.name) && !(accessors.size > 0 && accessorSlotProp(f.name) !== null),
|
||||
),
|
||||
];
|
||||
for (const entry of dynFields) {
|
||||
if ("accessor" in entry) {
|
||||
host.declare(
|
||||
`declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`,
|
||||
);
|
||||
const boxed = (kind: "get" | "set"): string => {
|
||||
const slot = byName.get(`%${kind}:${entry.accessor}`);
|
||||
if (slot === undefined) return "null";
|
||||
const fv = loadFieldOf(slot.name, slot.type);
|
||||
const conv = B.tmp();
|
||||
B.line(
|
||||
`${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`,
|
||||
);
|
||||
return conv;
|
||||
};
|
||||
const getter = boxed("get");
|
||||
const setter = boxed("set");
|
||||
B.line(
|
||||
`call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const f = entry;
|
||||
const klen = Buffer.byteLength(f.name, "utf8");
|
||||
const fv = loadFieldOf(f.name, f.type);
|
||||
// An ABSENT optional field contributes no key.
|
||||
const lSkip = f.type.kind === "union" ? this.skipIfAbsent(B, fv, f.type) : null;
|
||||
const conv = B.tmp();
|
||||
B.line(`${conv} = call ptr @${this.toDynHelper(f.type)}(${this.valTy(f.type)} ${fv})`);
|
||||
B.line(
|
||||
`call void @scr_dyn_obj_set(ptr ${d}, ptr ${host.cstr(f.name)}, ${host.sizeType} ${klen}, ptr ${conv}) ; ${llvmCommentText(f.name)}`,
|
||||
const orders =
|
||||
shape.accessorOrder !== undefined ? [shape.accessorOrder] : shapeKeyOrders(shape);
|
||||
const perOrder = (order: readonly string[]): void => {
|
||||
const inOrder = new Set(order);
|
||||
const accessors = new Set(
|
||||
shape.accessorOrder === undefined
|
||||
? []
|
||||
: shape.fields.flatMap((f) => {
|
||||
const slot = accessorSlotProp(f.name);
|
||||
return slot === null ? [] : [slot.prop];
|
||||
}),
|
||||
);
|
||||
if (lSkip !== null) {
|
||||
B.br(lSkip);
|
||||
B.startBlock(lSkip);
|
||||
type DynField = { name: string; type: IrType } | { accessor: string };
|
||||
const dynFields: DynField[] = [
|
||||
...order.flatMap((n) => {
|
||||
if (accessors.has(n)) return [{ accessor: n } as DynField];
|
||||
const f = byName.get(n);
|
||||
return f === undefined ? [] : [f as DynField];
|
||||
}),
|
||||
...shape.fields.filter(
|
||||
(f) =>
|
||||
!inOrder.has(f.name) &&
|
||||
f.name !== KEY_ORDER_FIELD &&
|
||||
!(accessors.size > 0 && accessorSlotProp(f.name) !== null),
|
||||
),
|
||||
];
|
||||
for (const entry of dynFields) {
|
||||
if ("accessor" in entry) {
|
||||
host.declare(
|
||||
`declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`,
|
||||
);
|
||||
const boxed = (kind: "get" | "set"): string => {
|
||||
const slot = byName.get(`%${kind}:${entry.accessor}`);
|
||||
if (slot === undefined) return "null";
|
||||
const fv = loadFieldOf(slot.name, slot.type);
|
||||
const conv = B.tmp();
|
||||
B.line(
|
||||
`${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`,
|
||||
);
|
||||
return conv;
|
||||
};
|
||||
const getter = boxed("get");
|
||||
const setter = boxed("set");
|
||||
B.line(
|
||||
`call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const f = entry;
|
||||
const klen = Buffer.byteLength(f.name, "utf8");
|
||||
const fv = loadFieldOf(f.name, f.type);
|
||||
// An ABSENT optional field contributes no key.
|
||||
const lSkip = f.type.kind === "union" ? this.skipIfAbsent(B, fv, f.type) : null;
|
||||
const conv = B.tmp();
|
||||
B.line(`${conv} = call ptr @${this.toDynHelper(f.type)}(${this.valTy(f.type)} ${fv})`);
|
||||
B.line(
|
||||
`call void @scr_dyn_obj_set(ptr ${d}, ptr ${host.cstr(f.name)}, ${host.sizeType} ${klen}, ptr ${conv}) ; ${llvmCommentText(f.name)}`,
|
||||
);
|
||||
if (lSkip !== null) {
|
||||
B.br(lSkip);
|
||||
B.startBlock(lSkip);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (orders.length === 1) perOrder(orders[0]!);
|
||||
else emitPerKeyOrder(B, shape, base, "%v", perOrder);
|
||||
// A Dirent row takes Node's Dirent prototype (the type probes) and
|
||||
// its Symbol(type) key; %dtype stays readable but non-enumerable.
|
||||
if (isDirentShape(shape)) {
|
||||
@@ -3157,6 +3298,7 @@ export class LlDyn {
|
||||
``,
|
||||
);
|
||||
}
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -3173,6 +3315,7 @@ export class LlDyn {
|
||||
if (existing) return existing;
|
||||
const name = `sc_pda_${this.promiseDynAdapters.size}`;
|
||||
this.promiseDynAdapters.set(key, name);
|
||||
this.host.journalAllocated?.(name, "dyn.promiseDynAdapterHelper", [inner]);
|
||||
const host = this.host;
|
||||
const B = new BlockBuilder();
|
||||
host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`);
|
||||
@@ -3250,6 +3393,7 @@ export class LlDyn {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -3262,6 +3406,7 @@ export class LlDyn {
|
||||
if (this.dynToStrFn) return this.dynToStrFn;
|
||||
const name = "sc_ds";
|
||||
this.dynToStrFn = name;
|
||||
this.host.journalAllocated?.(name, "dyn.dynToStrHelper", []);
|
||||
const host = this.host;
|
||||
host.declare(`declare void @scr_jb_init(ptr)`);
|
||||
host.declare(`declare ptr @scr_jb_finish(ptr)`);
|
||||
@@ -3566,6 +3711,7 @@ export class LlDyn {
|
||||
``,
|
||||
);
|
||||
}
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -3577,6 +3723,7 @@ export class LlDyn {
|
||||
if (this.caughtToDynFn) return this.caughtToDynFn;
|
||||
const name = "sc_cd";
|
||||
this.caughtToDynFn = name;
|
||||
this.host.journalAllocated?.(name, "dyn.caughtToDynHelper", []);
|
||||
const host = this.host;
|
||||
const B = new BlockBuilder();
|
||||
const kp = B.tmp();
|
||||
@@ -3682,6 +3829,7 @@ export class LlDyn {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -3695,6 +3843,7 @@ export class LlDyn {
|
||||
this.helperMemo.set(name, name);
|
||||
const B = new BlockBuilder();
|
||||
const host = this.host;
|
||||
this.host.journalAllocated?.(name, "dyn.dynComputedKeyGetHelper", []);
|
||||
const stringHelper = this.dynKeyGetHelper();
|
||||
host.declare(`declare ptr @scr_dyn_property_key_value(ptr)`);
|
||||
host.declare(`declare ptr @scr_dyn_symbol_key_get(ptr, ptr, i1 zeroext)`);
|
||||
@@ -3732,6 +3881,7 @@ export class LlDyn {
|
||||
"}",
|
||||
"",
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -3741,6 +3891,7 @@ export class LlDyn {
|
||||
if (existing) return existing;
|
||||
const name = "sc_dyn_key_get";
|
||||
this.dynBuilders.set(memoKey, name);
|
||||
this.host.journalAllocated?.(name, "dyn.dynKeyGetHelper", []);
|
||||
const host = this.host;
|
||||
const B = new BlockBuilder();
|
||||
const kd = this.kindOf(B, "%d");
|
||||
@@ -4134,6 +4285,7 @@ export class LlDyn {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -4145,6 +4297,7 @@ export class LlDyn {
|
||||
if (existing) return existing;
|
||||
const name = "sc_dyn_destr_check";
|
||||
this.dynBuilders.set(memoKey, name);
|
||||
this.host.journalAllocated?.(name, "dyn.dynDestrCheckHelper", []);
|
||||
const host = this.host;
|
||||
host.declare(`declare void @scr_jb_init(ptr)`);
|
||||
host.declare(`declare ptr @scr_jb_finish(ptr)`);
|
||||
@@ -4199,6 +4352,7 @@ export class LlDyn {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -4210,6 +4364,7 @@ export class LlDyn {
|
||||
if (existing) return existing;
|
||||
const name = "sc_dyn_iter_n";
|
||||
this.dynBuilders.set(memoKey, name);
|
||||
this.host.journalAllocated?.(name, "dyn.dynIterNHelper", []);
|
||||
const host = this.host;
|
||||
host.declare(`declare ptr @scr_dyn_new_arr()`);
|
||||
host.declare(`declare void @scr_dyn_arr_push(ptr, ptr)`);
|
||||
@@ -4414,6 +4569,7 @@ export class LlDyn {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -4422,6 +4578,31 @@ export class LlDyn {
|
||||
/** The dyn argument spelling of one static value: dyn passes through
|
||||
* (+1), funcs box anonymously, everything else rides toDyn. `expr` is
|
||||
* BORROWED in every arm. Emits into the given builder. */
|
||||
/** A live capsule of a record or array value (+1; the value is
|
||||
* borrowed), as `dynFrom` with `liveRef` builds it; null for other
|
||||
* types. Closures boxed in a declaration-typed boundary program return
|
||||
* their records and arrays this way. */
|
||||
private liveToDyn(B: BlockBuilder, t: IrType, expr: string): string | null {
|
||||
const host = this.host;
|
||||
if (t.kind === "union") {
|
||||
const arms = host.unionsById.get(t.unionId)?.arms ?? [];
|
||||
if (!arms.some((arm) => arm.kind === "record" || arm.kind === "array")) return null;
|
||||
const boxed = B.tmp();
|
||||
B.line(`${boxed} = call ptr @${host.liveDynUnionRefAdapter(t)}(ptr ${expr})`);
|
||||
return boxed;
|
||||
}
|
||||
if ((t.kind !== "record" && t.kind !== "array") || !streamTypedRefEligible(t)) return null;
|
||||
const key = typeKey(t);
|
||||
const adapter = host.liveDynRefAdapter(t);
|
||||
const rc = vAdapters(host, t);
|
||||
const boxed = B.tmp();
|
||||
B.line(
|
||||
`${boxed} = call ptr ${typedRefConstructor(host, t)}(ptr ${expr}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
|
||||
);
|
||||
for (const line of bindLiveArrayOps(host, adapter, boxed)) B.line(line);
|
||||
return boxed;
|
||||
}
|
||||
|
||||
private toDynExpr(B: BlockBuilder, t: IrType, expr: string): string {
|
||||
if (t.kind === "dyn") return this.retainDyn(B, expr);
|
||||
if (t.kind === "func") {
|
||||
@@ -4445,6 +4626,7 @@ export class LlDyn {
|
||||
if (existing) return existing;
|
||||
const name = `sc_dfk_${this.dynFuncThunks.size}`;
|
||||
this.dynFuncThunks.set(key, name);
|
||||
this.host.journalAllocated?.(name, "dyn.dynFuncThunkHelper", [t]);
|
||||
const host = this.host;
|
||||
const B = new BlockBuilder();
|
||||
const argNames: string[] = [];
|
||||
@@ -4552,6 +4734,26 @@ export class LlDyn {
|
||||
},
|
||||
"ptr null",
|
||||
);
|
||||
// Declaration-typed boundary programs: an argument package code
|
||||
// passes keeps its identity (scr_dyn_decl_adopt).
|
||||
const capsule = this.host.declBoundary?.() === true ? this.liveToDyn(B, p, a) : null;
|
||||
if (capsule !== null) {
|
||||
host.declare(`declare void @scr_dyn_decl_adopt(ptr, ptr)`);
|
||||
host.declare(`declare void @scr_dyn_release(ptr)`);
|
||||
B.line(`call void @scr_dyn_decl_adopt(ptr ${ad}, ptr ${capsule})`);
|
||||
B.line(`call void @scr_dyn_release(ptr ${capsule})`);
|
||||
this.pendingBail(
|
||||
B,
|
||||
"dfk.adopt",
|
||||
() => {
|
||||
t.params.slice(0, i + 1).forEach((q, j) => {
|
||||
const name = j === i ? a : argNames[j];
|
||||
if (isRefCounted(q)) B.line(`call void ${releaseSym(host, q)}(ptr ${name})`);
|
||||
});
|
||||
},
|
||||
"ptr null",
|
||||
);
|
||||
}
|
||||
argNames.push(a);
|
||||
}
|
||||
});
|
||||
@@ -4584,7 +4786,10 @@ export class LlDyn {
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ${retTy} ${fn}(${callArgs})`);
|
||||
this.pendingBail(B, "dfkr", () => {}, "ptr null");
|
||||
const out = this.toDynExpr(B, t.ret, r);
|
||||
const out =
|
||||
this.host.declBoundary?.() === true
|
||||
? (this.liveToDyn(B, t.ret, r) ?? this.toDynExpr(B, t.ret, r))
|
||||
: this.toDynExpr(B, t.ret, r);
|
||||
if (isRefCounted(t.ret)) B.line(`call void ${releaseSym(host, t.ret)}(ptr ${r})`);
|
||||
B.terminate(`ret ptr ${out}`);
|
||||
}
|
||||
@@ -4594,6 +4799,7 @@ export class LlDyn {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -4604,6 +4810,7 @@ export class LlDyn {
|
||||
if (existing) return existing;
|
||||
const name = `sc_dfb_${this.dynFuncBoxes.size}`;
|
||||
this.dynFuncBoxes.set(key, name);
|
||||
this.host.journalAllocated?.(name, "dyn.dynFuncBoxHelper", [t]);
|
||||
const host = this.host;
|
||||
const thunk = this.dynFuncThunkHelper(t);
|
||||
host.declare(`declare ptr @scr_closure_retain_v(ptr)`);
|
||||
@@ -4618,6 +4825,7 @@ export class LlDyn {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -4640,11 +4848,15 @@ export class LlDyn {
|
||||
(t.ret.kind === "void" || t.ret.kind === "f64" || t.ret.kind === "bool") &&
|
||||
this.messageReadable(param, new Set());
|
||||
if (!supported) {
|
||||
// A signature without an entry still takes a number.
|
||||
this.messageListeners.set(key, null);
|
||||
this.host.journalAllocated?.("", "dyn.messageListenerHelper", [t]);
|
||||
this.host.journalBuilt?.();
|
||||
return null;
|
||||
}
|
||||
const name = `sc_dml_${this.messageListeners.size}`;
|
||||
this.messageListeners.set(key, name);
|
||||
this.host.journalAllocated?.(name, "dyn.messageListenerHelper", [t]);
|
||||
const B = new BlockBuilder();
|
||||
B.entryAllocas.push(`%fail = alloca i1`);
|
||||
B.line(`store i1 false, ptr %fail`);
|
||||
@@ -4676,6 +4888,7 @@ export class LlDyn {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -4737,6 +4950,7 @@ export class LlDyn {
|
||||
if (existing) return existing;
|
||||
const name = `sc_dmr_${this.messageReaders.size}`;
|
||||
this.messageReaders.set(key, name);
|
||||
this.host.journalAllocated?.(name, "dyn.messageReaderHelper", [t]);
|
||||
const host = this.host;
|
||||
const S = host.sizeType;
|
||||
if (t.kind !== "record") host.declare(`declare i32 @scr_message_kind(ptr, ${S})`);
|
||||
@@ -4909,6 +5123,7 @@ export class LlDyn {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -4920,6 +5135,7 @@ export class LlDyn {
|
||||
if (existing) return existing;
|
||||
const name = `sc_dfa_${this.dynFuncAdapters.size}`;
|
||||
this.dynFuncAdapters.set(key, name);
|
||||
this.host.journalAllocated?.(name, "dyn.dynFuncAdapterHelper", [t]);
|
||||
const host = this.host;
|
||||
const B = new BlockBuilder();
|
||||
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
|
||||
@@ -5022,6 +5238,7 @@ export class LlDyn {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@ import {
|
||||
withInitializerBindings,
|
||||
withInitializerLocals,
|
||||
} from "../../ir/initializer-bindings.js";
|
||||
import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js";
|
||||
import { constructorClass } from "../../ir/final-fields.js";
|
||||
import { findInitializedByteLoopBindings } from "./byte-windows.js";
|
||||
import { findLoopArrayBorrows } from "./loop-array-borrows.js";
|
||||
import { findCallArrayReads, findLocalArrayReads } from "./local-array-reads.js";
|
||||
import { findScalarStringSlices, findScratchStringSlices } from "./string-slices.js";
|
||||
@@ -55,11 +53,15 @@ import { INT32_ABI_SUFFIX, SCALAR_UNION_RESULT } from "./int32-abi.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { RESULT_ENTRY_PREFIX } from "./record-results.js";
|
||||
import { emitIntegerClone, emitIntegerDispatcher } from "./integer-clones.js";
|
||||
import { beginFunctionLoops, LOOPS } from "./loop-state.js";
|
||||
import { runningExtremumUpdates } from "./running-extrema.js";
|
||||
import { findIntegerOrNaNLocals } from "./wrapping-sums.js";
|
||||
|
||||
export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
const B = new BlockBuilder();
|
||||
emitter.B = B;
|
||||
emitter.currentFunctionName = fn.name;
|
||||
emitter.findInt32Allocations(fn);
|
||||
emitter.currentTier = fn.tier;
|
||||
// Fiber switches restore the active exception cell before control
|
||||
// returns to a synchronous frame (emitPendingCheck).
|
||||
@@ -81,6 +83,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();
|
||||
@@ -99,7 +102,6 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
const initializerBindings = emitter.initializerBindings.get(fn.name) ?? [];
|
||||
const numericFn = withInitializerBindings(fn, initializerBindings);
|
||||
emitter.numericLocals = new Map(numericFn.locals.map((l) => [l.id, l]));
|
||||
emitter.unaliasedArrays = findUnaliasedLocalArrays(fn);
|
||||
emitter.borrowedParameters.clear();
|
||||
emitter.referenceWrites?.enter(fn);
|
||||
emitter.reboundOwnerSlots.clear();
|
||||
@@ -132,30 +134,18 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.integerLoopBindings.clear();
|
||||
emitter.integerTemps.clear();
|
||||
emitter.integerTempsEnabled = emitter.debug === null && !fn.async && !fn.generator;
|
||||
emitter.countedLoopsEnabled = emitter.debug === null && !fn.async && !fn.generator;
|
||||
emitter.speculativeLoopsEnabled = emitter.countedLoopsEnabled;
|
||||
beginFunctionLoops(emitter, fn, numericFn);
|
||||
emitter.speculativeLoopsEnabled = LOOPS.of(emitter).countedLoopsEnabled;
|
||||
emitter.speculativeFunction = fn.name;
|
||||
emitter.speculation = null;
|
||||
emitter.speculationResume = null;
|
||||
emitter.int32Kinds = null;
|
||||
emitter.int32KindResume = null;
|
||||
emitter.finalFieldBorrows =
|
||||
!fn.async &&
|
||||
!fn.generator &&
|
||||
constructorClass(fn.name) === null &&
|
||||
emitter.finalFields.size > 0;
|
||||
emitter.byteWindowsEnabled = emitter.countedLoopsEnabled;
|
||||
emitter.bytesLoopVersionsEnabled = emitter.countedLoopsEnabled;
|
||||
emitter.bytesLoopEntries = null;
|
||||
emitter.bytesLoopFunction = fn;
|
||||
emitter.denseLoopsEnabled = emitter.countedLoopsEnabled;
|
||||
emitter.denseLoop = null;
|
||||
emitter.denseLoopEntries =
|
||||
emitter.denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === "array")
|
||||
? findInitializedByteLoopBindings(fn)
|
||||
: new Map();
|
||||
emitter.byteWindowEntries =
|
||||
emitter.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes")
|
||||
? findInitializedByteLoopBindings(fn)
|
||||
: new Map();
|
||||
emitter.integerViews.clear();
|
||||
emitter.fieldPointerTags.clear();
|
||||
emitter.compressedFieldPtrs.clear();
|
||||
@@ -231,6 +221,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.integerRanges = rangeFacts.ranges;
|
||||
emitter.integerLoopRanges = rangeFacts.loopRanges;
|
||||
emitter.int32Elements = slotFacts.elements ?? null;
|
||||
emitter.integerOrNaNLocals = findIntegerOrNaNLocals(emitter, numericFn);
|
||||
emitter.bytesBounds = findBytesBounds(numericFn, emitter.integerRanges);
|
||||
emitter.bytesOwned = new Set();
|
||||
// Debug builds keep every number local a double for the debugger's
|
||||
@@ -413,7 +404,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
if (local.boxed || emitter.captureIds.has(local.id)) slots.add(`%${mangleLocal(local.id)}`);
|
||||
B.volatileSlots = slots;
|
||||
// Without optimization a versioned copy only adds code.
|
||||
emitter.bytesLoopVersionsEnabled = false;
|
||||
LOOPS.of(emitter).bytesLoopVersionsEnabled = false;
|
||||
}
|
||||
// Captured bindings come in through the environment — borrowed for the
|
||||
// whole call (the closure owns them): bound here, never released here.
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
/* Chunked function emission (emit-module.ts). An emitter that emits one
|
||||
* contiguous range of a module's functions records here the helpers and
|
||||
* sites it numbers from a counter (`sc_td_${size}` and the like). The
|
||||
* emitter that merges the ranges replays each range's records in order:
|
||||
* replay calls the same entry point with the same arguments, so every
|
||||
* helper gets the number and the definition the serial emission gives it,
|
||||
* and the range's text is renamed to those numbers. */
|
||||
import type { LlEmitter } from "./emitter.js";
|
||||
|
||||
/** One event of a range's emission, in order. */
|
||||
export type JournalEntry =
|
||||
/** A helper or site the range's emitter numbered: the name it gave it
|
||||
* (a counter prefix and a number), and the replay entry point
|
||||
* (registerReplay) with its arguments, which are plain IR values. */
|
||||
| {
|
||||
readonly kind: "op";
|
||||
readonly name: string;
|
||||
readonly op: string;
|
||||
readonly args: readonly unknown[];
|
||||
}
|
||||
/** The first request of a key of a module-level table made outside any
|
||||
* replayable definition, with the value the range's emitter stored. */
|
||||
| {
|
||||
readonly kind: "entry";
|
||||
readonly table: string;
|
||||
readonly key: string;
|
||||
readonly value: unknown;
|
||||
};
|
||||
|
||||
export class EmitJournal {
|
||||
readonly entries: JournalEntry[] = [];
|
||||
/** The replayable definitions being built, innermost last. */
|
||||
private readonly open: string[] = [];
|
||||
|
||||
/** The definitions still open (empty between functions). */
|
||||
get unclosed(): readonly string[] {
|
||||
return this.open;
|
||||
}
|
||||
/** Keys already recorded per table. */
|
||||
private readonly recorded = new Map<string, Set<string>>();
|
||||
|
||||
/** A keyed helper, recorded when its number is taken, before anything its
|
||||
* definition creates in turn: that keeps the serial numbering order.
|
||||
* Replaying a helper that exists answers its name, so helpers created
|
||||
* inside another helper's definition are recorded too. Opens the
|
||||
* definition, which the creating entry point closes with `built`. */
|
||||
allocated(name: string, op: string, args: readonly unknown[]): void {
|
||||
this.entries.push({ kind: "op", name, op, args });
|
||||
this.open.push(op);
|
||||
}
|
||||
|
||||
/** Opens a definition whose helper is not replayed itself (its
|
||||
* entry point records the request with `used`). */
|
||||
opened(op: string): void {
|
||||
this.open.push(op);
|
||||
}
|
||||
|
||||
/** A request, made outside any definition, of the helper `name` that an
|
||||
* entry point answered; recorded once per name. Replay makes the same
|
||||
* request. */
|
||||
used(name: string, op: string, args: readonly unknown[]): void {
|
||||
if (this.open.length !== 0) return;
|
||||
if (this.usedNames.has(name)) return;
|
||||
this.usedNames.add(name);
|
||||
this.entries.push({ kind: "op", name, op, args });
|
||||
}
|
||||
private readonly usedNames = new Set<string>();
|
||||
|
||||
/** Closes the definition `allocated` or `opened` opened. */
|
||||
built(): void {
|
||||
if (this.open.pop() === undefined) this.overclosed = true;
|
||||
}
|
||||
|
||||
/** A `built` closed a definition no `allocated` opened. */
|
||||
overclosed = false;
|
||||
|
||||
/** An unkeyed site (one per use). Sites taken while a definition is built
|
||||
* are taken again when the merging emitter replays that definition. */
|
||||
site(name: string, op: string, args: readonly unknown[] = []): void {
|
||||
if (this.open.length === 0) this.entries.push({ kind: "op", name, op, args });
|
||||
}
|
||||
|
||||
/** A request of `key` in a module-level table. Only the first request
|
||||
* made outside any definition is recorded: requests made while a
|
||||
* definition is built happen again when it is replayed. */
|
||||
entry(table: string, key: string, value: unknown): void {
|
||||
if (this.open.length !== 0) return;
|
||||
let keys = this.recorded.get(table);
|
||||
if (keys === undefined) this.recorded.set(table, (keys = new Set()));
|
||||
if (keys.has(key)) return;
|
||||
keys.add(key);
|
||||
this.entries.push({ kind: "entry", table, key, value });
|
||||
}
|
||||
}
|
||||
|
||||
/** A range emitter's result (LlEmitter.chunkResult): plain data, so a
|
||||
* worker process can write it. */
|
||||
export interface ChunkResult {
|
||||
readonly entries: readonly JournalEntry[];
|
||||
readonly flags: {
|
||||
readonly usesCoroutines: boolean;
|
||||
readonly usesTimers: boolean;
|
||||
readonly usesGenSlots: boolean;
|
||||
readonly needsUnionNone: boolean;
|
||||
readonly needsNullableAbsent: boolean;
|
||||
readonly needsOom: boolean;
|
||||
readonly needsBadTag: boolean;
|
||||
readonly needsBadKey: boolean;
|
||||
readonly needsRetainBox: boolean;
|
||||
};
|
||||
readonly publishedNamesSym: string | null;
|
||||
readonly rcHelpers: readonly string[];
|
||||
readonly immortalValues: readonly string[];
|
||||
readonly fnValues: readonly string[];
|
||||
readonly arraySearchKinds: readonly string[];
|
||||
readonly recordCloneShapes: readonly string[];
|
||||
readonly awaitCallees: readonly string[];
|
||||
readonly adoptCallees: readonly string[];
|
||||
readonly directAwaitCallees: readonly string[];
|
||||
readonly inlineAwaitCallees: readonly string[];
|
||||
}
|
||||
|
||||
type Replay = (emitter: LlEmitter, args: readonly unknown[]) => string;
|
||||
const replays = new Map<string, Replay>();
|
||||
|
||||
/** Registers the entry point that creates (or finds) a numbered helper or
|
||||
* site from its recorded arguments and answers its name. */
|
||||
export function registerReplay(op: string, replay: Replay): void {
|
||||
replays.set(op, replay);
|
||||
}
|
||||
|
||||
/** Replays a range's entries on the merging emitter, in record order, and
|
||||
* records the renames from the range's names to its own in `names`. */
|
||||
export function replayJournal(
|
||||
emitter: LlEmitter,
|
||||
entries: readonly JournalEntry[],
|
||||
names: Map<string, string>,
|
||||
tags: Map<number, number>,
|
||||
): void {
|
||||
for (const entry of entries) {
|
||||
if (entry.kind === "entry") {
|
||||
emitter.applyJournalEntry(entry.table, entry.key, entry.value, names, tags);
|
||||
continue;
|
||||
}
|
||||
const replay = replays.get(entry.op);
|
||||
if (replay === undefined)
|
||||
throw new Error(`llvm emitter bug: no replay for journal entry ${entry.op}`);
|
||||
const own = stripSigil(replay(emitter, entry.args));
|
||||
const local = stripSigil(entry.name);
|
||||
if (own !== local) names.set(local, own);
|
||||
}
|
||||
}
|
||||
|
||||
/** The rename of a range's text: numbered names (`names`, without the
|
||||
* sigil) and field alias tags (`tags`). One scan for each kind of name:
|
||||
* a numbered name starts `sc_` after a character that cannot be part of a
|
||||
* name, and ends at the last digit after its counter prefix. */
|
||||
export function renameChunk(
|
||||
names: ReadonlyMap<string, string>,
|
||||
tags: ReadonlyMap<number, number>,
|
||||
): (text: string) => string {
|
||||
const prefixes = new Set<string>();
|
||||
for (const name of names.keys()) prefixes.add(name.replace(/\d+$/, ""));
|
||||
if (prefixes.size === 0 && tags.size === 0) return (text) => text;
|
||||
return (text) => {
|
||||
let out = prefixes.size === 0 ? text : renameNames(text, names, prefixes);
|
||||
if (tags.size > 0) out = renameTags(out, tags);
|
||||
return out;
|
||||
};
|
||||
}
|
||||
|
||||
function renameNames(
|
||||
text: string,
|
||||
names: ReadonlyMap<string, string>,
|
||||
prefixes: ReadonlySet<string>,
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
let copied = 0;
|
||||
for (let at = text.indexOf("sc_"); at >= 0; at = text.indexOf("sc_", at + 3)) {
|
||||
if (at > 0 && isNameChar(text.charCodeAt(at - 1))) continue;
|
||||
// The counter prefix ends where the digits start; the number ends at
|
||||
// the last digit.
|
||||
let end = at + 3;
|
||||
while (end < text.length && isNameChar(text.charCodeAt(end)) && !isDigit(text.charCodeAt(end)))
|
||||
end++;
|
||||
if (end === text.length || !isDigit(text.charCodeAt(end))) continue;
|
||||
if (!prefixes.has(text.slice(at, end))) continue;
|
||||
let last = end;
|
||||
while (last < text.length && isDigit(text.charCodeAt(last))) last++;
|
||||
const own = names.get(text.slice(at, last));
|
||||
if (own === undefined) continue;
|
||||
parts.push(text.slice(copied, at), own);
|
||||
copied = last;
|
||||
}
|
||||
if (copied === 0) return text;
|
||||
parts.push(text.slice(copied));
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
function renameTags(text: string, tags: ReadonlyMap<number, number>): string {
|
||||
const marker = "!tbaa !";
|
||||
const parts: string[] = [];
|
||||
let copied = 0;
|
||||
for (let at = text.indexOf(marker); at >= 0; at = text.indexOf(marker, at + marker.length)) {
|
||||
const start = at + marker.length;
|
||||
let end = start;
|
||||
while (end < text.length && isDigit(text.charCodeAt(end))) end++;
|
||||
const own = tags.get(Number(text.slice(start, end)));
|
||||
if (own === undefined) continue;
|
||||
parts.push(text.slice(copied, start), String(own));
|
||||
copied = end;
|
||||
}
|
||||
if (copied === 0) return text;
|
||||
parts.push(text.slice(copied));
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
/** `[-$._A-Za-z0-9]`: a character of an LLVM identifier. */
|
||||
function isNameChar(code: number): boolean {
|
||||
return (
|
||||
(code >= 48 && code <= 57) ||
|
||||
(code >= 65 && code <= 90) ||
|
||||
(code >= 97 && code <= 122) ||
|
||||
code === 95 ||
|
||||
code === 36 ||
|
||||
code === 45 ||
|
||||
code === 46
|
||||
);
|
||||
}
|
||||
|
||||
function isDigit(code: number): boolean {
|
||||
return code >= 48 && code <= 57;
|
||||
}
|
||||
|
||||
function stripSigil(name: string): string {
|
||||
return name.startsWith("@") ? name.slice(1) : name;
|
||||
}
|
||||
@@ -2,6 +2,10 @@
|
||||
import { relieveHeapPressure } from "../../heap-pressure.js";
|
||||
import { emitWasmModules } from "./wasm-modules.js";
|
||||
import { type LlEmitter, ffiCallbackDummyLl, llStrBytes } from "./emitter.js";
|
||||
import { type ChunkResult, EmitJournal } from "./emit-journal.js";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { JS_IC_WORDS } from "./js-ic.js";
|
||||
import {
|
||||
mangleAsyncSpawn,
|
||||
@@ -24,7 +28,7 @@ import {
|
||||
} from "../../ir/ir.js";
|
||||
import {
|
||||
FN_ATTRS,
|
||||
limitInlineDynReleases,
|
||||
limitInlineReleases,
|
||||
releaseSym,
|
||||
typedRefConstructor,
|
||||
vAdapters,
|
||||
@@ -61,16 +65,150 @@ import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js";
|
||||
import { emitLibraryIdentityLines } from "../library-identity-markers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
|
||||
/** The function bodies emitted in `chunks` contiguous ranges, each by its
|
||||
* own emitter (LlEmitter.chunkEmitter), then merged in function order: the
|
||||
* text one emitter gives. */
|
||||
function emitFunctionsInChunks(emitter: LlEmitter, chunks: number): string[] {
|
||||
const functions = emitter.mod.functions;
|
||||
const size = Math.ceil(functions.length / chunks);
|
||||
const ranges: { chunk: LlEmitter; texts: string[] }[] = [];
|
||||
for (let start = 0; start < functions.length; start += size) {
|
||||
const chunk = emitter.chunkEmitter();
|
||||
const texts = functions.slice(start, start + size).map((fn) => {
|
||||
const text = chunk.emitFunction(fn);
|
||||
if (chunk.journal!.overclosed)
|
||||
throw new Error(`llvm emitter bug: journal definition closed twice in ${fn.name}`);
|
||||
if (chunk.journal!.unclosed.length > 0)
|
||||
throw new Error(
|
||||
`llvm emitter bug: unclosed journal definitions after ${fn.name}: ${chunk.journal!.unclosed.join(", ")}`,
|
||||
);
|
||||
return text;
|
||||
});
|
||||
ranges.push({ chunk, texts });
|
||||
}
|
||||
const fnDefs: string[] = [];
|
||||
for (const { chunk, texts } of ranges) {
|
||||
// The plain-data result a worker process would write, round-tripped.
|
||||
const result = JSON.parse(JSON.stringify(chunk.chunkResult())) as ChunkResult;
|
||||
const rename = emitter.mergeChunk(result);
|
||||
for (const text of texts) {
|
||||
const def = rename(text);
|
||||
emitter.fnDefText.set(functions[fnDefs.length]!.name, def);
|
||||
fnDefs.push(def);
|
||||
}
|
||||
}
|
||||
return fnDefs;
|
||||
}
|
||||
|
||||
/** Modules with fewer functions emit in one process: forking and merging
|
||||
* would cost more than the workers save (SCRIPTC_EMIT_WORKER_FUNCTIONS
|
||||
* lowers it, for tests). */
|
||||
function workerMinFunctions(): number {
|
||||
const value = Number(process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] ?? "2000");
|
||||
return Number.isFinite(value) && value > 0 ? value : 2000;
|
||||
}
|
||||
|
||||
/** The function bodies emitted on forked worker processes, `workers` at a
|
||||
* time: the function list splits into contiguous ranges, each emitted in
|
||||
* its own process, which inherits this emitter as it is at the fork and
|
||||
* records a journal (emit-journal.ts). Ranges merge in function order, so
|
||||
* the text is the serial text. A range whose worker fails is emitted here
|
||||
* when its turn comes. Null without fork primitives (Node, Windows, inside
|
||||
* a worker) or when the module is small. */
|
||||
function emitFunctionsOnWorkers(emitter: LlEmitter, workers: number): string[] | null {
|
||||
const fork = emitter.targetOptions.forkPrimitives;
|
||||
const functions = emitter.mod.functions;
|
||||
if (fork === undefined || workers < 2) return null;
|
||||
if (functions.length < workerMinFunctions()) return null;
|
||||
// More ranges than workers: a range's merge overlaps the later ranges'
|
||||
// emission, and a slow range holds back less.
|
||||
const count = Math.min(functions.length, workers * 3);
|
||||
const size = Math.ceil(functions.length / count);
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-emit-"));
|
||||
const pids: number[] = [];
|
||||
const start = (range: number): void => {
|
||||
const pid = fork.fork();
|
||||
if (pid === 0) {
|
||||
let code = 1;
|
||||
try {
|
||||
emitter.journal = new EmitJournal();
|
||||
const texts: string[] = [];
|
||||
for (const fn of functions.slice(range * size, (range + 1) * size)) {
|
||||
texts.push(emitter.emitFunction(fn));
|
||||
relieveHeapPressure();
|
||||
}
|
||||
writeFileSync(join(dir, `${range}.json`), JSON.stringify(emitter.chunkResult()));
|
||||
// Function text never holds NUL, so it separates the functions.
|
||||
writeFileSync(join(dir, `${range}.ll`), texts.join("\0"));
|
||||
code = 0;
|
||||
} catch {
|
||||
// The parent emits the range itself.
|
||||
}
|
||||
fork.exit(code);
|
||||
}
|
||||
pids.push(pid);
|
||||
};
|
||||
const fnDefs: string[] = [];
|
||||
try {
|
||||
for (let range = 0; range < Math.min(workers, count); range++) start(range);
|
||||
for (let range = 0; range < count; range++) {
|
||||
const pid = pids[range]!;
|
||||
const status = pid > 0 ? fork.wait(pid) : -1;
|
||||
// The next worker starts from this emitter as merged so far, which
|
||||
// the journal allows: it records requests, not table contents.
|
||||
const merged = status === 0 ? readRange(dir, range) : null;
|
||||
const slice = functions.slice(range * size, (range + 1) * size);
|
||||
if (merged !== null && merged.texts.length === slice.length) {
|
||||
const rename = emitter.mergeChunk(merged.result);
|
||||
slice.forEach((fn, index) => {
|
||||
const def = rename(merged.texts[index]!);
|
||||
emitter.fnDefText.set(fn.name, def);
|
||||
fnDefs.push(def);
|
||||
});
|
||||
} else
|
||||
for (const fn of slice) {
|
||||
const def = emitter.emitFunction(fn);
|
||||
emitter.fnDefText.set(fn.name, def);
|
||||
fnDefs.push(def);
|
||||
relieveHeapPressure();
|
||||
}
|
||||
if (pids.length < count) start(pids.length);
|
||||
}
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
return fnDefs;
|
||||
}
|
||||
|
||||
/** A worker's range result, or null when its files are missing. */
|
||||
function readRange(dir: string, range: number): { result: ChunkResult; texts: string[] } | null {
|
||||
try {
|
||||
const result = JSON.parse(readFileSync(join(dir, `${range}.json`), "utf8")) as ChunkResult;
|
||||
const texts = readFileSync(join(dir, `${range}.ll`), "utf8").split("\0");
|
||||
return { result, texts };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
// Function bodies first (the literal/unit/fn-value tables fill as they
|
||||
// emit), then the file assembles around them — the runtime ABI’s order.
|
||||
const fnDefs: string[] = [];
|
||||
for (const fn of emitter.mod.functions) {
|
||||
const def = emitter.emitFunction(fn);
|
||||
fnDefs.push(def);
|
||||
emitter.fnDefText.set(fn.name, def);
|
||||
relieveHeapPressure();
|
||||
}
|
||||
// Debug metadata is numbered per emitter and not journaled, so a debug
|
||||
// build emits every body here.
|
||||
const ranges = emitter.debug === null;
|
||||
const chunks = emitter.targetOptions.functionChunks ?? 1;
|
||||
const fnDefs =
|
||||
(ranges && chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ??
|
||||
(ranges ? emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) : null) ??
|
||||
[];
|
||||
if (fnDefs.length === 0)
|
||||
for (const fn of emitter.mod.functions) {
|
||||
const def = emitter.emitFunction(fn);
|
||||
fnDefs.push(def);
|
||||
emitter.fnDefText.set(fn.name, def);
|
||||
relieveHeapPressure();
|
||||
}
|
||||
emitter.selectStacklessAsync(fnDefs);
|
||||
const errorViews: string[] = [];
|
||||
for (const property of ["name", "message"])
|
||||
@@ -709,21 +847,19 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
for (const code of regexMatchers.values()) out.push(...code.lines, ``);
|
||||
for (const [, inst] of emitter.templateStringsInstances) {
|
||||
// One immortal ScrArr per tagged-template site: a data global of
|
||||
// interned cooked-string literals followed by their states (the
|
||||
// runtime finds them before data + cap * 9), and the ScrArr header over
|
||||
// it (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2, no REF
|
||||
// entry points). Every dense slot is present; reads retain immortal
|
||||
// strings — a no-op. Slots are 8 bytes on every target: on 32-bit
|
||||
// targets each holds the pointer in its low half and zero in its high
|
||||
// half, so the states start where the runtime looks for them.
|
||||
// interned cooked-string literals, and the ScrArr header over it
|
||||
// (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2, no REF entry
|
||||
// points, no state bytes: every dense slot is present). Reads retain
|
||||
// immortal strings — a no-op. Slots are 8 bytes on every target: on
|
||||
// 32-bit targets each holds the pointer in its low half and zero in its
|
||||
// high half.
|
||||
const n = inst.slots.length;
|
||||
const present = n === 0 ? "zeroinitializer" : `[ ${inst.slots.map(() => "i8 1").join(", ")} ]`;
|
||||
const wide = emitter.sizeType === "i64";
|
||||
const slot = wide ? "ptr" : "<{ ptr, i32 }>";
|
||||
const slotValue = (s: string): string =>
|
||||
wide ? `ptr ${s}` : `<{ ptr, i32 }> <{ ptr ${s}, i32 0 }>`;
|
||||
out.push(
|
||||
`@${inst.sym}_data = internal constant <{ [${n} x ${slot}], [${n} x i8] }> <{ [${n} x ${slot}] [ ${inst.slots.map(slotValue).join(", ")} ], [${n} x i8] ${present} }>, align 8`,
|
||||
`@${inst.sym}_data = internal constant [${n} x ${slot}] [ ${inst.slots.map(slotValue).join(", ")} ], align 8`,
|
||||
`@${inst.sym} = internal global %ScrArr { ${emitter.sizeType} -1, i32 ${n}, i32 ${n}, i8 2, i8 0, i16 0, i32 ${n}, ptr @${inst.sym}_data }`,
|
||||
);
|
||||
}
|
||||
@@ -737,6 +873,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
out.push(`@${sym} = internal constant { ptr, ptr, ptr, ptr, ptr, ptr, ptr } { ${body} }`);
|
||||
}
|
||||
if (emitter.mapOpsConsts.size > 0) out.push(``);
|
||||
for (const sym of emitter.methodIdSlots.values()) out.push(`@${sym} = internal global i32 -1`);
|
||||
if (emitter.methodIdSlots.size > 0) out.push(``);
|
||||
|
||||
for (const line of ffiCallbacks.globals) out.push(line);
|
||||
if (ffiCallbacks.globals.length > 0) out.push(``);
|
||||
@@ -2333,7 +2471,7 @@ export function emitFfiCallbackDefs(emitter: LlEmitter): { globals: string[]; de
|
||||
|
||||
export function emitParts(emitter: LlEmitter): string[] {
|
||||
const parts = [
|
||||
...limitInlineDynReleases(emitter.emitModuleParts()),
|
||||
...limitInlineReleases(emitter.emitModuleParts(), emitter.shapeHost.rcMode),
|
||||
...emitter.disposeObserverGates(),
|
||||
];
|
||||
if (!emitter.executableTls) return parts;
|
||||
|
||||
@@ -32,12 +32,19 @@ import { allocateLocalUnion, storeLocalUnion } from "./local-union-storage.js";
|
||||
import { boxAccess, boxNewCall, elemAccess } from "./shapes.js";
|
||||
import { dynHoleRead } from "./loop-array-borrows.js";
|
||||
import { endsWithJump, matchStringSelfConcat } from "../../ir/analysis.js";
|
||||
import { emitDiscardedSplice } from "./expr-containers.js";
|
||||
import { charAtUnitRead, emitDiscardedSplice } from "./expr-containers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { emitDenseArrayGet, emitDenseArrayPop, emitDenseArraySet } from "./dense-array-access.js";
|
||||
import {
|
||||
emitDenseArrayGet,
|
||||
emitDenseArrayPop,
|
||||
emitDenseArraySet,
|
||||
SCR_ELEM_I32,
|
||||
} 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 { f64Lit, llvmCommentText, loadArrCount, SCR_ARR_FIELD, sizeToDouble } from "./common.js";
|
||||
import { planInt32KindLoop, type Int32KindLoopPlan } from "./int32-kind-loops.js";
|
||||
import { withinInt32 } from "../../ir/integer-ranges.js";
|
||||
import { resultSlotStore } from "../result-slots.js";
|
||||
import {
|
||||
matchIntegerArrayForLoop,
|
||||
matchIntegerBytesForLoop,
|
||||
@@ -76,6 +83,7 @@ import {
|
||||
stringParts,
|
||||
} from "./string-construction.js";
|
||||
import { emitStringInputs } from "./string-lifetimes.js";
|
||||
import { LOOPS } from "./loop-state.js";
|
||||
|
||||
/** A callback loop version copies the loop; keep the copies small. */
|
||||
const MAX_CALLBACK_VERSION_COST = 400;
|
||||
@@ -410,7 +418,21 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
const v = emitter.emitExpr(s.value);
|
||||
if (s.arr.type.kind !== "array")
|
||||
throw new InternalCompilerError("llvm emitter bug: arraySet on non-array");
|
||||
const acc = elemAccess(s.arr.type.elem);
|
||||
if (emitter.checkedInt32Array(s.arr)) {
|
||||
// An int32 array with checked stores: the int32 arm stores an int32
|
||||
// value, and widens the array for any other first.
|
||||
emitter.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name);
|
||||
const elem = s.arr.type.elem;
|
||||
const proven = emitter.provenDenseIndex(s.arr, s.index);
|
||||
emitter.emitByElementKind(arr.name, null, (acc, widened) => {
|
||||
if (acc === "i32") emitter.emitCheckedInt32Store(arr.name, v.name, s.value, widened);
|
||||
emitDenseArraySet(emitter, arr.name, idx, s.index, acc, elem, v.name, proven);
|
||||
return null;
|
||||
});
|
||||
break;
|
||||
}
|
||||
const acc = emitter.arrayAccess(s.arr, s.arr.type.elem);
|
||||
const int32 = emitter.emitInt32KindStoreCheck(s.arr, v, s.value);
|
||||
emitter.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name);
|
||||
if (acc === "ref") emitter.toHeap(v);
|
||||
emitDenseArraySet(
|
||||
@@ -422,6 +444,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
s.arr.type.elem,
|
||||
v.name,
|
||||
emitter.provenDenseIndex(s.arr, s.index),
|
||||
int32,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -439,6 +462,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
const length = emitter.emitExpr(s.length);
|
||||
if (s.arr.type.kind !== "array")
|
||||
throw new InternalCompilerError("llvm emitter bug: arraySetLength on non-array");
|
||||
if (s.reserve === true) {
|
||||
emitter.declare(`declare void @scr_arr_reserve_len(ptr, double)`);
|
||||
B.line(`call void @scr_arr_reserve_len(ptr ${arr.name}, double ${length.name})`);
|
||||
break;
|
||||
}
|
||||
emitter.emitPublishedArrayGuard(arr.name, s.arr.type, s.pop === true ? 2 : 3);
|
||||
emitter.declare(`declare void @scr_arr_set_len(ptr, double)`);
|
||||
// Assigning the current length (a reused work array reset to where
|
||||
@@ -472,12 +500,19 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.condBr(dropsLast, pop, call);
|
||||
B.startBlock(pop);
|
||||
const elem = s.arr.type.elem;
|
||||
const acc = elemAccess(elem);
|
||||
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`);
|
||||
emitter.releaseHeap(old, elem);
|
||||
if (emitter.checkedInt32Array(s.arr)) {
|
||||
emitter.emitByElementKind(arr.name, null, (acc) => {
|
||||
emitDenseArrayPop(emitter, arr.name, acc, elem, call, call);
|
||||
return null;
|
||||
});
|
||||
} else {
|
||||
const acc = emitter.arrayAccess(s.arr, elem);
|
||||
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`);
|
||||
emitter.releaseHeap(old, elem);
|
||||
}
|
||||
}
|
||||
B.br(done);
|
||||
B.startBlock(call);
|
||||
@@ -573,13 +608,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 +815,12 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
const trueLabel = B.newLabel("if.t");
|
||||
const joinLabel = B.newLabel("if.j");
|
||||
const falseLabel = s.else_ ? B.newLabel("if.f") : joinLabel;
|
||||
B.condBr(cond, trueLabel, falseLabel);
|
||||
// A running maximum or minimum is rarely updated; the weights keep
|
||||
// the branch, where a select would make each element wait on the last.
|
||||
const weights = emitter.runningExtrema.has(s)
|
||||
? `, !prof !{!"branch_weights", i32 1, i32 1000}`
|
||||
: "";
|
||||
B.condBr(cond, trueLabel, falseLabel, weights);
|
||||
B.startBlock(trueLabel);
|
||||
emitter.emitBlock(s.then);
|
||||
B.br(joinLabel);
|
||||
@@ -805,9 +839,9 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
break;
|
||||
}
|
||||
const version =
|
||||
emitter.bytesLoopVersionsEnabled &&
|
||||
emitter.bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH &&
|
||||
!emitter.bytesLoopVersioning.has(s)
|
||||
LOOPS.of(emitter).bytesLoopVersionsEnabled &&
|
||||
LOOPS.of(emitter).bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH &&
|
||||
!LOOPS.of(emitter).bytesLoopVersioning.has(s)
|
||||
? emitter.matchBytesLoopVersion(s, null)
|
||||
: null;
|
||||
if (version) {
|
||||
@@ -896,33 +930,33 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
integerArrayLoop ??
|
||||
(emitter.debug === null ? matchIntegerBytesForLoop(s, emitter.numericLocals) : null);
|
||||
const countedLoop =
|
||||
!integerLoop && emitter.countedLoopsEnabled
|
||||
!integerLoop && LOOPS.of(emitter).countedLoopsEnabled
|
||||
? matchIntegerCountedForLoop(
|
||||
s,
|
||||
emitter.numericLocals,
|
||||
emitter.integerRanges,
|
||||
emitter.unaliasedArrays,
|
||||
LOOPS.of(emitter).unaliasedArrays,
|
||||
)
|
||||
: null;
|
||||
// Two counters closing in on each other (`lo++, hi--`) take the same
|
||||
// integer storage as a counted loop's one.
|
||||
const pairedLoop =
|
||||
!integerLoop && !countedLoop && emitter.countedLoopsEnabled
|
||||
!integerLoop && !countedLoop && LOOPS.of(emitter).countedLoopsEnabled
|
||||
? matchIntegerPairedForLoop(s, emitter.numericLocals, emitter.integerRanges)
|
||||
: null;
|
||||
// Byte windows and dense loops reason about one counter.
|
||||
const window =
|
||||
countedLoop && countedLoop.followers.length === 0 && emitter.byteWindowsEnabled
|
||||
countedLoop && countedLoop.followers.length === 0 && LOOPS.of(emitter).byteWindowsEnabled
|
||||
? matchByteWindow(
|
||||
s,
|
||||
countedLoop,
|
||||
emitter.numericLocals,
|
||||
emitter.captureIds,
|
||||
emitter.integerRanges,
|
||||
emitter.byteWindowEntries.get(s) ?? new Set(),
|
||||
LOOPS.of(emitter).byteWindowEntries.get(s) ?? new Set(),
|
||||
)
|
||||
: null;
|
||||
const denseShape = !emitter.denseLoopsEnabled
|
||||
const denseShape = !LOOPS.of(emitter).denseLoopsEnabled
|
||||
? null
|
||||
: integerArrayLoop
|
||||
? {
|
||||
@@ -939,8 +973,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
s,
|
||||
denseShape,
|
||||
emitter.numericLocals,
|
||||
emitter.unaliasedArrays,
|
||||
emitter.denseLoopEntries.get(s) ?? new Set(),
|
||||
LOOPS.of(emitter).unaliasedArrays,
|
||||
LOOPS.of(emitter).denseLoopEntries.get(s) ?? new Set(),
|
||||
)
|
||||
: null;
|
||||
if (dense) {
|
||||
@@ -949,18 +983,18 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
slow = B.newLabel("dense.loop.slow"),
|
||||
done = B.newLabel("dense.loop.done");
|
||||
B.condBr(valid, fast, slow);
|
||||
emitter.denseLoopsEnabled = false;
|
||||
LOOPS.of(emitter).denseLoopsEnabled = false;
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
emitter.denseLoop = dense;
|
||||
LOOPS.of(emitter).denseLoop = dense;
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
} finally {
|
||||
emitter.denseLoop = null;
|
||||
emitter.denseLoopsEnabled = true;
|
||||
LOOPS.of(emitter).denseLoop = null;
|
||||
LOOPS.of(emitter).denseLoopsEnabled = true;
|
||||
}
|
||||
B.startBlock(done);
|
||||
break;
|
||||
@@ -973,7 +1007,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.condBr(valid, fast, slow);
|
||||
const originalRanges = emitter.integerRanges,
|
||||
originalBounds = emitter.bytesBounds;
|
||||
emitter.byteWindowsEnabled = false;
|
||||
LOOPS.of(emitter).byteWindowsEnabled = false;
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
@@ -986,27 +1020,27 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
} finally {
|
||||
emitter.integerRanges = originalRanges;
|
||||
emitter.bytesBounds = originalBounds;
|
||||
emitter.byteWindowsEnabled = true;
|
||||
LOOPS.of(emitter).byteWindowsEnabled = true;
|
||||
}
|
||||
B.startBlock(done);
|
||||
break;
|
||||
}
|
||||
const versionable =
|
||||
emitter.bytesLoopVersionsEnabled &&
|
||||
emitter.bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH &&
|
||||
!emitter.bytesLoopVersioning.has(s);
|
||||
LOOPS.of(emitter).bytesLoopVersionsEnabled &&
|
||||
LOOPS.of(emitter).bytesLoopVersioning.size < MAX_BYTES_LOOP_VERSION_DEPTH &&
|
||||
!LOOPS.of(emitter).bytesLoopVersioning.has(s);
|
||||
// A loop to a typed array's or an array's length keeps its integer
|
||||
// induction. Its counted form only gives the version guard the
|
||||
// counter's iteration space, so the guard also covers accesses of
|
||||
// other typed arrays at the counter, such as `x[i]` and `y[i]` in
|
||||
// `for (let i = 0; i < out.length; i++) out[i] = x[i] * y[i]`.
|
||||
const versionCounted =
|
||||
versionable && !countedLoop && integerLoop && emitter.countedLoopsEnabled
|
||||
versionable && !countedLoop && integerLoop && LOOPS.of(emitter).countedLoopsEnabled
|
||||
? matchIntegerCountedForLoop(
|
||||
s,
|
||||
emitter.numericLocals,
|
||||
emitter.integerRanges,
|
||||
emitter.unaliasedArrays,
|
||||
LOOPS.of(emitter).unaliasedArrays,
|
||||
)
|
||||
: countedLoop;
|
||||
const version = versionable ? emitter.matchBytesLoopVersion(s, versionCounted) : null;
|
||||
@@ -1015,7 +1049,10 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
break;
|
||||
}
|
||||
let countedJoin: string | null = null;
|
||||
if ((countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) || pairedLoop?.guarded) {
|
||||
if (
|
||||
(countedLoop?.guarded && !LOOPS.of(emitter).provenCountedGuards.has(s)) ||
|
||||
pairedLoop?.guarded
|
||||
) {
|
||||
const safe = countedLoop
|
||||
? emitCountedLoopSafety(emitter, countedLoop)
|
||||
: emitPairedLoopSafety(emitter, pairedLoop!);
|
||||
@@ -1024,9 +1061,9 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
countedJoin = B.newLabel("counted.done");
|
||||
B.condBr(safe, fast, slow);
|
||||
B.startBlock(slow);
|
||||
emitter.countedLoopsEnabled = false;
|
||||
LOOPS.of(emitter).countedLoopsEnabled = false;
|
||||
emitter.emitStmt(s);
|
||||
emitter.countedLoopsEnabled = true;
|
||||
LOOPS.of(emitter).countedLoopsEnabled = true;
|
||||
B.br(countedJoin);
|
||||
B.startBlock(fast);
|
||||
}
|
||||
@@ -1056,8 +1093,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
slot: integerSlot,
|
||||
type: emitter.sizeType,
|
||||
range:
|
||||
emitter.denseLoop?.counter === integerLoop.localId
|
||||
? emitter.denseLoop.range
|
||||
LOOPS.of(emitter).denseLoop?.counter === integerLoop.localId
|
||||
? LOOPS.of(emitter).denseLoop!.range
|
||||
: { min: 0, max: integerArrayLoop ? 4294967294 : Number.MAX_SAFE_INTEGER - 1 },
|
||||
});
|
||||
} else if (countedLoop) {
|
||||
@@ -1074,8 +1111,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
type: "i64",
|
||||
signed: true,
|
||||
range:
|
||||
emitter.denseLoop?.counter === countedLoop.localId
|
||||
? emitter.denseLoop.range
|
||||
LOOPS.of(emitter).denseLoop?.counter === countedLoop.localId
|
||||
? LOOPS.of(emitter).denseLoop!.range
|
||||
: countedLoop.range,
|
||||
});
|
||||
for (const follower of countedLoop.followers) {
|
||||
@@ -1818,7 +1855,7 @@ export function emitTryCatch(emitter: LlEmitter, s: IrStmt & { kind: "tryCatch"
|
||||
* case, refcounted/boxed case-body locals are NULL-reset up front and
|
||||
* the scope-exit releases rely on NULL tolerance. */
|
||||
export function emitSwitch(emitter: LlEmitter, s: IrStmt & { kind: "switch" }): void {
|
||||
const unitSwitch = charAtUnitSwitch(s);
|
||||
const unitSwitch = charAtUnitSwitch(emitter, s);
|
||||
if (unitSwitch !== null) return emitSwitch(emitter, unitSwitch);
|
||||
const B = emitter.B;
|
||||
const discKind = s.disc.type.kind;
|
||||
@@ -2038,7 +2075,7 @@ function emitVersionedLoop(
|
||||
const originalBounds = emitter.bytesBounds,
|
||||
originalOwned = emitter.bytesOwned,
|
||||
originalRanges = emitter.integerRanges;
|
||||
emitter.bytesLoopVersioning.add(s);
|
||||
LOOPS.of(emitter).bytesLoopVersioning.add(s);
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitter.emitStmt(s);
|
||||
@@ -2048,15 +2085,15 @@ function emitVersionedLoop(
|
||||
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);
|
||||
if (version.counted?.guarded) LOOPS.of(emitter).provenCountedGuards.add(s);
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
} finally {
|
||||
emitter.bytesBounds = originalBounds;
|
||||
emitter.bytesOwned = originalOwned;
|
||||
emitter.integerRanges = originalRanges;
|
||||
emitter.provenCountedGuards.delete(s);
|
||||
emitter.bytesLoopVersioning.delete(s);
|
||||
LOOPS.of(emitter).provenCountedGuards.delete(s);
|
||||
LOOPS.of(emitter).bytesLoopVersioning.delete(s);
|
||||
}
|
||||
B.startBlock(done);
|
||||
}
|
||||
@@ -2090,9 +2127,26 @@ export function emitLoopCopies(
|
||||
)
|
||||
: null;
|
||||
if (!plan) {
|
||||
const proven = allowed ? planProvenLoop(emitter, loop) : null;
|
||||
if (proven) emitProvenLoop(emitter, proven, exit, emitCopy);
|
||||
else emitCopy(exit);
|
||||
const ordinary = (exit: string): void => {
|
||||
const proven = allowed ? planProvenLoop(emitter, loop) : null;
|
||||
if (proven) emitProvenLoop(emitter, proven, exit, emitCopy);
|
||||
else emitCopy(exit);
|
||||
};
|
||||
const kinds =
|
||||
allowed &&
|
||||
emitter.speculativeLoopsEnabled &&
|
||||
emitter.int32Slots.checkedInt32Arrays.size > 0 &&
|
||||
emitter.int32Kinds === null &&
|
||||
emitter.speculation === null &&
|
||||
emitter.speculationResume === null
|
||||
? planInt32KindLoop(
|
||||
loop,
|
||||
(id) => emitter.checkedInt32Local(id),
|
||||
(e) => withinInt32(emitter.integerRanges.get(e)),
|
||||
)
|
||||
: null;
|
||||
if (kinds) emitInt32KindLoop(emitter, kinds, exit, kinds.writes.size > 0 ? emitCopy : ordinary);
|
||||
else ordinary(exit);
|
||||
return;
|
||||
}
|
||||
const B = emitter.B;
|
||||
@@ -2116,8 +2170,7 @@ export function emitLoopCopies(
|
||||
// count. After SPECULATION_BAILS bailing entries in a row, the site runs
|
||||
// the ordinary copy only. The count is a hint: both copies give the same
|
||||
// results, and a race between workers loses at most an update.
|
||||
const bails = `@scr.spec.bails.${emitter.speculativeLoopSites++}`;
|
||||
emitter.declare(`${bails} = internal global i8 0`);
|
||||
const bails = emitter.speculativeLoopSite();
|
||||
const count = B.tmp(),
|
||||
trying = B.tmp();
|
||||
B.line(`${count} = load atomic i8, ptr ${bails} unordered, align 1`);
|
||||
@@ -2236,6 +2289,124 @@ export function emitLoopCopies(
|
||||
}
|
||||
}
|
||||
|
||||
/** An int32 kind loop (int32-kind-loops.ts). The entry test reads the kind
|
||||
* of each of the plan's arrays: when every one holds int32 slots the fast
|
||||
* copy runs, when every one holds doubles the wide copy, which reads and
|
||||
* writes them as double arrays, and otherwise (a plan with several arrays)
|
||||
* the mixed copy, which tests their kinds at each access. A failed store of
|
||||
* the fast copy re-executes in the third copy, which finishes its
|
||||
* iteration with kind tests and then enters the mixed copy, or the wide
|
||||
* copy when the plan has one array (it just widened). Each copy is emitted
|
||||
* by `ordinary`: the loop's plain copy when a store may fail, since a
|
||||
* proven integer loop keeps locals in shadows of its own copy that another
|
||||
* copy cannot resume from. */
|
||||
function emitInt32KindLoop(
|
||||
emitter: LlEmitter,
|
||||
plan: Int32KindLoopPlan,
|
||||
exit: string,
|
||||
ordinary: (exit: string) => void,
|
||||
): void {
|
||||
const B = emitter.B;
|
||||
const fn = emitter.currentFunctionName;
|
||||
const keys = new Set([...plan.arrays.keys()].map((id) => `${fn}\0${id}`));
|
||||
const several = keys.size > 1;
|
||||
let allInt32 = "true",
|
||||
allWide = "true";
|
||||
const and = (all: string, next: string): string => {
|
||||
if (all === "true") return next;
|
||||
const both = B.tmp();
|
||||
B.line(`${both} = and i1 ${all}, ${next}`);
|
||||
return both;
|
||||
};
|
||||
for (const ref of plan.arrays.values()) {
|
||||
const arr = emitter.emitStableReceiver(ref, []);
|
||||
const kindPtr = B.tmp(),
|
||||
kind = B.tmp(),
|
||||
isInt32 = B.tmp();
|
||||
B.line(
|
||||
`${kindPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 ${SCR_ARR_FIELD.elem}`,
|
||||
);
|
||||
emitter.markMemoryPointer(kindPtr, "array:header");
|
||||
B.line(`${kind} = load i8, ptr ${kindPtr}${emitter.fieldAliasAttachment(kindPtr)}`);
|
||||
B.line(`${isInt32} = icmp eq i8 ${kind}, ${SCR_ELEM_I32}`);
|
||||
allInt32 = and(allInt32, isInt32);
|
||||
if (several) {
|
||||
const isWide = B.tmp();
|
||||
B.line(`${isWide} = xor i1 ${isInt32}, true`);
|
||||
allWide = and(allWide, isWide);
|
||||
}
|
||||
}
|
||||
const fast = B.newLabel("kind.fast"),
|
||||
wide = B.newLabel("kind.wide"),
|
||||
mixed = several ? B.newLabel("kind.mixed") : null;
|
||||
if (mixed === null) B.condBr(allInt32, fast, wide);
|
||||
else {
|
||||
const other = B.newLabel("kind.other");
|
||||
B.condBr(allInt32, fast, other);
|
||||
B.startBlock(other);
|
||||
B.condBr(allWide, wide, mixed);
|
||||
}
|
||||
const resume = new Map<IrStmt, { label: string; placed: boolean }>();
|
||||
for (const s of plan.writes) resume.set(s, { label: B.newLabel("kind.resume"), placed: false });
|
||||
// The header the third copy enters: the mixed copy's, or the wide copy's.
|
||||
let header: string | null = null;
|
||||
const capture = (label: string): void => {
|
||||
header = label;
|
||||
};
|
||||
B.startBlock(wide);
|
||||
if (resume.size > 0 && mixed === null) emitter.speculationHeader = capture;
|
||||
emitter.int32Kinds = { int32: new Set(), wide: keys, resume: new Map() };
|
||||
try {
|
||||
ordinary(exit);
|
||||
} finally {
|
||||
emitter.speculationHeader = null;
|
||||
emitter.int32Kinds = null;
|
||||
}
|
||||
if (mixed !== null) {
|
||||
B.startBlock(mixed);
|
||||
if (resume.size > 0) emitter.speculationHeader = capture;
|
||||
try {
|
||||
ordinary(exit);
|
||||
} finally {
|
||||
emitter.speculationHeader = null;
|
||||
}
|
||||
}
|
||||
if (resume.size > 0) {
|
||||
if (header === null)
|
||||
throw new InternalCompilerError("llvm emitter bug: int32 kind loop copy without a header");
|
||||
// A failed store finishes its iteration here, as in a speculative
|
||||
// integer loop's third copy.
|
||||
B.startBlock(B.newLabel("kind.tail"));
|
||||
emitter.speculationResume = resume;
|
||||
emitter.speculationHeader = () => {
|
||||
B.br(header!);
|
||||
B.startBlock(B.newLabel("kind.unused"));
|
||||
};
|
||||
try {
|
||||
ordinary(exit);
|
||||
} finally {
|
||||
emitter.speculationResume = null;
|
||||
emitter.speculationHeader = null;
|
||||
}
|
||||
}
|
||||
B.startBlock(fast);
|
||||
if (![...resume.values()].every((r) => r.placed)) {
|
||||
// A write the third copy never reached: this copy tests the kinds.
|
||||
ordinary(exit);
|
||||
return;
|
||||
}
|
||||
emitter.int32Kinds = {
|
||||
int32: keys,
|
||||
wide: new Set(),
|
||||
resume: new Map([...resume].map(([s, r]) => [s, r.label])),
|
||||
};
|
||||
try {
|
||||
ordinary(exit);
|
||||
} finally {
|
||||
emitter.int32Kinds = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** At the top of a loop copy's header: lets a speculative loop find its
|
||||
* ordinary copy's header, and keeps its third copy out of the loop. */
|
||||
function enterLoopHeader(emitter: LlEmitter, header: string): void {
|
||||
@@ -2511,7 +2682,10 @@ function nullableStringSwitch(
|
||||
* one-unit string literals, as a switch on `s.charCodeAt(i)` with the units
|
||||
* as cases: no character string is built. An index out of range answers
|
||||
* NaN, which matches no case, as "" matches no one-unit literal. */
|
||||
function charAtUnitSwitch(s: IrStmt & { kind: "switch" }): (IrStmt & { kind: "switch" }) | null {
|
||||
function charAtUnitSwitch(
|
||||
emitter: LlEmitter,
|
||||
s: IrStmt & { kind: "switch" },
|
||||
): (IrStmt & { kind: "switch" }) | null {
|
||||
const disc = s.disc;
|
||||
if (disc.kind !== "strIntrinsic" || disc.method !== "charAt" || disc.args.length !== 1)
|
||||
return null;
|
||||
@@ -2524,7 +2698,7 @@ function charAtUnitSwitch(s: IrStmt & { kind: "switch" }): (IrStmt & { kind: "sw
|
||||
return null;
|
||||
return {
|
||||
...s,
|
||||
disc: { ...disc, method: "charCodeAt", type: F64 },
|
||||
disc: charAtUnitRead(emitter, disc),
|
||||
cases: s.cases.map((c) =>
|
||||
c.test?.kind === "strLit"
|
||||
? {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/* Per-feature emitter state. A feature defines its state in its own file
|
||||
* with defineModuleState and reads it through the returned key, so adding
|
||||
* or extending a feature does not edit LlEmitter's field list or
|
||||
* constructor. */
|
||||
import type { LlEmitter } from "./emitter.js";
|
||||
|
||||
/** A feature's state on an emitter, read with `key.of(emitter)`. */
|
||||
export interface EmitterStateKey<T> {
|
||||
of(emitter: LlEmitter): T;
|
||||
}
|
||||
|
||||
let moduleSlots = 0;
|
||||
|
||||
/** State that lives for the whole module, created on first read. */
|
||||
export function defineModuleState<T>(create: (emitter: LlEmitter) => T): EmitterStateKey<T> {
|
||||
const slot = moduleSlots++;
|
||||
return {
|
||||
of(emitter) {
|
||||
const states = emitter.moduleStates;
|
||||
if (slot >= states.length || states[slot] === undefined) states[slot] = create(emitter);
|
||||
return states[slot] as T;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type IrType,
|
||||
} from "../../ir/ir.js";
|
||||
import { emitLlvmModule, emitLlvmModuleSource, LlEmitter } from "./emitter.js";
|
||||
import { INLINE_STR_RELEASES_PER_FUNCTION } from "./shapes.js";
|
||||
|
||||
/** A pending-exception check: the inline active-cell test or a runtime call. */
|
||||
const PENDING_CHECK = /@scr_exc_(?:active|pending)\b/;
|
||||
@@ -201,17 +202,24 @@ test("detached exit defines the runtime opt-in symbol only when requested", () =
|
||||
expect(detached.replace("@scr_detached_exit = constant i8 1\n", "")).toBe(plain);
|
||||
});
|
||||
|
||||
test("inline RC inlines every fast path at speed and keeps string and box releases as calls in release", () => {
|
||||
test("inline RC inlines every fast path at speed and keeps box releases as calls in release", () => {
|
||||
for (const boxed of [false, true]) {
|
||||
const module = moduleFor(Array.from({ length: 4 }, call), { boxed });
|
||||
const release = emitLlvmModule(module);
|
||||
expect(release).not.toContain("@sc_rc_");
|
||||
expect(release).not.toContain("%ScrMapRc");
|
||||
// The release subset inlines string retains and dyn retains and releases
|
||||
// only; these string and box releases stay runtime calls.
|
||||
// The release subset inlines string and dyn retains and releases and Map
|
||||
// retains only: box releases stay runtime calls, and so do the string
|
||||
// releases of a function with more than INLINE_STR_RELEASES_PER_FUNCTION.
|
||||
const subset = emitLlvmModule(module, { inlineRc: "release" });
|
||||
expect(subset).not.toContain("@sc_rc_release_str");
|
||||
expect(subset).not.toContain("@sc_rc_release_box");
|
||||
expect(subset).not.toContain("call void @sc_rc_release_box(");
|
||||
const sites = (ir: string, fn: string): number => ir.split(`call void @${fn}(ptr`).length - 1;
|
||||
if (!boxed) {
|
||||
expect(sites(subset, "sc_rc_release_str")).toBe(0);
|
||||
// Every site calls the runtime, and so does the unused helper's slow path.
|
||||
expect(sites(subset, "scr_str_release")).toBe(sites(release, "scr_str_release") + 1);
|
||||
expect(sites(release, "scr_str_release")).toBeGreaterThan(INLINE_STR_RELEASES_PER_FUNCTION);
|
||||
}
|
||||
const speed = emitLlvmModule(module, { inlineRc: "speed" });
|
||||
const helper = boxed ? "sc_rc_release_box" : "sc_rc_release_str";
|
||||
const runtime = boxed ? "scr_box_release" : "scr_str_release";
|
||||
@@ -451,6 +459,33 @@ test("class brand probes borrow a local only across an inert literal key", () =>
|
||||
expect(computed).toContain("call ptr @scr_dyn_retain_v(");
|
||||
});
|
||||
|
||||
test("class constructor probes test the function tag and borrow across a literal key", () => {
|
||||
const dyn: IrType = { kind: "dyn" };
|
||||
const key: IrExpr = { kind: "strLit", value: "classval:Example", type: STRING, loc: receiverLoc };
|
||||
const probe: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "dyn.classIs",
|
||||
args: [ref(dyn), key],
|
||||
type: BOOL,
|
||||
loc: receiverLoc,
|
||||
};
|
||||
const borrowed = work(probe, dyn);
|
||||
expect(borrowed).not.toContain("call ptr @scr_dyn_retain_v(");
|
||||
expect(borrowed).toContain("icmp ne ptr");
|
||||
expect(borrowed).toContain("icmp eq i32 %t");
|
||||
expect(borrowed).toMatch(/icmp eq i32 %t\d+, 8\b/);
|
||||
expect(borrowed).toContain("call zeroext i1 @scr_dyn_class_is_key(");
|
||||
const computed = work(
|
||||
{
|
||||
...probe,
|
||||
args: [ref(dyn), { kind: "toString", operand: ref(dyn), type: STRING, loc: receiverLoc }],
|
||||
},
|
||||
dyn,
|
||||
);
|
||||
expect(computed).toContain("call ptr @scr_dyn_retain_v(");
|
||||
expect(computed).toContain("call zeroext i1 @scr_dyn_class_is_key(");
|
||||
});
|
||||
|
||||
test("class brand probes compare the program's key constant before the key text", () => {
|
||||
const dyn: IrType = { kind: "dyn" };
|
||||
const key: IrExpr = { kind: "strLit", value: "object:Example", type: STRING, loc: receiverLoc };
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ import { F64_INF, exactIndexLines, f64Lit, sizeToDouble } from "./common.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { byteNumberAccess } from "../../ir/byte-numbers.js";
|
||||
import { emitByteNumber } from "./byte-numbers.js";
|
||||
import { integerOrNaNRange } from "./wrapping-sums.js";
|
||||
import { integerOrNaNIndex, integerOrNaNRange } from "./wrapping-sums.js";
|
||||
import { bytesElementAlias, bytesHeaderAlias } from "./bytes-alias.js";
|
||||
|
||||
const BYTES_NUM_KIND: Record<string, { kind: number; le: boolean } | undefined> = {
|
||||
@@ -119,12 +119,28 @@ export function emitBytesIndex(
|
||||
B.line(`${narrow} = trunc i64 ${wideIndex} to ${host.sizeType}`);
|
||||
return narrow;
|
||||
}
|
||||
const exact = exactIndexLines(index, len, host.sizeType, () => B.tmp());
|
||||
for (const line of exact.lines) B.line(line);
|
||||
let ok: string;
|
||||
let wide: string;
|
||||
const nanIndex = integerOrNaNIndex(host, value, expr);
|
||||
if (nanIndex) {
|
||||
// An exact integer or NaN: in range when ordered and below the length.
|
||||
const wideLength = host.sizeType === "i64" ? len : B.tmp();
|
||||
if (host.sizeType !== "i64") B.line(`${wideLength} = zext ${host.sizeType} ${len} to i64`);
|
||||
const below = B.tmp();
|
||||
ok = B.tmp();
|
||||
B.line(`${below} = icmp ult i64 ${nanIndex.wide}, ${wideLength}`);
|
||||
B.line(`${ok} = and i1 ${nanIndex.ordered}, ${below}`);
|
||||
wide = nanIndex.wide;
|
||||
} else {
|
||||
const exact = exactIndexLines(index, len, host.sizeType, () => B.tmp());
|
||||
for (const line of exact.lines) B.line(line);
|
||||
ok = exact.ok;
|
||||
wide = exact.wide;
|
||||
}
|
||||
const invalid = B.newLabel("bytes.index.invalid");
|
||||
const valid = B.newLabel("bytes.index.valid");
|
||||
B.condBr(exact.ok, valid, invalid);
|
||||
let idx = exact.wide;
|
||||
B.condBr(ok, valid, invalid);
|
||||
let idx = wide;
|
||||
|
||||
B.startBlock(invalid);
|
||||
if (skipInvalid) B.br(skipInvalid);
|
||||
@@ -138,7 +154,7 @@ export function emitBytesIndex(
|
||||
B.startBlock(valid);
|
||||
if (host.sizeType !== "i64") {
|
||||
idx = B.tmp();
|
||||
B.line(`${idx} = trunc i64 ${exact.wide} to ${host.sizeType}`);
|
||||
B.line(`${idx} = trunc i64 ${wide} to ${host.sizeType}`);
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
@@ -455,11 +471,13 @@ export function emitToUint32(
|
||||
B.line(`${out} = select i1 ${ordered}, i32 ${low}, i32 0`);
|
||||
return out;
|
||||
}
|
||||
// Below 2^63 in magnitude, the i64 conversion truncates exactly (every
|
||||
// double above 2^53 is an integer), and its low 32 bits are ToUint32.
|
||||
const aboveMin = B.tmp();
|
||||
const belowMax = B.tmp();
|
||||
const fast = B.tmp();
|
||||
B.line(`${aboveMin} = fcmp oge double ${value}, ${f64Lit(-9007199254740992)}`);
|
||||
B.line(`${belowMax} = fcmp ole double ${value}, ${f64Lit(9007199254740992)}`);
|
||||
B.line(`${aboveMin} = fcmp oge double ${value}, ${f64Lit(-9223372036854775808)}`);
|
||||
B.line(`${belowMax} = fcmp olt double ${value}, ${f64Lit(9223372036854775808)}`);
|
||||
B.line(`${fast} = and i1 ${aboveMin}, ${belowMax}`);
|
||||
|
||||
const fastLabel = B.newLabel("uint32.coerce.fast");
|
||||
@@ -475,8 +493,8 @@ export function emitToUint32(
|
||||
B.br(done);
|
||||
|
||||
B.startBlock(slowLabel);
|
||||
// Only NaN, the infinities and magnitudes above 2^53 reach this block.
|
||||
// Such a finite double is the integer mantissa * 2^shift with shift >= 1,
|
||||
// Only NaN, the infinities and magnitudes from 2^63 reach this block.
|
||||
// Such a finite double is the integer mantissa * 2^shift with shift >= 11,
|
||||
// so its low 32 bits are the shifted mantissa's (none once shift >= 64),
|
||||
// negated for a negative value. NaN and the infinities have the maximum
|
||||
// exponent, a shift of 972, and convert to 0 like ToUint32. Integer
|
||||
@@ -1100,7 +1118,9 @@ export function emitInlineAtomicsOp(
|
||||
if (op !== 0 && op !== 1 && op !== 3 && ATOMIC_RMW[op] === undefined) return null;
|
||||
const elem = array.type.elem;
|
||||
const B = host.B;
|
||||
const receiver = host.emitExpr(array).name;
|
||||
// The instruction and scr_atomics_op borrow the array: an unchanged local
|
||||
// owns it across the later operands and the operation.
|
||||
const receiver = emitBorrowedInput(host, array).name;
|
||||
const idxValue = host.emitExpr(index);
|
||||
const operand = host.emitExpr(value);
|
||||
const swap = host.emitExpr(replacement);
|
||||
|
||||
@@ -14,6 +14,7 @@ import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { unionPeekLines, unionTagLines } from "./union-repr.js";
|
||||
import { inlineUnionCopyLines } from "./inline-unions.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
|
||||
/** Text-template union construction around an owned (+1) reference payload:
|
||||
* a fresh box, or the payload itself for a nullable union. */
|
||||
@@ -55,6 +56,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
|
||||
if (sym) return sym;
|
||||
sym = `sc_race_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "raceAdapterFor", [from, to]);
|
||||
const toArms = to.kind === "union" ? (host.unionsById.get(to.unionId)?.arms ?? []) : [];
|
||||
if (from.kind === "void" && !toArms.some((a) => a.kind === "undefinedT")) {
|
||||
// A Promise<never> entry (the frontend admits void entries without an
|
||||
@@ -70,6 +72,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
if (to.kind !== "union")
|
||||
@@ -98,6 +101,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
|
||||
const tag = tagOf(UNDEFINED_T);
|
||||
d.push(` ${fulfill(host.unitInstanceRef(to.unionId, tag))}`, ` ret void`, `}`, ``);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
if (from.kind !== "union") {
|
||||
@@ -138,6 +142,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
|
||||
}
|
||||
d.push(` ${fulfill("%u")}`, ` ret void`, `}`, ``);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
// Sub-union re-tag: switch over the entry's arms, rebuild under the
|
||||
@@ -200,6 +205,7 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
|
||||
``,
|
||||
);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -213,6 +219,7 @@ export function genResultThunkFor(
|
||||
if (sym) return sym;
|
||||
sym = mangleGenResThunk(host.resolveThunks.size);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "genResultThunkFor", [genT, recT]);
|
||||
const shape = host.recordsById.get(recT.shapeId);
|
||||
const base = host.nullableUnions.fieldBase(recT.shapeId);
|
||||
const valueT = shape?.fields.find((f) => f.name === "value")?.type;
|
||||
@@ -299,6 +306,7 @@ export function genResultThunkFor(
|
||||
``,
|
||||
];
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
if (valueT.kind !== "union") throw new LlvmUnsupportedError(`genResume:${valueT.kind}`);
|
||||
@@ -456,6 +464,7 @@ export function genResultThunkFor(
|
||||
``,
|
||||
);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -467,6 +476,7 @@ export function childExitThunkFor(host: LlvmEmitterContext, param: IrType): stri
|
||||
if (sym) return sym;
|
||||
sym = `sc_cx_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "childExitThunkFor", [param]);
|
||||
const def = host.unionsById.get(param.unionId);
|
||||
const f64Tag = def ? def.arms.findIndex((a) => a.kind === "f64") : -1;
|
||||
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
||||
@@ -494,6 +504,7 @@ export function childExitThunkFor(host: LlvmEmitterContext, param: IrType): stri
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -510,6 +521,7 @@ export function childExitSignalThunkFor(
|
||||
if (sym) return sym;
|
||||
sym = `sc_cx_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "childExitSignalThunkFor", [codeParam, sigParam]);
|
||||
const codeDef = host.unionsById.get(codeParam.unionId);
|
||||
const f64Tag = codeDef ? codeDef.arms.findIndex((a) => a.kind === "f64") : -1;
|
||||
const codeNullTag = codeDef ? codeDef.arms.findIndex((a) => a.kind === "nullT") : -1;
|
||||
@@ -560,6 +572,7 @@ export function childExitSignalThunkFor(
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -569,6 +582,7 @@ export function execFileThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
|
||||
if (sym) return sym;
|
||||
sym = `sc_exec_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "execFileThunkFor", [cbT]);
|
||||
const errorParam = cbT.params[0];
|
||||
let errorTag = -1;
|
||||
let nullTag = -1;
|
||||
@@ -627,6 +641,7 @@ export function execFileThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
|
||||
``,
|
||||
);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -639,6 +654,7 @@ export function ipcMessageThunkFor(
|
||||
if (sym) return sym;
|
||||
sym = `sc_ipc_msg_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "ipcMessageThunkFor", [cbT]);
|
||||
const param = cbT.params[0];
|
||||
const d: string[] = [
|
||||
`define internal void @${sym}(ptr %cb, ptr %message) ${FN_ATTRS} { ; IPC message ${typeKey(cbT)}`,
|
||||
@@ -679,6 +695,7 @@ export function ipcMessageThunkFor(
|
||||
}
|
||||
d.push(` ret void`, `}`, ``);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -688,6 +705,7 @@ export function ipcSendThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
|
||||
if (sym) return sym;
|
||||
sym = `sc_ipc_send_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "ipcSendThunkFor", [cbT]);
|
||||
const param = cbT.params[0];
|
||||
const d: string[] = [
|
||||
`define internal void @${sym}(ptr %cb, ptr %error) ${FN_ATTRS} { ; IPC send callback ${typeKey(cbT)}`,
|
||||
@@ -736,6 +754,7 @@ export function ipcSendThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
|
||||
}
|
||||
d.push(` ret void`, `}`, ``);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -747,6 +766,7 @@ export function childDataThunkFor(host: LlvmEmitterContext, param: IrType): stri
|
||||
if (sym) return sym;
|
||||
sym = `sc_cd_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "childDataThunkFor", [param]);
|
||||
const def = host.unionsById.get(param.unionId);
|
||||
const bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes" && a.elem === "u8") : -1;
|
||||
if (bytesTag < 0)
|
||||
@@ -768,6 +788,7 @@ export function childDataThunkFor(host: LlvmEmitterContext, param: IrType): stri
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -780,6 +801,7 @@ export function childDataDynThunkFor(host: LlvmEmitterContext, chunk: "bytes" |
|
||||
if (sym) return sym;
|
||||
sym = `sc_cdd_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "childDataDynThunkFor", [chunk]);
|
||||
const box = chunk === "bytes" ? "scr_dyn_new_buffer" : "scr_dyn_new_str";
|
||||
host.declare(`declare ptr @${box}(ptr)`);
|
||||
host.resolveThunkDefs.push(
|
||||
@@ -793,6 +815,7 @@ export function childDataDynThunkFor(host: LlvmEmitterContext, chunk: "bytes" |
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -813,6 +836,7 @@ export function emitterFixedAdapter(
|
||||
if (sym) return { fn: sym, shim };
|
||||
sym = `sc_ee_ad_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "emitterFixedAdapter", [cbT]);
|
||||
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
|
||||
host.declare(`declare void @scr_closure_release(ptr)`);
|
||||
const params = cursor ? "ptr %args" : cbT.params.map((_, i) => `ptr %a${i}`).join(", ");
|
||||
@@ -857,6 +881,7 @@ export function emitterFixedAdapter(
|
||||
}
|
||||
d.push(` call void @scr_closure_release(ptr %orig)`, ` ret void`, `}`, ``);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return { fn: sym, shim };
|
||||
}
|
||||
|
||||
@@ -882,6 +907,7 @@ export function emitterOnceAdapter(
|
||||
if (sym) return { fn: sym, shim };
|
||||
sym = `sc_ee_once_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "emitterOnceAdapter", [boxerT, fire]);
|
||||
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
|
||||
host.declare(`declare void @scr_closure_release(ptr)`);
|
||||
host.declare(`declare void @${fire}(ptr, ptr)`);
|
||||
@@ -921,6 +947,7 @@ export function emitterOnceAdapter(
|
||||
``,
|
||||
);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return { fn: sym, shim };
|
||||
}
|
||||
|
||||
@@ -990,6 +1017,7 @@ export function closeBindThunkFor(
|
||||
if (sym) return sym;
|
||||
sym = `sc_ncb_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "closeBindThunkFor", [cbUnion, retServer]);
|
||||
const def = host.unionsById.get(cbUnion.unionId);
|
||||
const funcTag = def ? def.arms.findIndex((a) => a.kind === "func") : -1;
|
||||
const funcArm = funcTag >= 0 ? (def!.arms[funcTag] as IrType & { kind: "func" }) : null;
|
||||
@@ -1079,6 +1107,7 @@ export function closeBindThunkFor(
|
||||
}
|
||||
d.push(`}`, ``);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -1094,6 +1123,7 @@ export function closeOverrideWrapFor(
|
||||
if (sym) return sym;
|
||||
sym = `sc_ncw_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "closeOverrideWrapFor", [cbUnion, retServer]);
|
||||
const def = host.unionsById.get(cbUnion.unionId);
|
||||
const undefTag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1;
|
||||
if (undefTag < 0)
|
||||
@@ -1122,6 +1152,7 @@ export function closeOverrideWrapFor(
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -1140,6 +1171,7 @@ export function settledStoreFor(
|
||||
if (sym) return sym;
|
||||
sym = `sc_settled_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "settledStoreFor", [from, to]);
|
||||
const def = host.unionsById.get(to.unionId);
|
||||
if (!def) throw new InternalCompilerError("llvm emitter bug: settled store to an unknown union");
|
||||
const armOf = (field: string): { tag: number; arm: IrType & { kind: "record" } } => {
|
||||
@@ -1230,5 +1262,52 @@ export function settledStoreFor(
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
registerReplay("raceAdapterFor", (host, args) =>
|
||||
raceAdapterFor(host, args[0] as IrType, args[1] as IrType),
|
||||
);
|
||||
registerReplay("genResultThunkFor", (host, args) =>
|
||||
genResultThunkFor(
|
||||
host,
|
||||
args[0] as IrType & { kind: "generator" },
|
||||
args[1] as IrType & { kind: "record" },
|
||||
),
|
||||
);
|
||||
registerReplay("childExitThunkFor", (host, args) => childExitThunkFor(host, args[0] as IrType));
|
||||
registerReplay("childExitSignalThunkFor", (host, args) =>
|
||||
childExitSignalThunkFor(host, args[0] as IrType, args[1] as IrType),
|
||||
);
|
||||
registerReplay("execFileThunkFor", (host, args) =>
|
||||
execFileThunkFor(host, args[0] as IrType & { kind: "func" }),
|
||||
);
|
||||
registerReplay("ipcMessageThunkFor", (host, args) =>
|
||||
ipcMessageThunkFor(host, args[0] as IrType & { kind: "func" }),
|
||||
);
|
||||
registerReplay("ipcSendThunkFor", (host, args) =>
|
||||
ipcSendThunkFor(host, args[0] as IrType & { kind: "func" }),
|
||||
);
|
||||
registerReplay("childDataThunkFor", (host, args) => childDataThunkFor(host, args[0] as IrType));
|
||||
registerReplay("childDataDynThunkFor", (host, args) =>
|
||||
childDataDynThunkFor(host, args[0] as "bytes" | "str"),
|
||||
);
|
||||
registerReplay("closeBindThunkFor", (host, args) =>
|
||||
closeBindThunkFor(host, args[0] as IrType, args[1] as boolean),
|
||||
);
|
||||
registerReplay("closeOverrideWrapFor", (host, args) =>
|
||||
closeOverrideWrapFor(host, args[0] as IrType, args[1] as boolean),
|
||||
);
|
||||
registerReplay("settledStoreFor", (host, args) =>
|
||||
settledStoreFor(host, args[0] as IrType, args[1] as IrType & { kind: "union" }),
|
||||
);
|
||||
registerReplay(
|
||||
"emitterFixedAdapter",
|
||||
(host, args) => emitterFixedAdapter(host, args[0] as IrType & { kind: "func" }).fn,
|
||||
);
|
||||
registerReplay(
|
||||
"emitterOnceAdapter",
|
||||
(host, args) =>
|
||||
emitterOnceAdapter(host, args[0] as IrType & { kind: "func" }, args[1] as string).fn,
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
classValueConstructor,
|
||||
type IrType,
|
||||
} from "../../ir/ir.js";
|
||||
import { liveRefView } from "./expr-stream-bridges.js";
|
||||
import { typedRefConstructor, vAdapters } from "./shapes.js";
|
||||
@@ -55,6 +56,7 @@ import {
|
||||
import type { Writes } from "./reference-writes.js";
|
||||
import { emitJsIcCall, jsIcSiteOf } from "./js-ic.js";
|
||||
import { emitClassMembership, emitDowncastGuard } from "./narrow-guards.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
|
||||
export function emitCallExpr(
|
||||
host: LlvmEmitterContext,
|
||||
@@ -815,22 +817,7 @@ export function emitCallExpr(
|
||||
const pushReceiver = (): void => {
|
||||
if (lazyReceiver) {
|
||||
const t = lazyReceiver.type;
|
||||
const key = typeKey(t);
|
||||
let recipe = host.typedRefRecipes.get(key);
|
||||
if (recipe === undefined) {
|
||||
const adapter = host.liveDynRefAdapter(t);
|
||||
const rc = vAdapters(host.shapeHost, t);
|
||||
const ctor = typedRefConstructor(host.shapeHost, t);
|
||||
const kind = ctor.endsWith("_traced") ? 2 : ctor.endsWith("_observed") ? 1 : 0;
|
||||
// The box binds the same live view eager boxing attaches, so
|
||||
// `in`, for-in and Object.keys see the class members.
|
||||
const view = liveRefView(adapter);
|
||||
recipe = `@sc_trr_${host.typedRefRecipes.size}`;
|
||||
host.typedRefRecipes.set(key, recipe);
|
||||
host.resolveThunkDefs.push(
|
||||
`${recipe} = internal constant { ptr, ptr, ptr, ${host.sizeType}, ptr, ptr, i32, i32, ptr } { ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit}, i32 ${kind}, i32 ${view?.code ?? 0}, ptr ${view?.ops ?? "null"} }`,
|
||||
);
|
||||
}
|
||||
const recipe = typedRefRecipe(host, t);
|
||||
host.declare("declare void @scr_dyn_this_push_typed(ptr, ptr)");
|
||||
B.line(`call void @scr_dyn_this_push_typed(ptr ${lazyReceiver.name}, ptr ${recipe})`);
|
||||
return;
|
||||
@@ -1493,3 +1480,28 @@ function emitNumericIndexRead(
|
||||
B.line(`${result} = phi ptr [ ${fast}, %${fastLabel} ], [ ${read.name}, %${slowLabel} ]`);
|
||||
return host.own({ name: result, type: e.type });
|
||||
}
|
||||
|
||||
/** The recipe a lazily boxed receiver of type `t` is built from: its RC
|
||||
* adapters, type key and live view, one per type. */
|
||||
function typedRefRecipe(host: LlvmEmitterContext, t: IrType): string {
|
||||
const key = typeKey(t);
|
||||
let recipe = host.typedRefRecipes.get(key);
|
||||
if (recipe !== undefined) return recipe;
|
||||
const adapter = host.liveDynRefAdapter(t);
|
||||
const rc = vAdapters(host.shapeHost, t);
|
||||
const ctor = typedRefConstructor(host.shapeHost, t);
|
||||
const kind = ctor.endsWith("_traced") ? 2 : ctor.endsWith("_observed") ? 1 : 0;
|
||||
// The box binds the same live view eager boxing attaches, so `in`,
|
||||
// for-in and Object.keys see the class members.
|
||||
const view = liveRefView(adapter);
|
||||
recipe = `@sc_trr_${host.typedRefRecipes.size}`;
|
||||
host.typedRefRecipes.set(key, recipe);
|
||||
host.journal?.allocated(recipe, "typedRefRecipe", [t]);
|
||||
host.resolveThunkDefs.push(
|
||||
`${recipe} = internal constant { ptr, ptr, ptr, ${host.sizeType}, ptr, ptr, i32, i32, ptr } { ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit}, i32 ${kind}, i32 ${view?.code ?? 0}, ptr ${view?.ops ?? "null"} }`,
|
||||
);
|
||||
host.journal?.built();
|
||||
return recipe;
|
||||
}
|
||||
|
||||
registerReplay("typedRefRecipe", (host, args) => typedRefRecipe(host, args[0] as IrType));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { arraySearchHelperName, arraySearchKind } from "./array-search.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { isStableReceiverOperand, undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { type IrExpr, type IrType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js";
|
||||
import { F64, type IrExpr, type IrType, isRefCounted, typeEquals, typeKey } from "../../ir/ir.js";
|
||||
import { mangleResolveThunk } from "../mangle.js";
|
||||
import {
|
||||
arrOps,
|
||||
@@ -51,6 +51,8 @@ import {
|
||||
STRING_CHAR_AT_INT_MARKER,
|
||||
STRING_CHAR_CODE_AT_DECL,
|
||||
STRING_CHAR_CODE_AT_INT_MARKER,
|
||||
emitAsciiChar,
|
||||
emitAsciiCharCode,
|
||||
STRING_UTF16_LEN_DECL,
|
||||
} from "./string-reads.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
@@ -58,8 +60,12 @@ import {
|
||||
emitDenseArrayNumber,
|
||||
emitDenseArrayPop,
|
||||
emitDenseArrayPush,
|
||||
runtimeAccess,
|
||||
type ArrayAccess,
|
||||
} from "./dense-array-access.js";
|
||||
import { INT32_ARRAY_METHODS } from "../../ir/int32-arrays.js";
|
||||
import { emitUnionWiden } from "./expr-records.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
|
||||
export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string {
|
||||
const key = typeKey(inner);
|
||||
@@ -67,6 +73,7 @@ export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string
|
||||
if (!sym) {
|
||||
sym = mangleResolveThunk(host.resolveThunks.size);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "resolveThunkFor", [inner]);
|
||||
const v = vAdapters(host.shapeHost, inner);
|
||||
host.declare(`declare void @scr_resolve_ref_impl(ptr, ptr, ptr, ptr, ptr)`);
|
||||
host.resolveThunkDefs.push(
|
||||
@@ -77,6 +84,7 @@ export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
}
|
||||
return sym;
|
||||
}
|
||||
@@ -110,7 +118,7 @@ export function tagInSet(
|
||||
export function arrPush(
|
||||
host: LlvmEmitterContext,
|
||||
arr: string,
|
||||
acc: "f64" | "bool" | "ref",
|
||||
acc: ArrayAccess,
|
||||
value: string,
|
||||
element: IrType | null,
|
||||
): string {
|
||||
@@ -121,7 +129,7 @@ export function arrPush(
|
||||
export function emitArrayValues(
|
||||
host: LlvmEmitterContext,
|
||||
arr: string,
|
||||
acc: "f64" | "bool" | "ref",
|
||||
acc: ArrayAccess,
|
||||
element: IrType,
|
||||
values: LlValue[],
|
||||
prepend = false,
|
||||
@@ -137,12 +145,15 @@ export function emitArrayValues(
|
||||
}
|
||||
if (values.length === 1) {
|
||||
if (!prepend) return arrPush(host, arr, acc, values[0]!.name, element);
|
||||
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
const flat = runtimeAccess(acc);
|
||||
const ty = flat === "f64" ? "double" : flat === "bool" ? "i1" : "ptr";
|
||||
host.declare(
|
||||
`declare double @scr_arr_unshift_${acc}(ptr, ${ty}${acc === "bool" ? " zeroext" : ""})`,
|
||||
`declare double @scr_arr_unshift_${flat}(ptr, ${ty}${flat === "bool" ? " zeroext" : ""})`,
|
||||
);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = call double @scr_arr_unshift_${acc}(ptr ${arr}, ${ty} ${values[0]!.name})`);
|
||||
B.line(
|
||||
`${result} = call double @scr_arr_unshift_${flat}(ptr ${arr}, ${ty} ${values[0]!.name})`,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
const capacity = Math.min(values.length, 64);
|
||||
@@ -160,8 +171,9 @@ export function emitArrayValues(
|
||||
const slot = B.tmp(),
|
||||
pointer = B.tmp();
|
||||
const value = values[start + i]!.name;
|
||||
// The runtime takes double bits for int32 arrays too, and converts.
|
||||
const pack =
|
||||
acc === "f64"
|
||||
acc === "f64" || acc === "i32"
|
||||
? `bitcast double ${value} to i64`
|
||||
: acc === "bool"
|
||||
? `zext i1 ${value} to i64`
|
||||
@@ -281,6 +293,33 @@ export function emitArrayCopyLoop(
|
||||
host.B.line(`call double @scr_arr_push_spread(ptr ${dst}, ptr ${src})`);
|
||||
}
|
||||
|
||||
/** `s.charCodeAt(i)` in place of `s.charAt(i)`, the same index read as a
|
||||
* code unit. The emitter records the charAt it stands for
|
||||
* (charAtUnitReads), so a loop version's proof for the charAt covers it. */
|
||||
export function charAtUnitRead(
|
||||
host: LlvmEmitterContext,
|
||||
charAt: IrExpr & { kind: "strIntrinsic" },
|
||||
): IrExpr & { kind: "strIntrinsic" } {
|
||||
const read: IrExpr & { kind: "strIntrinsic" } = {
|
||||
kind: "strIntrinsic",
|
||||
method: "charCodeAt",
|
||||
receiver: charAt.receiver,
|
||||
args: charAt.args,
|
||||
type: F64,
|
||||
loc: charAt.loc,
|
||||
};
|
||||
host.charAtUnitReads.set(read, charAt);
|
||||
return read;
|
||||
}
|
||||
|
||||
/** An enclosing loop version proved the string of `e` ASCII and its index
|
||||
* below the length (bytes-loop-versions.ts). */
|
||||
function provenStringRead(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
if (host.bytesBounds.has(e)) return true;
|
||||
const origin = host.charAtUnitReads.get(e);
|
||||
return origin !== undefined && host.bytesBounds.has(origin);
|
||||
}
|
||||
|
||||
export function emitStrIntrinsic(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "strIntrinsic" },
|
||||
@@ -326,8 +365,10 @@ export function emitStrIntrinsic(
|
||||
const t = B.tmp();
|
||||
const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null;
|
||||
if (index) {
|
||||
host.declare(STRING_CHAR_CODE_AT_INT_MARKER);
|
||||
const wide = widenInteger(host, index);
|
||||
if (provenStringRead(host, e))
|
||||
return { name: emitAsciiCharCode(host, r.name, wide), type: e.type };
|
||||
host.declare(STRING_CHAR_CODE_AT_INT_MARKER);
|
||||
B.line(`${t} = call double @sc_str_char_code_at_int(ptr ${r.name}, i64 ${wide})`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
@@ -340,8 +381,11 @@ export function emitStrIntrinsic(
|
||||
const t = B.tmp();
|
||||
const index = e.args[0] ? exactInteger(host, args[0]!, e.args[0]) : null;
|
||||
if (index) {
|
||||
host.declare(STRING_CHAR_AT_INT_MARKER);
|
||||
const wide = widenInteger(host, index);
|
||||
// The table entry is immortal: nothing owns it.
|
||||
if (provenStringRead(host, e))
|
||||
return host.markImmortal({ name: emitAsciiChar(host, r.name, wide), type: e.type });
|
||||
host.declare(STRING_CHAR_AT_INT_MARKER);
|
||||
B.line(`${t} = call ptr @sc_str_char_at_int(ptr ${r.name}, i64 ${wide})`);
|
||||
} else {
|
||||
const at = args[0]?.name ?? f64Lit(0);
|
||||
@@ -570,7 +614,15 @@ export function emitArrIntrinsic(
|
||||
if (e.receiver.type.kind !== "array")
|
||||
throw new InternalCompilerError("llvm emitter bug: arrIntrinsic on non-array");
|
||||
const elem = e.receiver.type.elem;
|
||||
const acc = elemAccess(elem);
|
||||
// Int32 arrays (ir/int32-arrays.ts) reach only these methods; the others
|
||||
// take the runtime family's flat access. One with checked stores tests
|
||||
// its kind at each inline access (emitByElementKind) and otherwise takes
|
||||
// the double entry points, which serve both kinds.
|
||||
const checked = host.checkedInt32Array(e.receiver);
|
||||
const access = checked ? "f64" : host.arrayAccess(e.receiver, elem);
|
||||
if ((access === "i32" || checked) && !INT32_ARRAY_METHODS.has(e.method))
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.method} on an int32 array`);
|
||||
const acc = runtimeAccess(access);
|
||||
const accTy = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
const accArg = acc === "bool" ? "i1 zeroext" : accTy;
|
||||
const method = e.method;
|
||||
@@ -591,6 +643,13 @@ export function emitArrIntrinsic(
|
||||
e.receiver.kind === "varRef"
|
||||
? host.constantNumericTables.get(e.receiver.localId)
|
||||
: undefined;
|
||||
if (!table && elem.kind === "f64" && checked) {
|
||||
const proven = host.provenDenseIndex(e.receiver, e.args[0]!);
|
||||
const value = host.emitByElementKind(r.name, "double", (kind) =>
|
||||
emitDenseArrayNumber(host, r.name, index, e.args[0], proven, kind),
|
||||
)!;
|
||||
return { name: value, type: e.type };
|
||||
}
|
||||
if (!table && elem.kind === "f64")
|
||||
return {
|
||||
name: emitDenseArrayNumber(
|
||||
@@ -599,6 +658,7 @@ export function emitArrIntrinsic(
|
||||
index,
|
||||
e.args[0],
|
||||
host.provenDenseIndex(e.receiver, e.args[0]!),
|
||||
access === "i32" ? "i32" : "f64",
|
||||
),
|
||||
type: e.type,
|
||||
};
|
||||
@@ -635,7 +695,22 @@ export function emitArrIntrinsic(
|
||||
// no-op zero-argument call.
|
||||
const vs = e.args.map((a) => host.emitExpr(a));
|
||||
host.emitPublishedArrayGuard(r.name, e.receiver.type, 1, f64Lit(0), f64Lit(vs.length));
|
||||
return { name: emitArrayValues(host, r.name, acc, elem, vs), type: e.type };
|
||||
if (checked && vs.length > 0) {
|
||||
// Each value appends in its own kind test: one that is not an int32
|
||||
// widens the array before it is pushed.
|
||||
let length = "";
|
||||
vs.forEach((v, i) => {
|
||||
length = host.emitByElementKind(r.name, "double", (kind, widened) => {
|
||||
if (kind === "i32") host.emitCheckedInt32Store(r.name, v.name, e.args[i]!, widened);
|
||||
return arrPush(host, r.name, kind, v.name, elem);
|
||||
})!;
|
||||
});
|
||||
return { name: length, type: e.type };
|
||||
}
|
||||
// In an int32 kind loop's fast copy, every value is checked before
|
||||
// the first appends.
|
||||
vs.forEach((v, i) => host.emitInt32KindStoreCheck(e.receiver, v, e.args[i]!));
|
||||
return { name: emitArrayValues(host, r.name, access, elem, vs), type: e.type };
|
||||
}
|
||||
case "pushSpread": {
|
||||
// `a.push(...src)`: append src's elements in order (borrowed src,
|
||||
@@ -709,7 +784,13 @@ export function emitArrIntrinsic(
|
||||
let slowLabel = "";
|
||||
if (method === "pop") {
|
||||
slowLabel = B.newLabel("remove.slow");
|
||||
denseRaw = emitDenseArrayPop(host, r.name, acc, elem, la, slowLabel);
|
||||
const empty = la,
|
||||
slow = slowLabel;
|
||||
denseRaw = checked
|
||||
? host.emitByElementKind(r.name, "i64", (kind) =>
|
||||
emitDenseArrayPop(host, r.name, kind, elem, empty, slow),
|
||||
)!
|
||||
: emitDenseArrayPop(host, r.name, access, elem, la, slowLabel);
|
||||
denseLabel = B.currentLabel();
|
||||
B.br(lp);
|
||||
B.startBlock(slowLabel);
|
||||
@@ -898,6 +979,14 @@ export function emitArrIntrinsic(
|
||||
case "fillUndefined": {
|
||||
const args = e.args.map((arg) => host.emitExpr(arg));
|
||||
host.emitPublishedArrayGuard(r.name, e.receiver.type, 7);
|
||||
if (host.checkedFills.has(e)) {
|
||||
// A new int32 array with checked stores, filled with a value that
|
||||
// may not be an int32: widen it first then.
|
||||
const filled = B.newLabel("arr.fill.kind");
|
||||
host.emitCheckedInt32Store(r.name, args[0]!.name, e.args[0]!, filled);
|
||||
B.br(filled);
|
||||
B.startBlock(filled);
|
||||
}
|
||||
const helper =
|
||||
e.method === "copyWithin"
|
||||
? "scr_arr_copy_within"
|
||||
@@ -1548,3 +1637,5 @@ export function emitSetNew(host: LlvmEmitterContext, e: IrExpr & { kind: "setNew
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
registerReplay("resolveThunkFor", (host, args) => resolveThunkFor(host, args[0] as IrType));
|
||||
|
||||
@@ -15,11 +15,23 @@ export interface LlValue {
|
||||
/** An exact integer snapshot, excluding negative zero, with no discarded
|
||||
* high bits and the proven range of its original value. */
|
||||
integer?: { name: string; type: "i32" | "i64"; signed: boolean; range: IntegerRange };
|
||||
/** An i64 holding the exact integer that the arithmetic producing this
|
||||
* number computes, when that integer has no proven safe range: its
|
||||
* absolute value is at most `bound`, and the double equals it whenever
|
||||
* its absolute value is below `below` (a zero may be -0). Index consumers
|
||||
* use it in place of a round trip (integer-values.ts `indexInteger`). */
|
||||
wideInteger?: WideInteger;
|
||||
slot?: boolean;
|
||||
/** A sequence local held in a capture box; release the box itself. */
|
||||
boxed?: boolean;
|
||||
}
|
||||
|
||||
export interface WideInteger {
|
||||
name: string;
|
||||
below: number;
|
||||
bound: number;
|
||||
}
|
||||
|
||||
export type ExprOf<K extends IrExpr["kind"]> = Extract<IrExpr, { kind: K }>;
|
||||
export type LibCallExpr = ExprOf<"libCall">;
|
||||
type LibCallPrefixOf<T extends string> = T extends `${infer Prefix}.${string}` ? Prefix : never;
|
||||
@@ -34,4 +46,7 @@ export interface LlStreamTypedRefAdapter {
|
||||
recordOps?: string;
|
||||
/** `@sym` of the ScrDynTypedClassOps table for class instance capsules. */
|
||||
classOps?: string;
|
||||
/** `@sym` of the ScrDynTypedBagClassOps table for capsules of bag-tracked
|
||||
* class instances whose views inherit from prototype data. */
|
||||
bagClassOps?: string;
|
||||
}
|
||||
|
||||
@@ -250,8 +250,10 @@ export function emitDynamicExpr(
|
||||
}
|
||||
// Buffer.prototype method names take their scr_bytes_dyn.c entry, which
|
||||
// runs them on Buffer receivers and dispatches any other receiver.
|
||||
const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared";
|
||||
host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
// Each call passes its method name's id slot, so only the first call
|
||||
// looks the name up.
|
||||
const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared_id";
|
||||
host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
let name = host.cstr(e.calleeName);
|
||||
if (e.calleeNameValue !== undefined) {
|
||||
const value = emitBorrowedInput(host, e.calleeNameValue);
|
||||
@@ -260,7 +262,7 @@ export function emitDynamicExpr(
|
||||
}
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`,
|
||||
`${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${host.methodIdSlot(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`,
|
||||
);
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
@@ -717,6 +719,10 @@ export function emitDynamicExpr(
|
||||
B.line(
|
||||
`${test} = call zeroext i1 @scr_dyn_typed_array_is(ptr ${d.name}, i32 ${BYTES_ELEM_NUM[e.bytesElem ?? "u8"]})`,
|
||||
);
|
||||
} else if (e.test === "classCapsule") {
|
||||
host.declare(`declare zeroext i1 @scr_dyn_is_class_capsule(ptr)`);
|
||||
test = B.tmp();
|
||||
B.line(`${test} = call zeroext i1 @scr_dyn_is_class_capsule(ptr ${d.name})`);
|
||||
} else if (e.test === "truthy") {
|
||||
host.declare(`declare zeroext i1 @scr_dyn_truthy(ptr)`);
|
||||
test = B.tmp();
|
||||
@@ -837,6 +843,7 @@ export function emitDynamicExpr(
|
||||
undefined: DYN_KIND.UNDEF,
|
||||
null: DYN_KIND.NULL,
|
||||
bytes: DYN_KIND.BYTES,
|
||||
funcKind: DYN_KIND.FUNC,
|
||||
};
|
||||
test = oneOf([kindOf[e.test]!]);
|
||||
}
|
||||
|
||||
@@ -200,6 +200,10 @@ test("identity bitwise forms over a proven int32 return the operand's number", (
|
||||
}
|
||||
// Unproven operands keep the full ToInt32; other operators still compute.
|
||||
expect(body("unknown_or")).toContain("uint32.coerce.slow");
|
||||
// The i64 conversion covers every magnitude below 2^63; only NaN, the
|
||||
// infinities and larger magnitudes take the slow block.
|
||||
expect(body("unknown_or")).toMatch(/ = fcmp oge double %\S+, 0xC3E0000000000000$/m);
|
||||
expect(body("unknown_or")).toMatch(/ = fcmp olt double %\S+, 0x43E0000000000000$/m);
|
||||
expect(body("length_and")).toMatch(/ = and i32 .*, 255$/m);
|
||||
});
|
||||
|
||||
@@ -308,7 +312,7 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits"
|
||||
});
|
||||
|
||||
test.each([32, 64] as const)(
|
||||
"tagged-template string arrays keep 8-byte slots before their states on %i-bit targets",
|
||||
"tagged-template string arrays keep 8-byte slots without states on %i-bit targets",
|
||||
(bits) => {
|
||||
const strings: IrExpr = {
|
||||
kind: "templateStrings",
|
||||
@@ -321,14 +325,17 @@ test.each([32, 64] as const)(
|
||||
mod.functions[0]!.body = [{ kind: "exprStmt", expr: strings, loc }];
|
||||
const llvm = emitLlvmModule(mod, { pointerBits: bits });
|
||||
const data = /^@sc_tsa_0_data = internal constant .*$/m.exec(llvm)![0];
|
||||
// The runtime reads each slot as 8 bytes and finds the states after
|
||||
// cap * 8 bytes of slots (scr_arr_states_end).
|
||||
if (bits === 64) expect(data).toContain("<{ [3 x ptr], [3 x i8] }>");
|
||||
// The runtime reads each slot as 8 bytes; an array without states
|
||||
// (`spare` 0 in its header) has no state bytes after them.
|
||||
if (bits === 64) expect(data).toMatch(/^\S+ = internal constant \[3 x ptr\] /);
|
||||
else {
|
||||
expect(data).toContain("<{ [3 x <{ ptr, i32 }>], [3 x i8] }>");
|
||||
expect(data).toMatch(/^\S+ = internal constant \[3 x <\{ ptr, i32 \}>\] /);
|
||||
expect(data.match(/<\{ ptr @\S+, i32 0 \}>/g)).toHaveLength(3);
|
||||
}
|
||||
expect(data).toContain("[3 x i8] [ i8 1, i8 1, i8 1 ]");
|
||||
expect(data).not.toContain("x i8]");
|
||||
expect(llvm).toMatch(
|
||||
/^@sc_tsa_0 = internal global %ScrArr \{ i\d+ -1, i32 3, i32 3, i8 2, i8 0, i16 0, i32 3, /m,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { F64, isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js";
|
||||
import { isRefCounted, type IrExpr, type IrStrCmpOp, type IrType } from "../../ir/ir.js";
|
||||
import { mangleRecordClone, mangleRecordNew } from "../mangle.js";
|
||||
import { arrNewCall, elemAccess } from "./shapes.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
|
||||
import { BYTES_ELEM_NUM, closureIdentityEqual, f64Lit } from "./common.js";
|
||||
import { CHECKED_INT32_MIN_LENGTH } from "../../ir/int32-arrays.js";
|
||||
import { emitStringInputs } from "./string-lifetimes.js";
|
||||
import { emitStringParts, numberPart, stringParts } from "./string-construction.js";
|
||||
import { emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
@@ -16,23 +17,33 @@ import {
|
||||
int32Number,
|
||||
integerNumber,
|
||||
narrowInteger,
|
||||
wideArithmetic,
|
||||
widenInteger,
|
||||
} from "./integer-values.js";
|
||||
import {
|
||||
bitwiseIdentityOperand,
|
||||
INT32_RANGE,
|
||||
integerArithmeticRange,
|
||||
isBitwiseOp,
|
||||
withinInt32,
|
||||
withinUint32,
|
||||
} from "../../ir/integer-ranges.js";
|
||||
import { emitArrayValues } from "./expr-containers.js";
|
||||
import { charAtUnitRead, emitArrayValues } from "./expr-containers.js";
|
||||
import {
|
||||
constantDivisor,
|
||||
emitCheckedRemainder,
|
||||
emitPowerOfTwoRemainder,
|
||||
emitSignedIntegerRemainder,
|
||||
} from "./integer-remainder.js";
|
||||
import { denseSlotWidth, emitDenseArrayGet, emitDenseArrayState } from "./dense-array-access.js";
|
||||
import {
|
||||
denseSlotWidth,
|
||||
emitDenseArrayRead,
|
||||
emitDenseArrayState,
|
||||
runtimeAccess,
|
||||
SCR_ELEM_I32,
|
||||
slotWidth,
|
||||
type ArrayAccess,
|
||||
} from "./dense-array-access.js";
|
||||
import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js";
|
||||
|
||||
export function emitLiteralExpr(
|
||||
@@ -337,7 +348,14 @@ export function emitOperatorExpr(
|
||||
if (arith[e.op] !== undefined) {
|
||||
B.line(`${t} = ${arith[e.op]} double ${l.name}, ${r.name}`);
|
||||
const uint32 = emitWrappingSum(host, e, l, r, li, ri);
|
||||
if (uint32 !== undefined) return { name: t, type: e.type, uint32 };
|
||||
// The exact integer beside the double, for index consumers.
|
||||
const wideInteger = wideArithmetic(host, e.op, l, li, r, ri);
|
||||
if (uint32 !== undefined || wideInteger) {
|
||||
const value: LlValue = { name: t, type: e.type };
|
||||
if (uint32 !== undefined) value.uint32 = uint32;
|
||||
if (wideInteger) value.wideInteger = wideInteger;
|
||||
return value;
|
||||
}
|
||||
} else B.line(`${t} = fcmp ${cmp[e.op]} double ${l.name}, ${r.name}`);
|
||||
} else if (bit[e.op] !== undefined) {
|
||||
if (e.left.type.kind !== "f64" || e.right.type.kind !== "f64") {
|
||||
@@ -563,14 +581,16 @@ export function regexLiteralTemplate(
|
||||
};
|
||||
host.regexInstances.set(key, re);
|
||||
}
|
||||
host.journal?.entry("regex", key, re);
|
||||
return `@${re.sym}`;
|
||||
}
|
||||
|
||||
/** A string equality between `s.charAt(i)` and a literal of exactly one
|
||||
* UTF-16 code unit: the charAt call and the unit. */
|
||||
/** A string equality or comparison between `s.charAt(i)` and a literal of
|
||||
* exactly one UTF-16 code unit: the charAt call, the unit, and whether the
|
||||
* charAt is the left operand. */
|
||||
function charAtUnitTest(
|
||||
e: ExprOf<"strEq">,
|
||||
): { charAt: ExprOf<"strIntrinsic">; code: number } | null {
|
||||
e: ExprOf<"strEq" | "strCmp">,
|
||||
): { charAt: ExprOf<"strIntrinsic">; code: number; left: boolean } | null {
|
||||
let text: string;
|
||||
let other: IrExpr;
|
||||
if (e.right.kind === "strLit") {
|
||||
@@ -585,7 +605,17 @@ function charAtUnitTest(
|
||||
if (text.length !== 1) return null;
|
||||
if (other.kind !== "strIntrinsic" || other.method !== "charAt" || other.args.length !== 1)
|
||||
return null;
|
||||
return { charAt: other, code: text.charCodeAt(0) };
|
||||
return { charAt: other, code: text.charCodeAt(0), left: other === e.left };
|
||||
}
|
||||
|
||||
/** The fcmp predicate of `s.charAt(i) op "c"` on the code unit (or of
|
||||
* `"c" op s.charAt(i)` when `charAtLeft` is false). A one-unit string
|
||||
* compares as its unit; an index out of range reads "", which is below
|
||||
* every one-unit literal, and its unit read is NaN, which the unordered
|
||||
* predicates count as below. */
|
||||
function charAtUnitPredicate(op: IrStrCmpOp, charAtLeft: boolean): string {
|
||||
const flipped = charAtLeft ? op : ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as const)[op];
|
||||
return { "<": "ult", "<=": "ule", ">": "ogt", ">=": "oge" }[flipped];
|
||||
}
|
||||
|
||||
export function emitStringExpr(
|
||||
@@ -636,14 +666,7 @@ export function emitStringExpr(
|
||||
// equals no unit, as "" equals no one-unit literal.
|
||||
const unit = charAtUnitTest(e);
|
||||
if (unit !== null) {
|
||||
const code = host.emitExpr({
|
||||
kind: "strIntrinsic",
|
||||
method: "charCodeAt",
|
||||
receiver: unit.charAt.receiver,
|
||||
args: unit.charAt.args,
|
||||
type: F64,
|
||||
loc: unit.charAt.loc,
|
||||
});
|
||||
const code = host.emitExpr(charAtUnitRead(host, unit.charAt));
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = fcmp ${e.negated ? "une" : "oeq"} double ${code.name}, ${f64Lit(unit.code)}`,
|
||||
@@ -673,6 +696,17 @@ export function emitStringExpr(
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
case "strCmp": {
|
||||
// `s.charAt(i) < "c"` (and `s[i]`) for a one-unit literal compares
|
||||
// the code unit, as the equality above does.
|
||||
const unit = charAtUnitTest(e);
|
||||
if (unit !== null) {
|
||||
const code = host.emitExpr(charAtUnitRead(host, unit.charAt));
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = fcmp ${charAtUnitPredicate(e.op, unit.left)} double ${code.name}, ${f64Lit(unit.code)}`,
|
||||
);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
const [l, r] = emitStringInputs(host, [e.left, e.right]);
|
||||
const fn = "scr_str_cmp_u16";
|
||||
host.declare(`declare i32 @${fn}(ptr, ptr)`);
|
||||
@@ -823,6 +857,7 @@ export function emitStringExpr(
|
||||
};
|
||||
host.templateStringsInstances.set(e.key, inst);
|
||||
}
|
||||
host.journal?.entry("templateStrings", e.key, inst);
|
||||
return host.own({ name: host.retainFrame(`@${inst.sym}`, e.type), type: e.type });
|
||||
}
|
||||
case "regexIntrinsic":
|
||||
@@ -869,9 +904,46 @@ export function emitContainerExpr(
|
||||
// the plain elements up to the next one (scr_arr_reserve_spread).
|
||||
const firstSpread = e.elems.findIndex((_, i) => spreadSet.has(i));
|
||||
const capacity = small ? 0 : (e.capacity ?? (firstSpread < 0 ? e.elems.length : firstSpread));
|
||||
B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
|
||||
// An int32 array's allocation (ir/int32-arrays.ts) constructs
|
||||
// SCR_ELEM_I32 storage; its elements are proven int32s. The empty
|
||||
// temporary of a fill that allocates an int32 array with checked
|
||||
// stores takes SCR_ELEM_I32 storage when the fill's length (which
|
||||
// evaluates without effects) is at least CHECKED_INT32_MIN_LENGTH, and
|
||||
// SCR_ELEM_F64 storage otherwise.
|
||||
const fillLength = host.checkedAllocations.get(e);
|
||||
if (fillLength !== undefined && e.elems.length === 0) {
|
||||
const length = host.emitExpr(fillLength);
|
||||
const long = B.tmp(),
|
||||
kind = B.tmp();
|
||||
B.line(`${long} = fcmp oge double ${length.name}, ${f64Lit(CHECKED_INT32_MIN_LENGTH)}`);
|
||||
B.line(`${kind} = select i1 ${long}, i32 ${SCR_ELEM_I32}, i32 0`);
|
||||
host.declare(
|
||||
small
|
||||
? `declare ptr @scr_arr_new_small(i32)`
|
||||
: `declare ptr @scr_arr_new(i32, ${host.sizeType})`,
|
||||
);
|
||||
B.line(
|
||||
small
|
||||
? `${arr} = call ptr @scr_arr_new_small(i32 ${kind})`
|
||||
: `${arr} = call ptr @scr_arr_new(i32 ${kind}, ${host.sizeType} ${capacity})`,
|
||||
);
|
||||
return host.own({ name: arr, type: e.type });
|
||||
}
|
||||
const int32 = host.int32Allocations.has(e);
|
||||
if (int32) {
|
||||
host.declare(
|
||||
small
|
||||
? `declare ptr @scr_arr_new_small(i32)`
|
||||
: `declare ptr @scr_arr_new(i32, ${host.sizeType})`,
|
||||
);
|
||||
B.line(
|
||||
small
|
||||
? `${arr} = call ptr @scr_arr_new_small(i32 ${SCR_ELEM_I32})`
|
||||
: `${arr} = call ptr @scr_arr_new(i32 ${SCR_ELEM_I32}, ${host.sizeType} ${capacity})`,
|
||||
);
|
||||
} else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
|
||||
const out = host.own({ name: arr, type: e.type });
|
||||
const acc = elemAccess(elem);
|
||||
const acc: ArrayAccess = int32 ? "i32" : elemAccess(elem);
|
||||
let pending: LlValue[] = [];
|
||||
let buffer: string | undefined;
|
||||
const flush = (): void => {
|
||||
@@ -892,7 +964,8 @@ export function emitContainerExpr(
|
||||
B.line(
|
||||
`call void @scr_arr_reserve_spread(ptr ${arr}, ptr ${v.name}, ${host.sizeType} ${plain})`,
|
||||
);
|
||||
host.emitArrayCopyLoop(arr, v.name, acc);
|
||||
// A literal with spreads is never an int32 allocation.
|
||||
host.emitArrayCopyLoop(arr, v.name, runtimeAccess(acc));
|
||||
return;
|
||||
}
|
||||
pending.push(host.emitExpr(el));
|
||||
@@ -937,33 +1010,65 @@ export function emitContainerExpr(
|
||||
// Ref-element reads return +1 (the inline dense path retains like
|
||||
// the runtime getter); own registers the owned temp in the frame.
|
||||
const elem = e.arr.type.elem;
|
||||
const acc = elemAccess(elem);
|
||||
const t = emitDenseArrayGet(
|
||||
if (host.checkedInt32Array(e.arr)) {
|
||||
const proven = host.provenDenseIndex(e.arr, e.index);
|
||||
const value = host.emitByElementKind(
|
||||
arr.name,
|
||||
"double",
|
||||
(kind) =>
|
||||
emitDenseArrayRead(
|
||||
host,
|
||||
arr.name,
|
||||
idx,
|
||||
e.index,
|
||||
kind,
|
||||
elem,
|
||||
"scr_arr_get_f64",
|
||||
true,
|
||||
proven,
|
||||
).value,
|
||||
)!;
|
||||
return { name: value, type: e.type };
|
||||
}
|
||||
const acc = host.arrayAccess(e.arr, elem);
|
||||
const read = emitDenseArrayRead(
|
||||
host,
|
||||
arr.name,
|
||||
idx,
|
||||
e.index,
|
||||
acc,
|
||||
elem,
|
||||
`scr_arr_get_${acc}`,
|
||||
`scr_arr_get_${runtimeAccess(acc)}`,
|
||||
true,
|
||||
host.provenDenseIndex(e.arr, e.index),
|
||||
);
|
||||
return host.own({ name: t, type: e.type });
|
||||
if (read.int32 !== null)
|
||||
// An int32 array's element: its i32 is exact for index consumers.
|
||||
return {
|
||||
name: read.value,
|
||||
type: e.type,
|
||||
uint32: read.int32,
|
||||
integer: { name: read.int32, type: "i32", signed: true, range: INT32_RANGE },
|
||||
};
|
||||
return host.own({ name: read.value, type: e.type });
|
||||
}
|
||||
case "arrayHas": {
|
||||
const arr = host.emitStableReceiver(e.arr, [e.index]);
|
||||
const idx = host.emitExpr(e.index);
|
||||
if (e.arr.type.kind !== "array")
|
||||
throw new InternalCompilerError("llvm emitter bug: arrayHas on non-array");
|
||||
const t = emitDenseArrayState(
|
||||
host,
|
||||
arr.name,
|
||||
idx,
|
||||
e.index,
|
||||
"has",
|
||||
denseSlotWidth(host, e.arr.type.elem),
|
||||
);
|
||||
const t = host.checkedInt32Array(e.arr)
|
||||
? host.emitByElementKind(arr.name, "i1", (kind) =>
|
||||
emitDenseArrayState(host, arr.name, idx, e.index, "has", slotWidth(kind)),
|
||||
)!
|
||||
: emitDenseArrayState(
|
||||
host,
|
||||
arr.name,
|
||||
idx,
|
||||
e.index,
|
||||
"has",
|
||||
host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem),
|
||||
);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
case "arrayState": {
|
||||
@@ -971,14 +1076,18 @@ export function emitContainerExpr(
|
||||
const idx = host.emitExpr(e.index);
|
||||
if (e.arr.type.kind !== "array")
|
||||
throw new InternalCompilerError("llvm emitter bug: arrayState on non-array");
|
||||
const t = emitDenseArrayState(
|
||||
host,
|
||||
arr.name,
|
||||
idx,
|
||||
e.index,
|
||||
"state",
|
||||
denseSlotWidth(host, e.arr.type.elem),
|
||||
);
|
||||
const t = host.checkedInt32Array(e.arr)
|
||||
? host.emitByElementKind(arr.name, "double", (kind) =>
|
||||
emitDenseArrayState(host, arr.name, idx, e.index, "state", slotWidth(kind)),
|
||||
)!
|
||||
: emitDenseArrayState(
|
||||
host,
|
||||
arr.name,
|
||||
idx,
|
||||
e.index,
|
||||
"state",
|
||||
host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem),
|
||||
);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
case "arrIntrinsic":
|
||||
|
||||
@@ -141,7 +141,7 @@ test("live record capsules bind field hooks in snapshot order", () => {
|
||||
const llvm = emitLlvmModule(module, { pointerBits: 64 });
|
||||
const tables = [
|
||||
...llvm.matchAll(
|
||||
/^@(\S+)_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr \} \{ i64 (\d+), /gm,
|
||||
/^@(\S+)_rops = internal global \{ i64, ptr, ptr, ptr, i8, ptr, ptr \} \{ i64 (\d+), /gm,
|
||||
),
|
||||
];
|
||||
// root and leaf get field hooks; the record with an index-like field name
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
classFieldAttributesName,
|
||||
isClassOwnEnumerableFieldName,
|
||||
isDirentShape,
|
||||
KEY_ORDER_FIELD,
|
||||
shapeKeyOrders,
|
||||
isDynTypedRefType,
|
||||
isRefCounted,
|
||||
shapeHasAccessorSlots,
|
||||
@@ -44,6 +46,8 @@ import type { NullableRefField } from "./nullable-fields.js";
|
||||
import { emitUnionPeek, emitUnionTag } from "./union-repr.js";
|
||||
import { inlineUnionCopyLines } from "./inline-unions.js";
|
||||
import { recordMayKeepSource } from "./record-sources.js";
|
||||
import { emitPerKeyOrder } from "./key-orders.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
|
||||
/** An owned (+1) union value for a nullable-pointer class field slot `p`
|
||||
* (the unit arm's immortal instance, or a fresh box around a retained
|
||||
@@ -91,6 +95,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri
|
||||
if (existing) return existing;
|
||||
const sym = `sc_dpa_${host.dynPromiseAdapters.size}`;
|
||||
host.dynPromiseAdapters.set(key, sym);
|
||||
host.journal?.allocated(sym, "dynPromiseAdapter", [inner]);
|
||||
host.declare(`declare ptr @scr_promise_payload_ref(ptr)`);
|
||||
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
||||
host.declare(`declare void @scr_promise_reject_pending(ptr)`);
|
||||
@@ -146,6 +151,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -602,6 +608,7 @@ export function liveDynUnionRefAdapter(
|
||||
|
||||
const sym = `sc_ldu_${host.liveDynUnionRefAdapters.size}`;
|
||||
host.liveDynUnionRefAdapters.set(key, sym);
|
||||
host.journal?.allocated(sym, "liveDynUnionRefAdapter", [t]);
|
||||
|
||||
const adapters = new Map<number, LlStreamTypedRefAdapter>();
|
||||
for (const { arm, tag } of mutableArms) {
|
||||
@@ -684,6 +691,7 @@ export function liveDynUnionRefAdapter(
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -732,6 +740,11 @@ export function streamTypedRefMaterializeAdapter(
|
||||
const snapshot = `sc_ldr_${host.liveDynRefAdapters.size}_materialize`;
|
||||
const adapter: LlStreamTypedRefAdapter = { snapshot, commit: "null" };
|
||||
host.liveDynRefAdapters.set(key, adapter);
|
||||
host.journal?.allocated(
|
||||
snapshot.replace(/_materialize$/, ""),
|
||||
"streamTypedRefMaterializeAdapter",
|
||||
[t],
|
||||
);
|
||||
if (isDynTypedRefType(t)) {
|
||||
const fields = host.classMeta.get(t.className)?.def.fields;
|
||||
if (
|
||||
@@ -744,6 +757,7 @@ export function streamTypedRefMaterializeAdapter(
|
||||
) {
|
||||
host.declare(`declare ptr @scr_dyn_class_view_unavailable(ptr)`);
|
||||
adapter.snapshot = "scr_dyn_class_view_unavailable";
|
||||
host.journal?.built();
|
||||
return adapter;
|
||||
}
|
||||
}
|
||||
@@ -753,6 +767,7 @@ export function streamTypedRefMaterializeAdapter(
|
||||
// class view ops are published first.
|
||||
const opsMeta = isDynTypedRefType(t) ? host.classMeta.get(t.className) : undefined;
|
||||
if (opsMeta && liveClassOpsEligible(opsMeta)) adapter.classOps = `@${snapshot}_cops`;
|
||||
else if (opsMeta && liveBagClassOpsEligible(opsMeta)) adapter.bagClassOps = `@${snapshot}_bops`;
|
||||
adapter.commit = `@${snapshot}_commit`;
|
||||
adapter.commit = host.streamTypedRefCommitAdapter(t, snapshot);
|
||||
const B = new BlockBuilder();
|
||||
@@ -765,6 +780,7 @@ export function streamTypedRefMaterializeAdapter(
|
||||
);
|
||||
}
|
||||
if (adapter.classOps !== undefined) liveClassOps(host, snapshot, meta);
|
||||
if (adapter.bagClassOps !== undefined) liveBagClassOps(host, snapshot, meta, t);
|
||||
if (meta.hierarchy && meta.children.length) {
|
||||
const pre = B.tmp();
|
||||
for (const line of classPreLines(meta, classStructSym(t.className), "%p", pre, host.sizeType))
|
||||
@@ -836,11 +852,7 @@ export function streamTypedRefMaterializeAdapter(
|
||||
B.line(`call void @scr_dyn_release_v(ptr ${prototype})`);
|
||||
B.line(`call void @scr_dyn_release_v(ptr ${attached})`);
|
||||
}
|
||||
for (const field of meta.def.fields.filter(
|
||||
(f) =>
|
||||
isClassOwnEnumerableFieldName(f.name) &&
|
||||
!(meta.root.def.name === "%Error" && (f.name === "message" || f.name === "name")),
|
||||
)) {
|
||||
for (const field of classViewFields(meta)) {
|
||||
const afterField = meta.def.tracksOwnFields ? B.newLabel("field.after") : null;
|
||||
if (afterField) {
|
||||
host.declare(`declare ptr @scr_dyn_obj_get(ptr, ptr, ${host.sizeType})`);
|
||||
@@ -854,32 +866,7 @@ export function streamTypedRefMaterializeAdapter(
|
||||
B.condBr(present, yes, afterField);
|
||||
B.startBlock(yes);
|
||||
}
|
||||
const { index, compressed } = classFieldIndex(meta, field.name, "compressed");
|
||||
const fieldPtr = B.tmp();
|
||||
let fieldValue = B.tmp();
|
||||
B.line(
|
||||
`${fieldPtr} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${index}`,
|
||||
);
|
||||
if (host.int32Slots.isField(t.className, field.name)) {
|
||||
const raw = B.tmp();
|
||||
B.line(`${raw} = load i32, ptr ${fieldPtr}`);
|
||||
B.line(`${fieldValue} = sitofp i32 ${raw} to double`);
|
||||
} else if (host.inlineUnions.classField(t.className, field.name))
|
||||
fieldValue = loadStructField(B, host.sizeType, fieldPtr, field.type, true);
|
||||
else if (compressed) {
|
||||
const idx = B.tmp();
|
||||
B.line(`${idx} = load i32, ptr ${fieldPtr}`);
|
||||
for (const line of compactRefLoadLines(idx, fieldValue, `${fieldValue}.r`))
|
||||
B.line(line.trimStart());
|
||||
} else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`);
|
||||
if (llFieldType(field.type, false) === "i8") {
|
||||
const boolValue = B.tmp();
|
||||
B.line(`${boolValue} = trunc i8 ${fieldValue} to i1`);
|
||||
fieldValue = boolValue;
|
||||
}
|
||||
const nullable = host.nullableFields.get(t.className, field.name);
|
||||
if (nullable) fieldValue = nullableFieldUnion(host, B, fieldValue, nullable);
|
||||
const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue);
|
||||
const { boxed, nullable, fieldValue } = classFieldDynValue(host, B, meta, t, field);
|
||||
// Attributes copied from the bag do not stop the view taking the value.
|
||||
const setter = withAttributes ? "scr_dyn_obj_set_field" : "scr_dyn_obj_set";
|
||||
if (withAttributes)
|
||||
@@ -1054,52 +1041,68 @@ export function streamTypedRefMaterializeAdapter(
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = call ptr @scr_dyn_new_obj()`);
|
||||
const byName = new Map(shape.fields.map((field) => [field.name, field]));
|
||||
const order = shape.declaredOrder ?? shape.fields.map((field) => field.name);
|
||||
const inOrder = new Set(order);
|
||||
const fields = [
|
||||
...order.map((name) => byName.get(name)).filter((field) => field !== undefined),
|
||||
...shape.fields.filter((field) => !inOrder.has(field.name)),
|
||||
];
|
||||
// A shape with orderVariants has one field list per key order; its
|
||||
// order index is no key.
|
||||
const orders = shapeKeyOrders(shape);
|
||||
const fieldsOf = (order: readonly string[]): IrRecordShape["fields"][number][] => {
|
||||
const inOrder = new Set(order);
|
||||
return [
|
||||
...order.map((name) => byName.get(name)).filter((field) => field !== undefined),
|
||||
...shape.fields.filter(
|
||||
(field) => !inOrder.has(field.name) && field.name !== KEY_ORDER_FIELD,
|
||||
),
|
||||
];
|
||||
};
|
||||
// Published before the field converters, like the array hooks below.
|
||||
// A Dirent row reads through its finished view (the prototype and
|
||||
// the non-enumerable %dtype).
|
||||
const dirent = isDirentShape(shape);
|
||||
if (!dirent && liveRecordOpsEligible(fields)) {
|
||||
adapter.recordOps = `@${snapshot}_rops`;
|
||||
liveRecordOps(host, t, snapshot, fields);
|
||||
}
|
||||
for (const field of fields) {
|
||||
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
|
||||
const fieldPtr = B.tmp();
|
||||
B.line(
|
||||
`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`,
|
||||
);
|
||||
const fieldValue = loadStructField(
|
||||
B,
|
||||
host.sizeType,
|
||||
fieldPtr,
|
||||
field.type,
|
||||
host.inlineUnions.record(field.type),
|
||||
);
|
||||
// An ABSENT optional field contributes no key.
|
||||
const undefinedTag = undefinedArmTag(field.type, host.unionsById);
|
||||
let skip: string | null = null;
|
||||
if (undefinedTag >= 0 && field.type.kind === "union") {
|
||||
const absent = host.fieldAbsentTestIn(B, fieldValue, field.type.unionId);
|
||||
skip = B.newLabel("live.record.absent");
|
||||
const set = B.newLabel("live.record.set");
|
||||
B.condBr(absent, skip, set);
|
||||
B.startBlock(set);
|
||||
}
|
||||
const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue);
|
||||
B.line(
|
||||
`call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`,
|
||||
);
|
||||
if (skip !== null) {
|
||||
B.br(skip);
|
||||
B.startBlock(skip);
|
||||
if (!dirent && orders.every((order) => liveRecordOpsEligible(fieldsOf(order)))) {
|
||||
if (orders.length === 1) {
|
||||
adapter.recordOps = `@${snapshot}_rops`;
|
||||
liveRecordOps(host, t, snapshot, fieldsOf(orders[0]!));
|
||||
} else {
|
||||
orders.forEach((order, i) =>
|
||||
liveRecordOps(host, t, `${snapshot}_o${i}`, fieldsOf(order)),
|
||||
);
|
||||
adapter.recordOps = `@${snapshot}_rops`;
|
||||
liveRecordOrderSelect(host, shape, snapshot);
|
||||
}
|
||||
}
|
||||
emitPerKeyOrder(B, shape, host.nullableUnions.fieldBase(t.shapeId), "%p", (order) => {
|
||||
for (const field of fieldsOf(order)) {
|
||||
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
|
||||
const fieldPtr = B.tmp();
|
||||
B.line(
|
||||
`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`,
|
||||
);
|
||||
const fieldValue = loadStructField(
|
||||
B,
|
||||
host.sizeType,
|
||||
fieldPtr,
|
||||
field.type,
|
||||
host.inlineUnions.record(field.type),
|
||||
);
|
||||
// An ABSENT optional field contributes no key.
|
||||
const undefinedTag = undefinedArmTag(field.type, host.unionsById);
|
||||
let skip: string | null = null;
|
||||
if (undefinedTag >= 0 && field.type.kind === "union") {
|
||||
const absent = host.fieldAbsentTestIn(B, fieldValue, field.type.unionId);
|
||||
skip = B.newLabel("live.record.absent");
|
||||
const set = B.newLabel("live.record.set");
|
||||
B.condBr(absent, skip, set);
|
||||
B.startBlock(set);
|
||||
}
|
||||
const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue);
|
||||
B.line(
|
||||
`call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`,
|
||||
);
|
||||
if (skip !== null) {
|
||||
B.br(skip);
|
||||
B.startBlock(skip);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (dirent) {
|
||||
host.declare(`declare void @scr_dyn_dirent_finish(ptr)`);
|
||||
B.line(`call void @scr_dyn_dirent_finish(ptr ${out})`);
|
||||
@@ -1185,6 +1188,7 @@ export function streamTypedRefMaterializeAdapter(
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@@ -1361,7 +1365,35 @@ function liveRecordOps(
|
||||
``,
|
||||
`@${names} = internal constant [${fields.length} x ptr] [${literals.join(", ")}]`,
|
||||
// Mutable: the runtime fills the trailing key-array slot on first use.
|
||||
`@${ops} = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr } { ${host.sizeType} ${fields.length}, ptr @${names}, ptr @${has}, ptr @${get}, i8 ${optional.length === 0 ? 1 : 0}, ptr null }`,
|
||||
`@${ops} = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr, ptr } { ${host.sizeType} ${fields.length}, ptr @${names}, ptr @${has}, ptr @${get}, i8 ${optional.length === 0 ? 1 : 0}, ptr null, ptr null }`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
/** `@<snapshot>_rops` for a shape with orderVariants: a record-ops table
|
||||
* whose `select` hook answers the `_o<i>_rops` table (liveRecordOps) of the
|
||||
* record's current key order, which a write of an optional key can change
|
||||
* after the record is boxed. */
|
||||
function liveRecordOrderSelect(
|
||||
host: LlvmEmitterContext,
|
||||
shape: IrRecordShape,
|
||||
snapshot: string,
|
||||
): void {
|
||||
const B = new BlockBuilder();
|
||||
const out = B.slot();
|
||||
B.entryAllocas.push(`${out} = alloca ptr`);
|
||||
emitPerKeyOrder(B, shape, host.nullableUnions.fieldBase(shape.id), "%p", (_, i) => {
|
||||
B.line(`store ptr @${snapshot}_o${i}_rops, ptr ${out}`);
|
||||
});
|
||||
const ops = B.tmp();
|
||||
B.line(`${ops} = load ptr, ptr ${out}`);
|
||||
B.terminate(`ret ptr ${ops}`);
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal ptr @${snapshot}_select(ptr %p) ${FN_ATTRS} { ; live record ops by key order`,
|
||||
B.render(),
|
||||
`}`,
|
||||
``,
|
||||
`@${snapshot}_rops = internal global { ${host.sizeType}, ptr, ptr, ptr, i8, ptr, ptr } { ${host.sizeType} 0, ptr null, ptr null, ptr null, i8 0, ptr null, ptr @${snapshot}_select }`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
@@ -1432,14 +1464,157 @@ function liveClassOps(host: LlvmEmitterContext, snapshot: string, meta: LlClassM
|
||||
);
|
||||
}
|
||||
|
||||
/** The checked-dynamic value a class instance's view holds for `field`, read
|
||||
* from its native slot of `%p`: `boxed` (+1), and the nullable union
|
||||
* `fieldValue` the caller releases when `nullable` is set. */
|
||||
function classFieldDynValue(
|
||||
host: LlvmEmitterContext,
|
||||
B: BlockBuilder,
|
||||
meta: LlClassMeta,
|
||||
t: Extract<IrType, { kind: "object" }>,
|
||||
field: LlClassMeta["def"]["fields"][number],
|
||||
): { boxed: string; nullable: NullableRefField | null; fieldValue: string } {
|
||||
const { index, compressed } = classFieldIndex(meta, field.name, "compressed");
|
||||
const fieldPtr = B.tmp();
|
||||
let fieldValue = B.tmp();
|
||||
B.line(
|
||||
`${fieldPtr} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${index}`,
|
||||
);
|
||||
if (host.int32Slots.isField(t.className, field.name)) {
|
||||
const raw = B.tmp();
|
||||
B.line(`${raw} = load i32, ptr ${fieldPtr}`);
|
||||
B.line(`${fieldValue} = sitofp i32 ${raw} to double`);
|
||||
} else if (host.inlineUnions.classField(t.className, field.name))
|
||||
fieldValue = loadStructField(B, host.sizeType, fieldPtr, field.type, true);
|
||||
else if (compressed) {
|
||||
const idx = B.tmp();
|
||||
B.line(`${idx} = load i32, ptr ${fieldPtr}`);
|
||||
for (const line of compactRefLoadLines(idx, fieldValue, `${fieldValue}.r`))
|
||||
B.line(line.trimStart());
|
||||
} else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`);
|
||||
if (llFieldType(field.type, false) === "i8") {
|
||||
const boolValue = B.tmp();
|
||||
B.line(`${boolValue} = trunc i8 ${fieldValue} to i1`);
|
||||
fieldValue = boolValue;
|
||||
}
|
||||
const nullable = host.nullableFields.get(t.className, field.name);
|
||||
if (nullable) fieldValue = nullableFieldUnion(host, B, fieldValue, nullable);
|
||||
return { boxed: host.streamTypedRefBoxValue(B, field.type, fieldValue), nullable, fieldValue };
|
||||
}
|
||||
|
||||
/** The fields an instance's view takes from their native slots, in order:
|
||||
* its own enumerable fields, except an Error's message and name. */
|
||||
function classViewFields(meta: LlClassMeta): LlClassMeta["def"]["fields"] {
|
||||
return meta.def.fields.filter(
|
||||
(f) =>
|
||||
isClassOwnEnumerableFieldName(f.name) &&
|
||||
!(meta.root.def.name === "%Error" && (f.name === "message" || f.name === "name")),
|
||||
);
|
||||
}
|
||||
|
||||
/** Classes whose instance views the materializer builds from the property
|
||||
* bag and the prototype data alone (see liveBagClassOps): JavaScript classes
|
||||
* tracking their own fields, outside the Error and runtime families, with no
|
||||
* subclass whose view another materializer builds. */
|
||||
function liveBagClassOpsEligible(meta: LlClassMeta): boolean {
|
||||
return (
|
||||
meta.def.tracksOwnFields === true &&
|
||||
(meta.def.prototypeDataHelper !== undefined ||
|
||||
meta.def.instancePrototypeHelper !== undefined) &&
|
||||
!meta.def.runtime &&
|
||||
!meta.root.def.runtime &&
|
||||
meta.root.def.name !== "%Error" &&
|
||||
!(meta.hierarchy && meta.children.length > 0) &&
|
||||
meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES) &&
|
||||
!meta.def.fields.some(
|
||||
(field) =>
|
||||
field.name === CLASS_ATTRIBUTES ||
|
||||
meta.def.fields.some((f) => f.name === classFieldAttributesName(field.name)),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** The hooks of a bag-tracked class instance capsule
|
||||
* (ScrDynTypedBagClassOps). The materializer gives the instance's view
|
||||
* exactly the bag's string-keyed properties, the values of the declared
|
||||
* fields among them from their native slots, and the prototype these hooks
|
||||
* return, so a method call resolves without building the view. */
|
||||
function liveBagClassOps(
|
||||
host: LlvmEmitterContext,
|
||||
snapshot: string,
|
||||
meta: LlClassMeta,
|
||||
t: Extract<IrType, { kind: "object" }>,
|
||||
): void {
|
||||
const struct = classStructSym(t.className);
|
||||
const bag = new BlockBuilder();
|
||||
const slot = bag.tmp(),
|
||||
value = bag.tmp();
|
||||
bag.line(
|
||||
`${slot} = getelementptr inbounds %${struct}, ptr %p, i64 0, i32 ${classFieldIndex(meta, DYN_CLASS_PROPERTIES).index}`,
|
||||
);
|
||||
bag.line(`${value} = load ptr, ptr ${slot}`);
|
||||
bag.terminate(`ret ptr ${value}`);
|
||||
const proto = new BlockBuilder();
|
||||
const prototype = proto.tmp();
|
||||
if (meta.def.instancePrototypeHelper) {
|
||||
const receiver = proto.tmp();
|
||||
proto.line(`${receiver} = call ptr ${vAdapters(host.shapeHost, t).retain}(ptr %p)`);
|
||||
proto.line(
|
||||
`${prototype} = call ptr @${mangleFunction(meta.def.instancePrototypeHelper)}(ptr ${receiver})`,
|
||||
);
|
||||
} else proto.line(`${prototype} = call ptr @${mangleFunction(meta.def.prototypeDataHelper!)}()`);
|
||||
proto.terminate(`ret ptr ${prototype}`);
|
||||
// Field `%i` of the view's field list, as the materializer boxes it.
|
||||
const fields = classViewFields(meta);
|
||||
const get = new BlockBuilder();
|
||||
const cases = fields.map((_, i) => get.newLabel(`field.${i}`));
|
||||
const none = get.newLabel("field.none");
|
||||
get.terminate(
|
||||
`switch ${host.sizeType} %i, label %${none} [ ${cases.map((label, i) => `${host.sizeType} ${i}, label %${label}`).join(" ")} ]`,
|
||||
);
|
||||
fields.forEach((field, i) => {
|
||||
get.startBlock(cases[i]!);
|
||||
const { boxed, nullable, fieldValue } = classFieldDynValue(host, get, meta, t, field);
|
||||
if (nullable) {
|
||||
host.declare(`declare void @scr_union_release(ptr)`);
|
||||
get.line(`call void @scr_union_release(ptr ${fieldValue})`);
|
||||
}
|
||||
get.terminate(`ret ptr ${boxed}`);
|
||||
});
|
||||
get.startBlock(none);
|
||||
get.terminate(`ret ptr null`);
|
||||
const names = fields.map((field) => `ptr ${host.internLiteral(field.name)}`);
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal ptr @${snapshot}_field(ptr %p, ${host.sizeType} %i) ${FN_ATTRS} { ; view field of ${meta.def.name}`,
|
||||
get.render(),
|
||||
`}`,
|
||||
``,
|
||||
`@${snapshot}_fnames = internal constant [${names.length} x ptr] ${names.length ? `[${names.join(", ")}]` : "zeroinitializer"}`,
|
||||
);
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal ptr @${snapshot}_bag(ptr %p) ${FN_ATTRS} { ; property bag of ${meta.def.name}`,
|
||||
bag.render(),
|
||||
`}`,
|
||||
``,
|
||||
`define internal ptr @${snapshot}_proto(ptr %p) ${FN_ATTRS} { ; view prototype of ${meta.def.name}`,
|
||||
proto.render(),
|
||||
`}`,
|
||||
``,
|
||||
`@${snapshot}_bops = internal constant { ptr, ptr, ${host.sizeType}, ptr, ptr } { ptr @${snapshot}_bag, ptr @${snapshot}_proto, ${host.sizeType} ${fields.length}, ptr @${snapshot}_fnames, ptr @${snapshot}_field }`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
/** The live view a capsule of this adapter binds: the runtime bind call
|
||||
* and its ops constant, with the view code a deferred receiver recipe
|
||||
* records (1 array, 2 record, 3 class). */
|
||||
* records (1 array, 2 record, 3 class, 4 bag-tracked class). */
|
||||
export function liveRefView(
|
||||
adapter: LlStreamTypedRefAdapter,
|
||||
): { bind: string; ops: string; code: 1 | 2 | 3 } | null {
|
||||
): { bind: string; ops: string; code: 1 | 2 | 3 | 4 } | null {
|
||||
if (adapter.classOps !== undefined)
|
||||
return { bind: "scr_dyn_typed_ref_bind_class", ops: adapter.classOps, code: 3 };
|
||||
if (adapter.bagClassOps !== undefined)
|
||||
return { bind: "scr_dyn_typed_ref_bind_bag_class", ops: adapter.bagClassOps, code: 4 };
|
||||
if (adapter.recordOps !== undefined)
|
||||
return { bind: "scr_dyn_typed_ref_bind_record", ops: adapter.recordOps, code: 2 };
|
||||
if (adapter.arrayOps !== undefined)
|
||||
@@ -1469,6 +1644,7 @@ export function streamFromArrayAdapter(
|
||||
if (existing) return existing;
|
||||
const sym = `sc_sfa_${host.streamFromArrayAdapters.size}`;
|
||||
host.streamFromArrayAdapters.set(key, sym);
|
||||
host.journal?.allocated(sym, "streamFromArrayAdapter", [t]);
|
||||
const B = new BlockBuilder();
|
||||
const unionDef = elem.kind === "union" ? host.unionsById.get(elem.unionId) : undefined;
|
||||
if (elem.kind === "union" && !unionDef) {
|
||||
@@ -1585,5 +1761,17 @@ export function streamFromArrayAdapter(
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
registerReplay("dynPromiseAdapter", (host, args) => dynPromiseAdapter(host, args[0] as IrType));
|
||||
registerReplay("liveDynUnionRefAdapter", (host, args) =>
|
||||
liveDynUnionRefAdapter(host, args[0] as IrType & { kind: "union" }),
|
||||
);
|
||||
registerReplay("streamTypedRefMaterializeAdapter", (host, args) =>
|
||||
streamTypedRefMaterializeAdapter(host, args[0] as IrType).snapshot.replace(/_materialize$/, ""),
|
||||
);
|
||||
registerReplay("streamFromArrayAdapter", (host, args) =>
|
||||
streamFromArrayAdapter(host, args[0] as IrType & { kind: "array" }),
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { type IrType, isRefCounted, typeKey } from "../../ir/ir.js";
|
||||
import { FN_ATTRS, releaseSym, retainSym, traceArg } from "./shapes.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { unionPeekLines, unionTagLines } from "./union-repr.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
|
||||
export function streamDataAdapter(
|
||||
host: LlvmEmitterContext,
|
||||
@@ -14,6 +15,7 @@ export function streamDataAdapter(
|
||||
if (sym) return sym;
|
||||
sym = `sc_ee_dad_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "streamDataAdapter", [cbT]);
|
||||
const p = cbT.params[0];
|
||||
if (cbT.params.length > 1) {
|
||||
throw new InternalCompilerError(
|
||||
@@ -207,12 +209,13 @@ export function streamDataAdapter(
|
||||
d.push(`}`, ``);
|
||||
}
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
export function streamDoneFnFor(
|
||||
host: LlvmEmitterContext,
|
||||
kind: "w" | "f" | "d" | "t" | "l",
|
||||
kind: "w" | "f" | "d" | "t" | "l" | "c",
|
||||
doneT: IrType & { kind: "func" },
|
||||
): string {
|
||||
const key = `sd:${kind}:${typeKey(doneT)}`;
|
||||
@@ -220,6 +223,7 @@ export function streamDoneFnFor(
|
||||
if (sym) return sym;
|
||||
sym = `sc_sd_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "streamDoneFnFor", [kind, doneT]);
|
||||
const errT = doneT.params[0];
|
||||
let errTag = -1;
|
||||
if (errT !== undefined) {
|
||||
@@ -256,19 +260,21 @@ export function streamDoneFnFor(
|
||||
}
|
||||
}
|
||||
const entry =
|
||||
kind === "w"
|
||||
? "scr_stream_write_done"
|
||||
: kind === "f"
|
||||
? "scr_stream_final_done"
|
||||
: kind === "d"
|
||||
? "scr_stream_destroy_done"
|
||||
: kind === "t"
|
||||
? valueData
|
||||
? "scr_stream_transform_done_value"
|
||||
: "scr_stream_transform_done"
|
||||
: valueData
|
||||
? "scr_stream_flush_done_value"
|
||||
: "scr_stream_flush_done";
|
||||
kind === "c"
|
||||
? "scr_stream_construct_done"
|
||||
: kind === "w"
|
||||
? "scr_stream_write_done"
|
||||
: kind === "f"
|
||||
? "scr_stream_final_done"
|
||||
: kind === "d"
|
||||
? "scr_stream_destroy_done"
|
||||
: kind === "t"
|
||||
? valueData
|
||||
? "scr_stream_transform_done_value"
|
||||
: "scr_stream_transform_done"
|
||||
: valueData
|
||||
? "scr_stream_flush_done_value"
|
||||
: "scr_stream_flush_done";
|
||||
const twoSlot = (kind === "t" || kind === "l") && !valueData;
|
||||
host.declare(
|
||||
`declare void @${entry}(ptr, ptr${twoSlot ? ", ptr, ptr" : valueData ? ", ptr" : ""})`,
|
||||
@@ -347,6 +353,7 @@ export function streamDoneFnFor(
|
||||
d.push(` call void ${releaseSym(host.shapeHost, dataT)}(ptr %d)`);
|
||||
d.push(` ret void`, `}`, ``);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -360,6 +367,7 @@ export function fsRenameThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
|
||||
if (sym) return sym;
|
||||
sym = `sc_fsren_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "fsRenameThunkFor", [cbT]);
|
||||
const param = cbT.params[0]!;
|
||||
const d: string[] = [
|
||||
`define internal void @${sym}(ptr %cb, ptr %err) ${FN_ATTRS} { ; fs.rename callback ${typeKey(cbT)}`,
|
||||
@@ -415,6 +423,7 @@ export function fsRenameThunkFor(host: LlvmEmitterContext, cbT: IrType & { kind:
|
||||
``,
|
||||
);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -447,6 +456,7 @@ export function cryptoIntThunkFor(
|
||||
if (sym) return sym;
|
||||
sym = `sc_cryptoint_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "cryptoIntThunkFor", [cbT]);
|
||||
const d: string[] = [
|
||||
`define internal void @${sym}(ptr %cb, double %value) ${FN_ATTRS} { ; randomInt callback ${typeKey(cbT)}`,
|
||||
`entry:`,
|
||||
@@ -489,6 +499,7 @@ export function cryptoIntThunkFor(
|
||||
``,
|
||||
);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -507,6 +518,7 @@ export function zlibBytesThunkFor(
|
||||
if (sym) return sym;
|
||||
sym = `sc_${family}bytes_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "zlibBytesThunkFor", [cbT, arrayBuffer, family, valueMode]);
|
||||
const d: string[] = [
|
||||
`define internal void @${sym}(ptr %cb, ptr %err, ptr %value) ${FN_ATTRS} { ; zlib bytes callback ${typeKey(cbT)}`,
|
||||
`entry:`,
|
||||
@@ -618,12 +630,13 @@ export function zlibBytesThunkFor(
|
||||
``,
|
||||
);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
export function streamCbThunkFor(
|
||||
host: LlvmEmitterContext,
|
||||
kind: "r" | "w" | "f" | "d" | "t" | "l" | "e",
|
||||
kind: "r" | "w" | "f" | "d" | "t" | "l" | "e" | "c" | "v",
|
||||
cbT: IrType,
|
||||
): string {
|
||||
if (cbT.kind !== "func")
|
||||
@@ -633,14 +646,17 @@ export function streamCbThunkFor(
|
||||
if (sym) return sym;
|
||||
sym = `sc_scb_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "streamCbThunkFor", [kind, cbT]);
|
||||
const runtimeParams =
|
||||
kind === "r"
|
||||
? ["ptr %s", "double %size"]
|
||||
: kind === "w" || kind === "t"
|
||||
? ["ptr %s", "ptr %chunk"]
|
||||
: kind === "d" || kind === "e"
|
||||
? ["ptr %s", "ptr %err"]
|
||||
: ["ptr %s"];
|
||||
: kind === "v"
|
||||
? ["ptr %s", "ptr %chunks"]
|
||||
: kind === "d" || kind === "e"
|
||||
? ["ptr %s", "ptr %err"]
|
||||
: ["ptr %s"];
|
||||
const declared = cbT.params;
|
||||
const hasThis = declared[0] !== undefined && declared[0].kind === "object";
|
||||
if (declared.length === 0) {
|
||||
@@ -649,7 +665,9 @@ export function streamCbThunkFor(
|
||||
);
|
||||
}
|
||||
const off = hasThis ? 1 : 0;
|
||||
const full = (kind === "r" ? 1 : kind === "w" || kind === "t" ? 3 : kind === "d" ? 2 : 1) + off;
|
||||
const full =
|
||||
(kind === "r" ? 1 : kind === "w" || kind === "t" ? 3 : kind === "d" || kind === "v" ? 2 : 1) +
|
||||
off;
|
||||
if (declared.length > full) {
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: stream '${kind}' callback declares ${declared.length} params (frontend must fence)`,
|
||||
@@ -697,6 +715,31 @@ export function streamCbThunkFor(
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (kind === "v" && pos === 0) {
|
||||
// writev's chunks: the batch array (borrowed from the runtime), as
|
||||
// a dyn value or converted to the declared type
|
||||
if (p.kind === "dyn") {
|
||||
host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
|
||||
d.push(` %vc${i} = call ptr @scr_dyn_retain_v(ptr %chunks)`);
|
||||
passed.push(`ptr %vc${i}`);
|
||||
} else {
|
||||
const helper = host.dyn.dynCheckHelper(p, false);
|
||||
const ty = host.llType(p);
|
||||
host.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`);
|
||||
d.push(
|
||||
` %vt${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %chunks, ptr null)`,
|
||||
` %vtbad${i} = call zeroext i1 @scr_exc_pending()`,
|
||||
` br i1 %vtbad${i}, label %vtfail${i}, label %vtok${i}`,
|
||||
`vtfail${i}:`,
|
||||
...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []),
|
||||
` ret void`,
|
||||
`vtok${i}:`,
|
||||
);
|
||||
passed.push(`${ty} %vt${i}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const isChunkPos = (kind === "w" || kind === "t") && pos === 0;
|
||||
const isEncPos = (kind === "w" || kind === "t") && pos === 1;
|
||||
const isErrPos = (kind === "d" || kind === "e") && pos === 0;
|
||||
@@ -705,7 +748,7 @@ export function streamCbThunkFor(
|
||||
? false
|
||||
: kind === "w" || kind === "t"
|
||||
? pos === 2
|
||||
: kind === "f" || kind === "l"
|
||||
: kind === "f" || kind === "l" || kind === "c"
|
||||
? pos === 0
|
||||
: pos === 1;
|
||||
if (p.kind === "dyn") {
|
||||
@@ -751,7 +794,7 @@ export function streamCbThunkFor(
|
||||
d.push(` br label %ego${i}`, `ego${i}:`, ` %edv${i} = load ptr, ptr %edslot${i}`);
|
||||
passed.push(`ptr %edv${i}`);
|
||||
} else if (isDonePos) {
|
||||
const glue = `scr_stream_done_dyn_${kind}`;
|
||||
const glue = `scr_stream_done_dyn_${kind === "v" ? "w" : kind}`;
|
||||
host.declare(`declare ptr @${glue}(ptr, ptr, ${host.sizeType})`);
|
||||
host.declare(`declare ptr @scr_dyn_new_func(ptr, ptr, i32, ptr, ptr)`);
|
||||
const clo = doneClosure(`@${glue}`, `dd${i}`);
|
||||
@@ -872,7 +915,8 @@ export function streamCbThunkFor(
|
||||
);
|
||||
passed.push(`ptr %euv${i}`);
|
||||
} else if (isDonePos) {
|
||||
const doneKind = kind as "w" | "f" | "d" | "t" | "l"; // "e" has no done position
|
||||
// "e" has no done position; writev completes as a write
|
||||
const doneKind = (kind === "v" ? "w" : kind) as "w" | "f" | "d" | "t" | "l" | "c";
|
||||
if (p.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: stream done callback not a func");
|
||||
const doneFn = host.streamDoneFnFor(doneKind, p);
|
||||
@@ -901,5 +945,35 @@ export function streamCbThunkFor(
|
||||
}
|
||||
d.push(` ret void`, `}`, ``);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
registerReplay("streamDataAdapter", (host, args) =>
|
||||
streamDataAdapter(host, args[0] as IrType & { kind: "func" }),
|
||||
);
|
||||
registerReplay("streamDoneFnFor", (host, args) =>
|
||||
streamDoneFnFor(
|
||||
host,
|
||||
args[0] as "w" | "f" | "d" | "t" | "l",
|
||||
args[1] as IrType & { kind: "func" },
|
||||
),
|
||||
);
|
||||
registerReplay("fsRenameThunkFor", (host, args) =>
|
||||
fsRenameThunkFor(host, args[0] as IrType & { kind: "func" }),
|
||||
);
|
||||
registerReplay("cryptoIntThunkFor", (host, args) =>
|
||||
cryptoIntThunkFor(host, args[0] as IrType & { kind: "func" }),
|
||||
);
|
||||
registerReplay("zlibBytesThunkFor", (host, args) =>
|
||||
zlibBytesThunkFor(
|
||||
host,
|
||||
args[0] as IrType & { kind: "func" },
|
||||
args[1] as boolean,
|
||||
args[2] as string,
|
||||
args[3] as BytesThunkValue,
|
||||
),
|
||||
);
|
||||
registerReplay("streamCbThunkFor", (host, args) =>
|
||||
streamCbThunkFor(host, args[0] as "r" | "w" | "f" | "d" | "t" | "l" | "e", args[1] as IrType),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { IrExpr, IrStmt } from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import { MAX_LOOP_NODES, SIMPLE_STATEMENTS } from "./speculative-integers.js";
|
||||
|
||||
/** Int32 kind loops.
|
||||
*
|
||||
* An int32 array with checked stores (ir/int32-slots.ts) holds int32 slots
|
||||
* (or doubles, when its fill was short) until a store of a value that is
|
||||
* not an int32 widens it to doubles in place, so an access to it tests its
|
||||
* element kind first (LlEmitter.emitByElementKind). An innermost loop that
|
||||
* accesses such arrays tests their kinds once on entry instead
|
||||
* (emitLoopCopies). The fast copy runs when every one of them holds int32
|
||||
* slots and reads and writes them as int32 arrays; the wide copy runs when
|
||||
* every one holds doubles and accesses them as double arrays; a loop over
|
||||
* several arrays whose kinds differ runs the mixed copy, which tests the
|
||||
* kinds at each access. A store of the fast copy whose value is not an
|
||||
* int32 branches to the same statement in a third copy, which re-executes
|
||||
* it with kind tests (widening the array) and finishes the iteration, then
|
||||
* enters the mixed copy, or the wide copy when the loop has one array.
|
||||
* Re-execution is exact because such a store's statement evaluates nothing
|
||||
* but locals, literals, number arithmetic and element reads before it
|
||||
* writes.
|
||||
*
|
||||
* Nothing else can widen these arrays: they never reach code other than
|
||||
* the compiled element operations of their own function
|
||||
* (ir/int32-arrays.ts). */
|
||||
export interface Int32KindLoopPlan {
|
||||
/** The arrays the fast copy reads and writes as int32 arrays, by local,
|
||||
* with one of their references in the loop. */
|
||||
arrays: Map<string, IrExpr>;
|
||||
/** Element writes and pushes whose value may not be an int32: the
|
||||
* statements the fast copy may leave at. */
|
||||
writes: Set<IrStmt>;
|
||||
}
|
||||
|
||||
type KindLoop = Extract<IrStmt, { kind: "for" | "while" | "doWhile" }>;
|
||||
|
||||
/** A number or boolean expression whose evaluation has no effect: locals,
|
||||
* literals, arithmetic, comparisons and element reads of local arrays. */
|
||||
function isPure(e: IrExpr): boolean {
|
||||
if (e.type.kind !== "f64" && e.type.kind !== "bool") return false;
|
||||
switch (e.kind) {
|
||||
case "numLit":
|
||||
case "boolLit":
|
||||
case "varRef":
|
||||
return true;
|
||||
case "bin":
|
||||
return (
|
||||
(e.left.type.kind === "f64" || e.left.type.kind === "bool") &&
|
||||
isPure(e.left) &&
|
||||
isPure(e.right)
|
||||
);
|
||||
case "unary":
|
||||
return isPure(e.operand);
|
||||
case "ternary":
|
||||
return isPure(e.cond) && isPure(e.then) && isPure(e.else_);
|
||||
case "arrayGet":
|
||||
case "arrayHas":
|
||||
return e.arr.kind === "varRef" && isPure(e.index);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The plan for `loop`, or null. `checked` names the int32 array locals
|
||||
* with checked stores of the function; `provenInt32` the values the static
|
||||
* range analysis proves int32s (stores that never leave the fast copy). */
|
||||
export function planInt32KindLoop(
|
||||
loop: KindLoop,
|
||||
checked: (localId: string) => boolean,
|
||||
provenInt32: (e: IrExpr) => boolean,
|
||||
): Int32KindLoopPlan | null {
|
||||
const arrays = new Map<string, IrExpr>();
|
||||
const writes = new Set<IrStmt>();
|
||||
const assigned = new Set<string>();
|
||||
let nodes = 0;
|
||||
let rejected = false;
|
||||
const reject = (): false => {
|
||||
rejected = true;
|
||||
return false;
|
||||
};
|
||||
const array = (e: IrExpr): string | null =>
|
||||
e.kind === "varRef" && checked(e.localId) ? e.localId : null;
|
||||
const push = (e: IrExpr): (IrExpr & { kind: "arrIntrinsic" }) | null =>
|
||||
e.kind === "arrIntrinsic" && e.method === "push" && array(e.receiver) !== null ? e : null;
|
||||
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (rejected) return false;
|
||||
nodes++;
|
||||
if (e.kind === "varRef" && checked(e.localId) && !arrays.has(e.localId))
|
||||
arrays.set(e.localId, e);
|
||||
// A push the statement walk did not take as a write: its values must
|
||||
// be proven int32s, since it cannot leave the fast copy.
|
||||
const pushed = push(e);
|
||||
if (pushed && !pushed.args.every(provenInt32)) return reject();
|
||||
return everyExprChild(e, expr, nested);
|
||||
};
|
||||
// Statements inside expressions run mid-expression, where the fast copy
|
||||
// cannot leave: they may not write these arrays.
|
||||
const nested = (s: IrStmt): boolean => {
|
||||
nodes++;
|
||||
if (s.kind === "assign" || s.kind === "varDecl") assigned.add(s.localId);
|
||||
if (s.kind === "arraySet" && array(s.arr) !== null && !provenInt32(s.value)) return reject();
|
||||
return everyStmtChild(s, expr, nested);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (rejected) return false;
|
||||
nodes++;
|
||||
if (!SIMPLE_STATEMENTS.has(s.kind)) return reject();
|
||||
if (s.kind === "assign" || s.kind === "varDecl") assigned.add(s.localId);
|
||||
if (s.kind === "arraySet" && array(s.arr) !== null && !provenInt32(s.value)) {
|
||||
if (!isPure(s.index) || !isPure(s.value)) return reject();
|
||||
writes.add(s);
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
}
|
||||
const pushed = s.kind === "exprStmt" ? push(s.expr) : null;
|
||||
if (pushed && !pushed.args.every(provenInt32)) {
|
||||
if (!pushed.args.every(isPure)) return reject();
|
||||
writes.add(s);
|
||||
expr(pushed.receiver);
|
||||
pushed.args.forEach(expr);
|
||||
return !rejected;
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
|
||||
if (loop.cond) expr(loop.cond);
|
||||
if (loop.kind === "for" && loop.update) stmt(loop.update);
|
||||
for (const s of loop.body) stmt(s);
|
||||
if (rejected || nodes > MAX_LOOP_NODES || arrays.size === 0) return null;
|
||||
// A loop that rebinds one of the arrays could reach another array of
|
||||
// either kind.
|
||||
for (const id of arrays.keys()) if (assigned.has(id)) return null;
|
||||
return { arrays, writes };
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { F64, arrayOf, bytesOf, type IrExpr, type IrModule, type IrStmt } from "../../ir/ir.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
|
||||
const loc = { file: "integer-or-nan-indexes.ts", start: 0, end: 0 };
|
||||
const nums = arrayOf(F64);
|
||||
const u8 = bytesOf("u8");
|
||||
const ref = (localId: string, type: IrExpr["type"]): IrExpr => ({
|
||||
kind: "varRef",
|
||||
localId,
|
||||
type,
|
||||
loc,
|
||||
});
|
||||
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
|
||||
/** `t[i]!`: the element, or NaN for an index that misses. */
|
||||
const byteRead = (index: IrExpr): IrExpr => ({
|
||||
kind: "bytesIntrinsic",
|
||||
method: "get",
|
||||
receiver: ref("t", u8),
|
||||
args: [index],
|
||||
type: F64,
|
||||
loc,
|
||||
invalidNaN: true,
|
||||
});
|
||||
|
||||
function module(body: IrStmt[]): IrModule {
|
||||
return {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "__main",
|
||||
unions: [],
|
||||
functions: [
|
||||
{ name: "__main", params: [], returnType: F64, locals: [], body: [], loc },
|
||||
{
|
||||
name: "read",
|
||||
params: [
|
||||
{ name: "a", localId: "a", type: nums },
|
||||
{ name: "t", localId: "t", type: u8 },
|
||||
{ name: "i", localId: "i", type: F64 },
|
||||
],
|
||||
returnType: F64,
|
||||
locals: [
|
||||
{ id: "a", name: "a", type: nums, mutable: false },
|
||||
{ id: "t", name: "t", type: u8, mutable: false },
|
||||
{ id: "i", name: "i", type: F64, mutable: false },
|
||||
{ id: "c", name: "c", type: F64, mutable: true },
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function body(llvm: string): string {
|
||||
return /^define internal [^\n]*@sc_bf_read(?:\.i32)?\([^]*?^}/m.exec(llvm)![0];
|
||||
}
|
||||
|
||||
const count = (text: string, pattern: RegExp): number => text.match(pattern)?.length ?? 0;
|
||||
|
||||
test("an index bound to a byte read that may miss checks only that it is ordered", () => {
|
||||
const llvm = emitLlvmModule(
|
||||
module([
|
||||
{ kind: "varDecl", localId: "c", init: byteRead(ref("i", F64)), loc },
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left: { kind: "arrayGet", arr: ref("a", nums), index: ref("c", F64), type: F64, loc },
|
||||
right: {
|
||||
kind: "bytesIntrinsic",
|
||||
method: "get",
|
||||
receiver: ref("t", u8),
|
||||
args: [{ kind: "bin", op: "+", left: ref("c", F64), right: num(1), type: F64, loc }],
|
||||
type: F64,
|
||||
loc,
|
||||
invalidNaN: true,
|
||||
},
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
]),
|
||||
);
|
||||
const fn = body(llvm);
|
||||
// Only the parameter index `i` round-trips through a double.
|
||||
expect(count(fn, /fcmp oeq double/g)).toBe(1);
|
||||
expect(count(fn, /fcmp ord double/g)).toBe(2);
|
||||
});
|
||||
|
||||
test("a local also written an ordinary number keeps the exact index check", () => {
|
||||
const llvm = emitLlvmModule(
|
||||
module([
|
||||
{ kind: "varDecl", localId: "c", init: byteRead(ref("i", F64)), loc },
|
||||
{ kind: "assign", localId: "c", value: ref("i", F64), loc },
|
||||
{
|
||||
kind: "return",
|
||||
value: { kind: "arrayGet", arr: ref("a", nums), index: ref("c", F64), type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
]),
|
||||
);
|
||||
const fn = body(llvm);
|
||||
expect(count(fn, /fcmp oeq double/g)).toBe(2);
|
||||
expect(fn).not.toContain("fcmp ord double");
|
||||
});
|
||||
|
||||
test("a local whose writes grow from its own value keeps the exact index check", () => {
|
||||
const llvm = emitLlvmModule(
|
||||
module([
|
||||
{ kind: "varDecl", localId: "c", init: byteRead(ref("i", F64)), loc },
|
||||
{
|
||||
kind: "assign",
|
||||
localId: "c",
|
||||
value: { kind: "bin", op: "+", left: ref("c", F64), right: num(1), type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "return",
|
||||
value: { kind: "arrayGet", arr: ref("a", nums), index: ref("c", F64), type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
]),
|
||||
);
|
||||
const fn = body(llvm);
|
||||
expect(count(fn, /fcmp oeq double/g)).toBe(2);
|
||||
expect(fn).not.toContain("fcmp ord double");
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IrExpr, IrType } from "../../ir/ir.js";
|
||||
import type { IntegerRange } from "../../ir/integer-ranges.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import type { LlValue, LlvmEmitterContext, WideInteger } from "./expr-context.js";
|
||||
|
||||
export type LlInteger = NonNullable<LlValue["integer"]>;
|
||||
|
||||
@@ -51,6 +51,89 @@ export function exactInteger(
|
||||
return { name, type: "i64", signed: true, range };
|
||||
}
|
||||
|
||||
/** Doubles hold every integer of smaller magnitude exactly. */
|
||||
const EXACT_DOUBLES = 2 ** 53;
|
||||
/** Wide views stay within this magnitude: a product of two int32s reaches
|
||||
* 2^62, one exact term of at most 2^53 may be added to it, and no i64
|
||||
* result wraps. */
|
||||
const WIDE_BOUND = 2 ** 62 + 2 ** 53;
|
||||
/** Array and string indexes are below 2^32 - 1: a wide view exact below
|
||||
* this many is exact at every index a length can admit. */
|
||||
const INDEX_LIMIT = 2 ** 32;
|
||||
|
||||
function magnitude(range: IntegerRange): number {
|
||||
return Math.max(Math.abs(range.min), Math.abs(range.max));
|
||||
}
|
||||
|
||||
/** The wide view of `l op r` for `+`, `-` and `*` when the exact operands
|
||||
* have no proven result range (their product or sum may pass 2^53).
|
||||
*
|
||||
* - A product of two exact integers equals its double whenever its
|
||||
* magnitude is below 2^53: the double product of integers is exact there.
|
||||
* - A sum or difference of two exact integers likewise.
|
||||
* - A sum or difference of a wide view (exact below T) and an exact integer
|
||||
* of magnitude at most B equals its double below min(T - B, 2^53): the
|
||||
* view's magnitude is then below T, so its double is exact, and the
|
||||
* double sum of two exact integers below 2^53 is exact.
|
||||
*
|
||||
* Every i64 result stays within 2^62 + 2^53 in magnitude, so none wraps. A view
|
||||
* exact below fewer than 2^32 helps no index and is not kept. */
|
||||
export function wideArithmetic(
|
||||
host: LlvmEmitterContext,
|
||||
op: string,
|
||||
l: LlValue,
|
||||
li: LlInteger | null,
|
||||
r: LlValue,
|
||||
ri: LlInteger | null,
|
||||
): WideInteger | null {
|
||||
if (op !== "+" && op !== "-" && op !== "*") return null;
|
||||
const view = (value: LlValue, integer: LlInteger | null): WideInteger | null =>
|
||||
integer
|
||||
? { name: "", below: Infinity, bound: magnitude(integer.range) }
|
||||
: (value.wideInteger ?? null);
|
||||
const lv = view(l, li),
|
||||
rv = view(r, ri);
|
||||
if (!lv || !rv) return null;
|
||||
let below: number, bound: number;
|
||||
if (op === "*") {
|
||||
if (lv.below !== Infinity || rv.below !== Infinity) return null;
|
||||
below = EXACT_DOUBLES;
|
||||
bound = lv.bound * rv.bound;
|
||||
} else if (lv.below === Infinity && rv.below === Infinity) {
|
||||
below = EXACT_DOUBLES;
|
||||
bound = lv.bound + rv.bound;
|
||||
} else if (lv.below === Infinity || rv.below === Infinity) {
|
||||
const [exact, wide] = lv.below === Infinity ? [lv, rv] : [rv, lv];
|
||||
below = Math.min(wide.below - exact.bound, EXACT_DOUBLES);
|
||||
bound = exact.bound + wide.bound;
|
||||
} else return null;
|
||||
if (!(bound <= WIDE_BOUND) || below < INDEX_LIMIT) return null;
|
||||
const operand = (value: LlValue, integer: LlInteger | null): string =>
|
||||
integer ? widenInteger(host, integer) : value.wideInteger!.name;
|
||||
const left = operand(l, li),
|
||||
right = operand(r, ri);
|
||||
const name = host.B.tmp();
|
||||
host.B.line(
|
||||
`${name} = ${op === "+" ? "add" : op === "-" ? "sub" : "mul"} nsw i64 ${left}, ${right}`,
|
||||
);
|
||||
return { name, below, bound };
|
||||
}
|
||||
|
||||
/** An i64 index for a consumer that compares it, unsigned, with a length or
|
||||
* capacity below 2^32 and takes the double's path otherwise: the exact
|
||||
* integer of `value`, or its wide view, which equals the double at every
|
||||
* such index. Null when the index needs the double round trip. */
|
||||
export function indexInteger(
|
||||
host: LlvmEmitterContext,
|
||||
value: LlValue,
|
||||
expr?: IrExpr,
|
||||
): string | null {
|
||||
const integer = exactInteger(host, value, expr);
|
||||
if (integer) return widenInteger(host, integer);
|
||||
const wide = value.wideInteger;
|
||||
return wide && wide.below >= INDEX_LIMIT ? wide.name : null;
|
||||
}
|
||||
|
||||
export function widenInteger(host: LlvmEmitterContext, value: LlInteger): string {
|
||||
if (value.type === "i64") return value.name;
|
||||
const wide = host.B.tmp();
|
||||
|
||||
@@ -732,7 +732,7 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue {
|
||||
|
||||
/** dynInvoke's method read on an ordinary object through the site's
|
||||
* cache, else scr_dyn_prepare_method. Returns the owned method (or NULL,
|
||||
* which scr_dyn_invoke_prepared answers with the full dispatch). */
|
||||
* which scr_dyn_invoke_prepared_id answers with the full dispatch). */
|
||||
export function emitJsIcMethod(host: Host, e: ExprOf<"dynInvoke">, receiver: string): string {
|
||||
const B = host.B;
|
||||
const method = e.method;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/* Key-order dispatch for shapes whose values record the order their keys
|
||||
* were written in (IrRecordShape.orderVariants): an emitter that walks a
|
||||
* record's keys emits one walk per order and branches on the value's
|
||||
* KEY_ORDER_FIELD. */
|
||||
import type { IrRecordShape } from "../../ir/ir.js";
|
||||
import { KEY_ORDER_FIELD, shapeKeyOrders } from "../../ir/ir.js";
|
||||
import { mangleRecordStruct } from "../mangle.js";
|
||||
import type { BlockBuilder } from "./blocks.js";
|
||||
|
||||
/** Emits `walk(order, index)` for record `rec` of `shape`, once per order behind a
|
||||
* switch on the value's order index when the shape has several. Each walk
|
||||
* starts in its own block, and the code after this call continues in the
|
||||
* block they all branch to. */
|
||||
export function emitPerKeyOrder(
|
||||
B: BlockBuilder,
|
||||
shape: IrRecordShape,
|
||||
fieldBase: number,
|
||||
rec: string,
|
||||
walk: (order: readonly string[], index: number) => void,
|
||||
): void {
|
||||
const orders = shapeKeyOrders(shape);
|
||||
if (orders.length === 1) {
|
||||
walk(orders[0]!, 0);
|
||||
return;
|
||||
}
|
||||
const index = shape.fields.findIndex((f) => f.name === KEY_ORDER_FIELD) + fieldBase;
|
||||
const p = B.tmp();
|
||||
const n = B.tmp();
|
||||
const i = B.tmp();
|
||||
B.line(
|
||||
`${p} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr ${rec}, i64 0, i32 ${index} ; .${KEY_ORDER_FIELD}`,
|
||||
);
|
||||
B.line(`${n} = load double, ptr ${p}`);
|
||||
B.line(`${i} = fptosi double ${n} to i32`);
|
||||
const labels = orders.map(() => B.newLabel("ko"));
|
||||
const join = B.newLabel("ko.join");
|
||||
B.terminate(
|
||||
`switch i32 ${i}, label %${labels[0]} [ ${labels
|
||||
.slice(1)
|
||||
.map((label, k) => `i32 ${k + 1}, label %${label}`)
|
||||
.join(" ")} ]`,
|
||||
);
|
||||
orders.forEach((order, k) => {
|
||||
B.startBlock(labels[k]!);
|
||||
walk(order, k);
|
||||
B.br(join);
|
||||
});
|
||||
B.startBlock(join);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { typeKey } from "../../ir/ir.js";
|
||||
import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { abiValue, callbackType, finishRuntimeCall, ptr } from "./lib-abi.js";
|
||||
import { FN_ATTRS } from "./shapes.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
|
||||
/** CONNECT transfers owned request/socket/response values to the listener. */
|
||||
function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): string {
|
||||
@@ -18,6 +19,7 @@ function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): str
|
||||
if (previous !== undefined) return previous;
|
||||
const symbol = `sc_connect_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, symbol);
|
||||
host.journal?.allocated(symbol, "connectAdapter", [cb, h2]);
|
||||
const retain = h2 ? "scr_http_res_retain_v" : "scr_net_sock_retain_v";
|
||||
const release = h2 ? "scr_http_res_release_v" : "scr_net_sock_release_v";
|
||||
host.declare(`declare ptr @${retain}(ptr)`);
|
||||
@@ -37,6 +39,7 @@ function connectAdapter(host: LlvmEmitterContext, cb: LlValue, h2: boolean): str
|
||||
"}",
|
||||
"",
|
||||
);
|
||||
host.journal?.built();
|
||||
return symbol;
|
||||
}
|
||||
|
||||
@@ -70,3 +73,7 @@ export function emitConnectListener(host: LlvmEmitterContext, e: LibCallExpr): L
|
||||
a[2]!,
|
||||
]);
|
||||
}
|
||||
|
||||
registerReplay("connectAdapter", (host, args) =>
|
||||
connectAdapter(host, args[0] as LlValue, args[1] as boolean),
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ptr,
|
||||
} from "./lib-abi.js";
|
||||
import { FN_ATTRS } from "./shapes.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
|
||||
function messageAdapter(host: LlvmEmitterContext, type: IrType): string {
|
||||
if (type.kind !== "record")
|
||||
@@ -20,6 +21,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string {
|
||||
if (previous !== undefined) return previous;
|
||||
const symbol = `sc_dgram_message_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, symbol);
|
||||
host.journal?.allocated(symbol, "messageAdapter", [type]);
|
||||
const shape = host.recordsById.get(type.shapeId);
|
||||
if (shape === undefined) throw new InternalCompilerError("missing datagram remote info shape");
|
||||
const lines: string[] = [];
|
||||
@@ -48,6 +50,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string {
|
||||
"}",
|
||||
"",
|
||||
);
|
||||
host.journal?.built();
|
||||
return symbol;
|
||||
}
|
||||
|
||||
@@ -67,6 +70,7 @@ function lookupAdapter(host: LlvmEmitterContext, cb: LlValue): string {
|
||||
if (previous !== undefined) return previous;
|
||||
const symbol = `sc_dns_lookup_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, symbol);
|
||||
host.journal?.allocated(symbol, "lookupAdapter", [cb]);
|
||||
host.declare("declare ptr @scr_error_new(i32, ptr)");
|
||||
host.declare("declare ptr @scr_error_retain_v(ptr)");
|
||||
host.declare("declare void @scr_error_release_v(ptr)");
|
||||
@@ -93,6 +97,7 @@ function lookupAdapter(host: LlvmEmitterContext, cb: LlValue): string {
|
||||
"}",
|
||||
"",
|
||||
);
|
||||
host.journal?.built();
|
||||
return symbol;
|
||||
}
|
||||
|
||||
@@ -105,6 +110,10 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
|
||||
"dgram.sendStr": "scr_dgram_send_str",
|
||||
"dgram.sendBytes": "scr_dgram_send_bytes",
|
||||
"dgram.sendChk": "scr_dgram_send_chk",
|
||||
"dgram.unixSocket": "scr_dgram_unix_socket",
|
||||
"dgram.unixConnect": "scr_dgram_unix_connect",
|
||||
"dgram.unixSend": "scr_dgram_unix_send",
|
||||
"dgram.unixClose": "scr_dgram_unix_close",
|
||||
"dgram.ref": "scr_dgram_ref",
|
||||
"dgram.unref": "scr_dgram_unref",
|
||||
"dgram.setBroadcast": "scr_dgram_set_broadcast",
|
||||
@@ -250,3 +259,6 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
|
||||
throw new InternalCompilerError(`unhandled datagram call ${e.fn}`);
|
||||
}
|
||||
}
|
||||
|
||||
registerReplay("messageAdapter", (host, args) => messageAdapter(host, args[0] as IrType));
|
||||
registerReplay("lookupAdapter", (host, args) => lookupAdapter(host, args[0] as LlValue));
|
||||
|
||||
@@ -297,6 +297,41 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
B.line(`${result} = load i1, ptr ${slot}`);
|
||||
return { name: result, type: e.type };
|
||||
}
|
||||
if (e.fn === "dyn.classIs") {
|
||||
// Only a class constructor's function value can match. Expose the
|
||||
// function tag to LLVM so a chain of constructor probes (a static
|
||||
// property bag's dispatch) costs one test for every other value, and
|
||||
// borrow the receiver across a literal key as the capsule probes do.
|
||||
const literal = e.args[1]!.kind === "strLit";
|
||||
const value = literal ? host.emitReadReceiver(e.args[0]!) : host.emitExpr(e.args[0]!);
|
||||
const key = host.emitExpr(e.args[1]!);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca i1`);
|
||||
B.line(`store i1 false, ptr ${slot}`);
|
||||
const present = B.tmp();
|
||||
const inspect = B.newLabel("ctor.inspect");
|
||||
const compare = B.newLabel("ctor.compare");
|
||||
const done = B.newLabel("ctor.done");
|
||||
B.line(`${present} = icmp ne ptr ${value.name}, null`);
|
||||
B.condBr(present, inspect, done);
|
||||
B.startBlock(inspect);
|
||||
const kind = host.dynKind(value.name);
|
||||
const fn = B.tmp();
|
||||
B.line(`${fn} = icmp eq i32 ${kind}, ${DYN_KIND.FUNC}`);
|
||||
B.condBr(fn, compare, done);
|
||||
B.startBlock(compare);
|
||||
host.declare("declare zeroext i1 @scr_dyn_class_is_key(ptr, ptr)");
|
||||
const matches = B.tmp();
|
||||
B.line(
|
||||
`${matches} = call zeroext i1 @scr_dyn_class_is_key(ptr ${value.name}, ptr ${key.name})`,
|
||||
);
|
||||
B.line(`store i1 ${matches}, ptr ${slot}`);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = load i1, ptr ${slot}`);
|
||||
return { name: result, type: e.type };
|
||||
}
|
||||
if (e.fn === "crypto.randomFillCb" || e.fn === "crypto.randomFillCbDyn") {
|
||||
host.usesTimers = true;
|
||||
const args = e.args.map((arg) => host.emitExpr(arg));
|
||||
@@ -553,7 +588,11 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
// which also takes those streams.
|
||||
e.fn === "stream.unboxDyn" && host.usesDynBuiltinStream
|
||||
? "scr_dyn_builtin_stream_unbox"
|
||||
: LIB_FN_SYMS[e.fn]);
|
||||
: // for await over a checked-dynamic value iterates a native
|
||||
// stream behind it (scr_dyn_for_await_stream.c)
|
||||
e.fn === "dyn.asyncIteratorOpen" && host.dynForAwaitStreams
|
||||
? "scr_stream_dyn_async_iterator_open"
|
||||
: LIB_FN_SYMS[e.fn]);
|
||||
if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
|
||||
const args = READ_ONLY_STRING_LIB_FNS.has(e.fn)
|
||||
? emitBorrowedInputs(host, e.args)
|
||||
@@ -734,7 +773,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
if (e.fn === "process.nextTick") return host.emitAsyncContextLibCall(e);
|
||||
if (
|
||||
e.fn === "sp.pipeline" ||
|
||||
e.fn === "sp.pipelineStages" ||
|
||||
e.fn.startsWith("sp.pipelineStages") ||
|
||||
e.fn.startsWith("sp.pipelineOpts") ||
|
||||
e.fn.startsWith("sp.finishedOpts")
|
||||
)
|
||||
@@ -858,6 +897,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
return host.emitGenericLibCall(e);
|
||||
case "weakMap":
|
||||
case "weakSet":
|
||||
case "finalizationRegistry":
|
||||
case "ffi":
|
||||
case "sharedArrayBuffer":
|
||||
case "arrayBuffer":
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
requestHandler,
|
||||
} from "./lib-abi.js";
|
||||
import { FN_ATTRS } from "./shapes.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
|
||||
/** The runtime owns the answer closure; its arguments arrive owned from the caller. */
|
||||
function sniAnswer(host: LlvmEmitterContext, type: IrType): string {
|
||||
@@ -22,6 +23,7 @@ function sniAnswer(host: LlvmEmitterContext, type: IrType): string {
|
||||
if (previous !== undefined) return previous;
|
||||
const symbol = `sc_sni_answer_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, symbol);
|
||||
host.journal?.allocated(symbol, "sniAnswer", [type]);
|
||||
const params = ["ptr %self"],
|
||||
body: string[] = [];
|
||||
let hasError = "false",
|
||||
@@ -82,6 +84,7 @@ function sniAnswer(host: LlvmEmitterContext, type: IrType): string {
|
||||
"}",
|
||||
"",
|
||||
);
|
||||
host.journal?.built();
|
||||
return symbol;
|
||||
}
|
||||
|
||||
@@ -318,3 +321,5 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa
|
||||
const handler = thunk === null ? [a[1]!] : callbackAdapter(host, cb, thunk, params);
|
||||
return finishRuntimeCall(host, e, runtime, [a[0]!, ...handler, ...a.slice(2)]);
|
||||
}
|
||||
|
||||
registerReplay("sniAnswer", (host, args) => sniAnswer(host, args[0] as IrType));
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { typeKey } from "../../ir/ir.js";
|
||||
import { type IrType, typeKey } from "../../ir/ir.js";
|
||||
import { mangleRecordRelease, mangleRecordStruct } from "../mangle.js";
|
||||
import type { LibCallExpr, LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { abiValue, callbackType, finishRuntimeCall, ptr } from "./lib-abi.js";
|
||||
import { FN_ATTRS } from "./shapes.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
|
||||
export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const args = e.args.map((arg) => host.emitExpr(arg)),
|
||||
@@ -12,6 +13,18 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
answer = lookup.params[2];
|
||||
if (answer?.kind !== "func" || answer.params.length !== 2)
|
||||
throw new InternalCompilerError("lookup answer must have two parameters");
|
||||
const symbol = lookupAnswer(host, answer);
|
||||
host.usesTimers = true;
|
||||
host.toHeap(cb);
|
||||
return finishRuntimeCall(host, e, "scr_net_connect_lookup", [
|
||||
...args.map((arg) => abiValue(host, arg)),
|
||||
ptr("@" + symbol),
|
||||
]);
|
||||
}
|
||||
|
||||
/** The thunk that hands a lookup's answer (error, addresses) to the
|
||||
* runtime, one per answer signature. */
|
||||
function lookupAnswer(host: LlvmEmitterContext, answer: IrType & { kind: "func" }): string {
|
||||
const error = answer.params[0]!,
|
||||
addresses = answer.params[1]!;
|
||||
if (error.kind !== "union" || addresses.kind !== "array" || addresses.elem.kind !== "record")
|
||||
@@ -26,6 +39,7 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
if (symbol === undefined) {
|
||||
symbol = `sc_lookup_answer_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, symbol);
|
||||
host.journal?.allocated(symbol, "lookupAnswer", [answer]);
|
||||
for (const declaration of [
|
||||
"declare ptr @scr_str_retain_v(ptr)",
|
||||
"declare void @scr_str_release(ptr)",
|
||||
@@ -84,11 +98,11 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
"}",
|
||||
"",
|
||||
);
|
||||
host.journal?.built();
|
||||
}
|
||||
host.usesTimers = true;
|
||||
host.toHeap(cb);
|
||||
return finishRuntimeCall(host, e, "scr_net_connect_lookup", [
|
||||
...args.map((arg) => abiValue(host, arg)),
|
||||
ptr("@" + symbol),
|
||||
]);
|
||||
return symbol;
|
||||
}
|
||||
|
||||
registerReplay("lookupAnswer", (host, args) =>
|
||||
lookupAnswer(host, args[0] as IrType & { kind: "func" }),
|
||||
);
|
||||
|
||||
@@ -44,10 +44,12 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"weakSet.is": "scr_weak_set_is",
|
||||
"weakMap.new": "scr_weak_map_new",
|
||||
"weakSet.new": "scr_weak_set_new",
|
||||
"finalizationRegistry.new": "scr_finalization_registry_new",
|
||||
"finalizationRegistry.is": "scr_finalization_registry_is",
|
||||
"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",
|
||||
@@ -132,6 +134,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"error.setCodeDyn": "scr_error_set_code_dyn",
|
||||
"error.assign": "scr_error_assign_dyn",
|
||||
"error.hasKey": "scr_error_has_key",
|
||||
"error.ownAccessor": "scr_error_own_accessor",
|
||||
"error.accessorGet": "scr_error_accessor_get",
|
||||
"error.accessorSet": "scr_error_accessor_set",
|
||||
"error.hasCode": "scr_error_has_code",
|
||||
"error.defineCause": "scr_error_define_cause",
|
||||
"error.deleteCause": "scr_error_delete_cause",
|
||||
@@ -579,6 +584,11 @@ 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.getCipherInfo": "scr_crypto_get_cipher_info",
|
||||
"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 +764,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",
|
||||
@@ -886,6 +897,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
// ambient-this read never throw. The fs dyn read is the sync-fs story.
|
||||
"fetch.start": "scr_fetch_static",
|
||||
"fetch.responseNew": "scr_fetch_response_new",
|
||||
"fetch.responseLight": "scr_fetch_response_light",
|
||||
"fetch.responseJsonStatic": "scr_fetch_response_json_static",
|
||||
"fetch.input": "scr_fetch_input",
|
||||
"abort.controllerNew": "scr_fetch_abort_controller_new",
|
||||
@@ -966,6 +978,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.typedRefIs": "scr_dyn_typed_ref_is_key",
|
||||
"dyn.classIs": "scr_dyn_class_is_key",
|
||||
"dyn.iterPack": "scr_dyn_iter_pack",
|
||||
"dyn.mathFold": "scr_dyn_math_fold",
|
||||
"dyn.destructureRest": "scr_dyn_destructure_rest",
|
||||
"dyn.iterator": "scr_dyn_iterator",
|
||||
"dyn.asyncIteratorOpen": "scr_dyn_async_iterator_open",
|
||||
@@ -1121,11 +1134,24 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
// chaining forms answer the receiver +1. The throwing members
|
||||
// (listeners and option callbacks run synchronously inside) ride the
|
||||
// generic pending check; loop liveness rides USES_TIMERS_LIB_FNS.
|
||||
"readable.push": "scr_stream_push",
|
||||
"readable.push": "scr_stream_push_view",
|
||||
"readable.pushStr": "scr_stream_push_str",
|
||||
"readable.pushStrEnc": "scr_stream_push_str_enc",
|
||||
"readable.pushEncoding": "scr_stream_set_push_encoding",
|
||||
"stream.objectMode": "scr_stream_object_mode",
|
||||
"stream.decodeStrings": "scr_stream_decode_strings",
|
||||
"stream.duplexFromGen": "scr_stream_duplex_from_gen",
|
||||
"stream.duplexFromDyn": "scr_stream_duplex_from_dyn",
|
||||
"stream.duplexifyReadable": "scr_stream_duplexify_readable",
|
||||
"stream.duplexifyWritable": "scr_stream_duplexify_writable",
|
||||
"stream.duplexifyPair": "scr_stream_duplexify_pair",
|
||||
"stream.duplexFromPromise": "scr_stream_duplex_from_promise",
|
||||
"stream.duplexFromAsyncFn": "scr_stream_duplex_from_async_fn",
|
||||
"stream.duplexFromGenSignal": "scr_stream_duplex_from_gen_signal",
|
||||
"stream.duplexFromAsyncFnSignal": "scr_stream_duplex_from_async_fn_signal",
|
||||
"fetch.readableFromWeb": "scr_stream_readable_from_web",
|
||||
"writable.writeEncoding": "scr_stream_write_encoding",
|
||||
"writable.setDefaultEncoding": "scr_stream_set_default_encoding",
|
||||
"stream.chunkDone": "scr_stream_chunk_done",
|
||||
"readable.pushNull": "scr_stream_push_null",
|
||||
"readable.pushDyn": "scr_stream_push_dyn",
|
||||
@@ -1189,6 +1215,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"readable.opMap": "scr_stream_op_map",
|
||||
"readable.opFilter": "scr_stream_op_filter",
|
||||
"readable.opFlatMap": "scr_stream_op_flat_map",
|
||||
"readable.opFlatMapAsyncGen": "scr_stream_op_flat_map_async_gen",
|
||||
"readable.opFlatMapSyncGen": "scr_stream_op_flat_map_sync_gen",
|
||||
"readable.opDrop": "scr_stream_op_drop",
|
||||
"readable.opTake": "scr_stream_op_take",
|
||||
"readable.opToArray": "scr_stream_op_to_array",
|
||||
@@ -1197,6 +1225,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"readable.opEvery": "scr_stream_op_every",
|
||||
"readable.opFind": "scr_stream_op_find",
|
||||
"readable.opReduce": "scr_stream_op_reduce",
|
||||
"readable.opSignalList": "scr_stream_op_signal_list",
|
||||
"readable.opReduceSignal": "scr_stream_op_reduce_signal",
|
||||
"readable.opSignalOf": "scr_stream_op_signal_of",
|
||||
"readable.opOnceOptions": "scr_stream_op_once_options",
|
||||
"readable.opSignalOpts": "scr_stream_op_signal_opts",
|
||||
"readable.pipe": "scr_stream_pipe",
|
||||
"readable.pipeGeneral": "scr_stream_pipe_general",
|
||||
"writable.cork": "scr_stream_cork",
|
||||
@@ -1291,6 +1324,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"http.reqPipeClient": "scr_http_req_pipe_client",
|
||||
"http.reqPipeSock": "scr_http_req_pipe_sock",
|
||||
"http.reqResume": "scr_http_req_resume",
|
||||
"http.reqPause": "scr_http_req_pause",
|
||||
"http.reqDestroy": "scr_http_req_destroy",
|
||||
"http.reqSetEncoding": "scr_http_req_set_encoding",
|
||||
"http.resSetHeader": "scr_http_res_set_header",
|
||||
@@ -1525,11 +1559,13 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.assignPrototype": "scr_dyn_assign_prototype",
|
||||
"dyn.definePrototypeProps": "scr_dyn_define_prototype_props",
|
||||
"dyn.bagGet": "scr_dyn_bag_get",
|
||||
"dyn.bagCreateField": "scr_dyn_bag_create_field",
|
||||
"dyn.bagSet": "scr_dyn_bag_set",
|
||||
"dyn.objCreate": "scr_dyn_obj_create",
|
||||
"dyn.nameClassPrototype": "scr_dyn_name_class_prototype",
|
||||
"dyn.objCreateWithProperties": "scr_dyn_obj_create_with_properties",
|
||||
"dyn.getPrototype": "scr_dyn_get_prototype",
|
||||
"dyn.getPrototypeForIdentity": "scr_dyn_get_prototype_for_identity",
|
||||
"dyn.setPrototype": "scr_dyn_set_prototype",
|
||||
"dyn.getOwnPropertyNames": "scr_dyn_get_own_property_names",
|
||||
"dyn.ownKeys": "scr_dyn_own_keys",
|
||||
@@ -1543,6 +1579,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.objValueList": "scr_dyn_obj_value_list",
|
||||
"dyn.objEntries": "scr_dyn_obj_entries",
|
||||
"dyn.structuredClone": "scr_structured_clone",
|
||||
"dyn.declAdopt": "scr_dyn_decl_adopt",
|
||||
"dyn.cloneMissing": "scr_structured_clone_missing",
|
||||
"dyn.cloneTransferFail": "scr_structured_clone_transfer_fail",
|
||||
"dyn.cloneTransferFailWith": "scr_structured_clone_transfer_fail_with",
|
||||
@@ -1556,6 +1593,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"regex.resultProp": "scr_regex_result_prop",
|
||||
"array.namedKeys": "scr_arr_named_keys",
|
||||
"array.hasNamedKey": "scr_arr_has_named_key",
|
||||
"array.hasProperties": "scr_arr_has_properties",
|
||||
"array.undefinedKey": "scr_arr_undefined_key",
|
||||
// queueMicrotask's checked-dynamic form (borrowed dyn; a non-function
|
||||
// throws synchronously), the minted setImmediate value, and the
|
||||
// timers/promises immediate — all mark the loop live.
|
||||
@@ -1600,6 +1639,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",
|
||||
@@ -1700,6 +1741,8 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"readable.opMap",
|
||||
"readable.opFilter",
|
||||
"readable.opFlatMap",
|
||||
"readable.opFlatMapAsyncGen",
|
||||
"readable.opFlatMapSyncGen",
|
||||
"readable.opDrop",
|
||||
"readable.opTake",
|
||||
"readable.opToArray",
|
||||
@@ -1708,6 +1751,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"readable.opEvery",
|
||||
"readable.opFind",
|
||||
"readable.opReduce",
|
||||
"readable.opReduceSignal",
|
||||
"readable.resume",
|
||||
"readable.pipe",
|
||||
"readable.pipeGeneral",
|
||||
@@ -1729,6 +1773,19 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"stream.setDestroy",
|
||||
"stream.setTransform",
|
||||
"stream.setFlush",
|
||||
"stream.setConstruct",
|
||||
"stream.duplexFromGen",
|
||||
"stream.duplexFromDyn",
|
||||
"stream.duplexifyReadable",
|
||||
"stream.duplexifyWritable",
|
||||
"stream.duplexifyPair",
|
||||
"stream.duplexFromPromise",
|
||||
"stream.duplexFromAsyncFn",
|
||||
"stream.duplexFromGenSignal",
|
||||
"stream.duplexFromAsyncFnSignal",
|
||||
"fetch.readableFromWeb",
|
||||
"stream.setWritev",
|
||||
"stream.setWritevDefault",
|
||||
"process.activeResources",
|
||||
"stream.finished",
|
||||
"stream.finishedDyn",
|
||||
@@ -1738,8 +1795,10 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"sp.finished",
|
||||
"sp.pipeline",
|
||||
"sp.pipelineStages",
|
||||
"sp.pipelineStagesOpts",
|
||||
"stream.pipelineStages",
|
||||
"stream.pipelineStagesDyn",
|
||||
"stream.composeStages",
|
||||
"sp.pipelineOpts",
|
||||
"sp.pipelineOptsSignal",
|
||||
"sp.finishedOpts",
|
||||
|
||||
@@ -187,18 +187,24 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
e.fn === "stream.setFinal" ||
|
||||
e.fn === "stream.setDestroy" ||
|
||||
e.fn === "stream.setTransform" ||
|
||||
e.fn === "stream.setFlush"
|
||||
e.fn === "stream.setFlush" ||
|
||||
e.fn === "stream.setConstruct" ||
|
||||
e.fn === "stream.setWritev" ||
|
||||
e.fn === "stream.setWritevDefault"
|
||||
) {
|
||||
// The underscore-method assignment surface: the runtime slot swaps
|
||||
// its closure (+1 moves in) and invoke thunk — the next
|
||||
// _read/_write/... dispatch uses it, Node's timing.
|
||||
const kindOf: Record<string, "r" | "w" | "f" | "d" | "t" | "l"> = {
|
||||
const kindOf: Record<string, "r" | "w" | "f" | "d" | "t" | "l" | "c" | "v"> = {
|
||||
"stream.setRead": "r",
|
||||
"stream.setWrite": "w",
|
||||
"stream.setFinal": "f",
|
||||
"stream.setDestroy": "d",
|
||||
"stream.setTransform": "t",
|
||||
"stream.setFlush": "l",
|
||||
"stream.setConstruct": "c",
|
||||
"stream.setWritev": "v",
|
||||
"stream.setWritevDefault": "v",
|
||||
};
|
||||
const symOf: Record<string, string> = {
|
||||
"stream.setRead": "scr_stream_set_read",
|
||||
@@ -207,6 +213,9 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
"stream.setDestroy": "scr_stream_set_destroy",
|
||||
"stream.setTransform": "scr_stream_set_transform",
|
||||
"stream.setFlush": "scr_stream_set_flush",
|
||||
"stream.setConstruct": "scr_stream_set_construct",
|
||||
"stream.setWritev": "scr_stream_set_writev",
|
||||
"stream.setWritevDefault": "scr_stream_set_writev_default",
|
||||
};
|
||||
const recv = host.emitExpr(e.args[0]!);
|
||||
const cb = host.emitExpr(e.args[1]!);
|
||||
@@ -692,12 +701,15 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
}
|
||||
if (
|
||||
e.fn === "sp.pipelineStages" ||
|
||||
e.fn === "sp.pipelineStagesOpts" ||
|
||||
e.fn === "stream.pipelineStages" ||
|
||||
e.fn === "stream.pipelineStagesDyn"
|
||||
e.fn === "stream.pipelineStagesDyn" ||
|
||||
e.fn === "stream.composeStages"
|
||||
) {
|
||||
// (n, k, positions, flags, mask, s1..sn, stage1..stagek, helpers...,
|
||||
// box?, (signal, abort)?[, cb]): the streams, the stage closures and
|
||||
// the helpers ride stack arrays; absent groups pass null.
|
||||
// box?, (signal, abort)?[, cb | end, signal?]): the streams, the
|
||||
// stage closures and the helpers ride stack arrays; absent groups
|
||||
// pass null.
|
||||
const [nArg, kArg, positionsArg, flagsArg, maskArg] = e.args;
|
||||
if (
|
||||
nArg?.kind !== "numLit" ||
|
||||
@@ -710,7 +722,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
const n = nArg.value;
|
||||
const k = kArg.value;
|
||||
const flags = flagsArg.value;
|
||||
const helperCount = 3 * pipelinePumpedRuns(n + k, positionsArg.value);
|
||||
const helperCount = 3 * pipelinePumpedRuns(n + k, positionsArg.value, flags);
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
const packed = (from: number, count: number): string => {
|
||||
if (count === 0) return "null";
|
||||
@@ -740,6 +752,21 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
if (e.fn === "sp.pipelineStages") {
|
||||
host.declare(`declare ptr @scr_sp_pipeline_stages(${params})`);
|
||||
B.line(`${t} = call ptr @scr_sp_pipeline_stages(${head})`);
|
||||
} else if (e.fn === "sp.pipelineStagesOpts") {
|
||||
const end = args[at++]!.name;
|
||||
const outer = take((flags & 64) !== 0);
|
||||
let add = "null";
|
||||
if (outer !== "null") {
|
||||
host.declare(`declare ptr @scr_events_add_abort_listener(ptr, ptr)`);
|
||||
add = "@scr_events_add_abort_listener";
|
||||
}
|
||||
host.declare(`declare ptr @scr_sp_pipeline_stages_opts(${params}, i1, ptr, ptr)`);
|
||||
B.line(
|
||||
`${t} = call ptr @scr_sp_pipeline_stages_opts(${head}, i1 ${end}, ptr ${outer}, ptr ${add})`,
|
||||
);
|
||||
} else if (e.fn === "stream.composeStages") {
|
||||
host.declare(`declare ptr @scr_stream_compose_stages(${params})`);
|
||||
B.line(`${t} = call ptr @scr_stream_compose_stages(${head})`);
|
||||
} else if (e.fn === "stream.pipelineStagesDyn") {
|
||||
host.declare(`declare ptr @scr_stream_pipeline_stages_dyn(${params}, ptr)`);
|
||||
B.line(
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import { analyzeCallLifetimes, type CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { ReferenceEffects } from "./reference-effects.js";
|
||||
import { SCR_ARR_FIELD, loadArrCount, scrArrStateAt } from "./common.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { indexInteger } from "./integer-values.js";
|
||||
import { READ_FALLBACK } from "./dense-array-access.js";
|
||||
import { compactRefLoadLines } from "./classes.js";
|
||||
|
||||
@@ -476,7 +476,8 @@ function emitDenseReferenceArrayRead(
|
||||
no: string,
|
||||
slow: string,
|
||||
join: string,
|
||||
loadType: "ptr" | "cref" | "i64" | "i8" = "ptr",
|
||||
/** `i32`: an int32 array's slot, presented as the int32. */
|
||||
loadType: "ptr" | "cref" | "i64" | "i8" | "i32" = "ptr",
|
||||
): void {
|
||||
const B = host.B;
|
||||
const header = (field: number): string => {
|
||||
@@ -567,7 +568,7 @@ function emitDenseReferenceArrayRead(
|
||||
statePtr,
|
||||
data,
|
||||
cap,
|
||||
loadType === "i8" ? 1 : loadType === "cref" ? 4 : 8,
|
||||
loadType === "i8" ? 1 : loadType === "cref" || loadType === "i32" ? 4 : 8,
|
||||
offset,
|
||||
);
|
||||
host.markMemoryPointer(statePtr, "array:present");
|
||||
@@ -578,9 +579,10 @@ function emitDenseReferenceArrayRead(
|
||||
B.startBlock(valueLabel);
|
||||
const valuePtr = B.tmp(),
|
||||
raw = B.tmp();
|
||||
// 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";
|
||||
// Boolean arrays store one byte per element, compact references and int32
|
||||
// arrays four, every other kind an 8-byte slot.
|
||||
const slotType =
|
||||
loadType === "i8" ? "i8" : loadType === "cref" || loadType === "i32" ? "i32" : "i64";
|
||||
B.line(
|
||||
`${valuePtr} = getelementptr inbounds ${slotType}, ptr ${data}, ${host.sizeType} ${offset}`,
|
||||
);
|
||||
@@ -638,11 +640,11 @@ export function emitBorrowedArrayRead(
|
||||
return;
|
||||
}
|
||||
const index = host.emitExpr(read.index);
|
||||
// A proven integer index (an int32 local, say) needs no double
|
||||
// round-trip; a negative one compares above the capacity as unsigned.
|
||||
const exact =
|
||||
loopIndex === null && host.sizeType === "i64" ? exactInteger(host, index, read.index) : null;
|
||||
const integerIndex = loopIndex ?? (exact ? widenInteger(host, exact) : null);
|
||||
// A proven integer index (an int32 local, say), or a wide view exact at
|
||||
// every index below the capacity, needs no double round-trip; a negative
|
||||
// one compares above the capacity as unsigned.
|
||||
const integerIndex =
|
||||
loopIndex ?? (host.sizeType === "i64" ? indexInteger(host, index, read.index) : null);
|
||||
const slow = B.newLabel("borrow.array.slow"),
|
||||
join = B.newLabel("borrow.array.join");
|
||||
emitDenseReferenceArrayRead(
|
||||
@@ -678,8 +680,12 @@ 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);
|
||||
// As in emitBorrowedArrayRead: an exact integer or wide view needs no
|
||||
// round trip, and only the loop's own counter is an induction variable.
|
||||
const integerIndex =
|
||||
loopIndex ?? (inline && host.sizeType === "i64" ? indexInteger(host, index, read.index) : 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
|
||||
@@ -699,7 +705,7 @@ export function emitLocalArrayRead(
|
||||
array,
|
||||
index,
|
||||
integerIndex,
|
||||
integerIndex !== null,
|
||||
loopIndex !== null,
|
||||
store,
|
||||
no,
|
||||
slow,
|
||||
@@ -736,8 +742,15 @@ export function emitLocalArrayRead(
|
||||
const no = B.newLabel("local.array.missing"),
|
||||
join = B.newLabel("local.array.join");
|
||||
const scalar = isScalarElement(read.element);
|
||||
// An int32 array's arms join on the int32 element, converted to the
|
||||
// payload's double once at the join, so a consumer converting the payload
|
||||
// back to an integer folds to the element.
|
||||
const int32 = scalar && read.element.kind === "f64" && host.int32Array(read.array);
|
||||
const element = int32 ? B.slot() : null;
|
||||
if (element) B.entryAllocas.push(`${element} = alloca i32`);
|
||||
const storeValue = (value: string): void => {
|
||||
if (scalar) {
|
||||
if (element) B.line(`store i32 ${value}, ptr ${element}`);
|
||||
else if (scalar) {
|
||||
// Slot bits for f64; a canonical 0/1 for bool, like scr_union_new_bool.
|
||||
let bits = value;
|
||||
if (read.element.kind === "bool") {
|
||||
@@ -754,18 +767,54 @@ export function emitLocalArrayRead(
|
||||
}
|
||||
B.line(`store i32 ${read.presentTag}, ptr ${tag}`);
|
||||
};
|
||||
if (inline)
|
||||
if (inline && scalar && host.checkedInt32Array(read.array))
|
||||
// An int32 array with checked stores: its int32 arm stores the element's
|
||||
// double bits, as the double arm does.
|
||||
host.emitByElementKind(array.name, null, (kind) => {
|
||||
const present =
|
||||
kind === "i32"
|
||||
? (value: string): void => {
|
||||
const number = B.tmp(),
|
||||
bits = B.tmp();
|
||||
B.line(`${number} = sitofp i32 ${value} to double`);
|
||||
B.line(`${bits} = bitcast double ${number} to i64`);
|
||||
storeValue(bits);
|
||||
}
|
||||
: storeValue;
|
||||
const loadType = kind === "i32" ? "i32" : "i64";
|
||||
const induction = loopIndex !== null;
|
||||
emitDenseReferenceArrayRead(
|
||||
host,
|
||||
array,
|
||||
index,
|
||||
integerIndex,
|
||||
induction,
|
||||
present,
|
||||
no,
|
||||
slow,
|
||||
join,
|
||||
loadType,
|
||||
);
|
||||
return null;
|
||||
});
|
||||
else if (inline)
|
||||
emitDenseReferenceArrayRead(
|
||||
host,
|
||||
array,
|
||||
index,
|
||||
integerIndex,
|
||||
integerIndex !== null,
|
||||
loopIndex !== null,
|
||||
storeValue,
|
||||
no,
|
||||
slow,
|
||||
join,
|
||||
!scalar ? referenceLoad(host, read.array.type) : read.element.kind === "bool" ? "i8" : "i64",
|
||||
!scalar
|
||||
? referenceLoad(host, read.array.type)
|
||||
: read.element.kind === "bool"
|
||||
? "i8"
|
||||
: int32
|
||||
? "i32"
|
||||
: "i64",
|
||||
);
|
||||
else B.br(slow);
|
||||
B.startBlock(slow);
|
||||
@@ -792,7 +841,8 @@ export function emitLocalArrayRead(
|
||||
B.line(
|
||||
`${value} = call double @scr_arr_get_f64(ptr ${array.name}, double ${index.name})${READ_FALLBACK}`,
|
||||
);
|
||||
B.line(`${bits} = bitcast double ${value} to i64`);
|
||||
if (element) B.line(`${bits} = fptosi double ${value} to i32`);
|
||||
else B.line(`${bits} = bitcast double ${value} to i64`);
|
||||
} else {
|
||||
host.declare(`declare zeroext i1 @scr_arr_get_bool(ptr, double)`);
|
||||
B.line(
|
||||
@@ -800,7 +850,8 @@ export function emitLocalArrayRead(
|
||||
);
|
||||
B.line(`${bits} = zext i1 ${value} to i64`);
|
||||
}
|
||||
B.line(`store i64 ${bits}, ptr ${payload}`);
|
||||
if (element) B.line(`store i32 ${bits}, ptr ${element}`);
|
||||
else B.line(`store i64 ${bits}, ptr ${payload}`);
|
||||
B.line(`store i32 ${read.presentTag}, ptr ${tag}`);
|
||||
} else {
|
||||
const value = B.tmp(),
|
||||
@@ -814,10 +865,20 @@ export function emitLocalArrayRead(
|
||||
}
|
||||
B.br(join);
|
||||
B.startBlock(no);
|
||||
B.line(`store i64 0, ptr ${payload}`);
|
||||
if (element) B.line(`store i32 0, ptr ${element}`);
|
||||
else B.line(`store i64 0, ptr ${payload}`);
|
||||
B.line(`store i32 ${read.missingTag}, ptr ${tag}`);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
if (element) {
|
||||
const value = B.tmp(),
|
||||
number = B.tmp(),
|
||||
bits = B.tmp();
|
||||
B.line(`${value} = load i32, ptr ${element}`);
|
||||
B.line(`${number} = sitofp i32 ${value} to double`);
|
||||
B.line(`${bits} = bitcast double ${number} to i64`);
|
||||
B.line(`store i64 ${bits}, ptr ${payload}`);
|
||||
}
|
||||
B.line(`store ptr ${box}, ptr ${localSlot}`);
|
||||
return read.borrow || scalar ? null : { slot: payload, type: read.element };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/* The loop optimizations' per-function state: counted loops, dense array
|
||||
* loops, byte windows and typed-array loop versioning, and the unaliased
|
||||
* local arrays they rely on. */
|
||||
import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js";
|
||||
import type { IrFunction, IrStmt } from "../../ir/ir.js";
|
||||
import { findInitializedByteLoopBindings } from "./byte-windows.js";
|
||||
import type { DenseArrayLoop } from "./dense-array-loops.js";
|
||||
import type { LlEmitter } from "./emitter.js";
|
||||
import { defineModuleState } from "./emitter-state.js";
|
||||
|
||||
/** Sets the loop state for `fn` at the start of its emission; `numericFn`
|
||||
* is `fn` with its initializer bindings (emitFunction). */
|
||||
export function beginFunctionLoops(
|
||||
emitter: LlEmitter,
|
||||
fn: IrFunction,
|
||||
numericFn: IrFunction,
|
||||
): void {
|
||||
const loops = LOOPS.of(emitter);
|
||||
loops.unaliasedArrays = findUnaliasedLocalArrays(fn);
|
||||
loops.countedLoopsEnabled = emitter.debug === null && !fn.async && !fn.generator;
|
||||
loops.byteWindowsEnabled = loops.countedLoopsEnabled;
|
||||
loops.bytesLoopVersionsEnabled = loops.countedLoopsEnabled;
|
||||
loops.bytesLoopEntries = null;
|
||||
loops.bytesLoopFunction = fn;
|
||||
loops.denseLoopsEnabled = loops.countedLoopsEnabled;
|
||||
loops.denseLoop = null;
|
||||
loops.denseLoopEntries =
|
||||
loops.denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === "array")
|
||||
? findInitializedByteLoopBindings(fn)
|
||||
: new Map();
|
||||
loops.byteWindowEntries =
|
||||
loops.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes")
|
||||
? findInitializedByteLoopBindings(fn)
|
||||
: new Map();
|
||||
}
|
||||
|
||||
/** The state LOOPS holds on each emitter. */
|
||||
export interface LoopState {
|
||||
/** Local arrays of the current function that no other binding can hold
|
||||
* (ir/array-length-stability.ts). */
|
||||
unaliasedArrays: ReadonlySet<string>;
|
||||
/** Dense array loops (dense-array-loops.ts) run in this function. */
|
||||
denseLoopsEnabled: boolean;
|
||||
denseLoopEntries: ReturnType<typeof findInitializedByteLoopBindings>;
|
||||
/** The fast copy of a dense array loop being emitted. */
|
||||
denseLoop: DenseArrayLoop | null;
|
||||
countedLoopsEnabled: boolean;
|
||||
byteWindowsEnabled: boolean;
|
||||
byteWindowEntries: ReturnType<typeof findInitializedByteLoopBindings>;
|
||||
/** Typed-array loop versioning (bytes-loop-versions.ts) for the current
|
||||
* function: off in debug and suspending functions and inside a copy. */
|
||||
bytesLoopVersionsEnabled: boolean;
|
||||
/** Loops being versioned around the statement being emitted: a loop is
|
||||
* never versioned inside its own copies, and versions nest boundedly. */
|
||||
readonly bytesLoopVersioning: Set<IrStmt>;
|
||||
bytesLoopEntries: ReturnType<typeof findInitializedByteLoopBindings> | null;
|
||||
bytesLoopFunction: IrFunction | null;
|
||||
/** Counted loops whose integer-form safety test an enclosing version
|
||||
* guard already passed. */
|
||||
readonly provenCountedGuards: Set<IrStmt>;
|
||||
}
|
||||
|
||||
export const LOOPS = defineModuleState<LoopState>(() => ({
|
||||
unaliasedArrays: new Set(),
|
||||
denseLoopsEnabled: false,
|
||||
denseLoopEntries: new Map(),
|
||||
denseLoop: null,
|
||||
countedLoopsEnabled: false,
|
||||
byteWindowsEnabled: true,
|
||||
byteWindowEntries: new Map(),
|
||||
bytesLoopVersionsEnabled: false,
|
||||
bytesLoopVersioning: new Set<IrStmt>(),
|
||||
bytesLoopEntries: null,
|
||||
bytesLoopFunction: null,
|
||||
provenCountedGuards: new Set<IrStmt>(),
|
||||
}));
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type IrUnionDef,
|
||||
} from "../../ir/ir.js";
|
||||
import { everyModuleNode } from "../../ir/traverse.js";
|
||||
import type { IrNodeIndex } from "../../ir/node-index.js";
|
||||
|
||||
/** A union of exactly one reference arm and one or two unit arms
|
||||
* (`C | undefined`, `C | null`, `C | null | undefined`) whose VALUES are
|
||||
@@ -120,12 +121,12 @@ export class NullableUnions {
|
||||
|
||||
/** `shapes`: allow shape unions (debug builds keep boxes, whose
|
||||
* debug-info layouts describe every arm). */
|
||||
constructor(mod: IrModule, shapes = true) {
|
||||
constructor(mod: IrModule, shapes = true, nodes?: IrNodeIndex) {
|
||||
const emitted = new Set<string>();
|
||||
for (const cls of mod.classes ?? []) if (!cls.runtime) emitted.add(cls.name);
|
||||
const records = new Set<string>();
|
||||
for (const shape of mod.records ?? []) records.add(shape.id);
|
||||
const boxed = boxedUnionIds(mod);
|
||||
const boxed = boxedUnionIds(mod, nodes);
|
||||
// Shape unions stay out of library builds (their host ABI marshals
|
||||
// boxes) and of programs with lifetime regions (whose walks report a
|
||||
// reference slot by its type's release entry point).
|
||||
@@ -277,11 +278,20 @@ function shapeUnion(
|
||||
/** Unions the runtime itself inspects as tagged boxes keep that
|
||||
* representation: Map keys and Set elements hash and compare union keys by
|
||||
* tag and payload inside the C map. */
|
||||
function boxedUnionIds(mod: IrModule): Set<string> {
|
||||
function boxedUnionIds(mod: IrModule, nodes?: IrNodeIndex): 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);
|
||||
};
|
||||
if (nodes !== undefined) {
|
||||
for (const t of nodes.types) {
|
||||
if (t.kind === "map") mark(t.key);
|
||||
else if (t.kind === "set") mark(t.elem);
|
||||
}
|
||||
return boxed;
|
||||
}
|
||||
everyModuleNode(mod, {
|
||||
expr: () => true,
|
||||
stmt: () => true,
|
||||
|
||||
@@ -31,6 +31,13 @@ import type { InlineUnionFields } from "./inline-unions.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
|
||||
export interface PublishHost {
|
||||
/** Chunk emitters only (emit-journal.ts): EmitJournal's allocated, built,
|
||||
* opened, used and entry. */
|
||||
journalAllocated?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined;
|
||||
journalBuilt?: (() => void) | undefined;
|
||||
journalOpened?: ((op: string) => void) | undefined;
|
||||
journalUsed?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined;
|
||||
journalEntry?: ((table: string, key: string, value: unknown) => void) | undefined;
|
||||
declare(decl: string): void;
|
||||
/** An interned NUL-terminated constant. */
|
||||
cstr(text: string): string;
|
||||
@@ -103,6 +110,14 @@ export class PublishWalkers {
|
||||
|
||||
constructor(private readonly host: PublishHost) {}
|
||||
|
||||
/** Journal replay (emit-journal.ts): builds or finds the walker a chunk
|
||||
* emitter recorded, and answers its name. */
|
||||
replayHelper(op: string, args: readonly unknown[]): string {
|
||||
if (op === "publish.walker")
|
||||
return this.walker(args[0] as IrType, args[1] as string, args[2] as SrcLoc)!;
|
||||
return this.refusal(args[0] as string, args[1] as string);
|
||||
}
|
||||
|
||||
/** The definitions emitted so far (module-level LLVM text). */
|
||||
defs(): string[] {
|
||||
return this.defsOut;
|
||||
@@ -119,6 +134,7 @@ export class PublishWalkers {
|
||||
}
|
||||
const fn = this.walker(t, "the published value", loc);
|
||||
if (fn === null) return [];
|
||||
this.host.journalUsed?.(fn, "publish.walker", [t, "the published value", loc]);
|
||||
this.host.declare(`declare void @scr_publish(ptr, ptr)`);
|
||||
return [` call void @scr_publish(ptr ${v}, ptr ${fn})`];
|
||||
}
|
||||
@@ -156,8 +172,10 @@ export class PublishWalkers {
|
||||
if (known !== undefined) return known;
|
||||
const sym = `@sc_pub_${this.counter++}`;
|
||||
this.walkers.set(key, sym);
|
||||
this.host.journalOpened?.("publish.walker");
|
||||
const body = this.body(t, sym, path, loc);
|
||||
this.defsOut.push(...body, ``);
|
||||
this.host.journalBuilt?.();
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -172,6 +190,7 @@ export class PublishWalkers {
|
||||
if (known !== undefined) return known;
|
||||
const sym = `@sc_pub_${this.counter++}`;
|
||||
this.refusals.set(key, sym);
|
||||
this.host.journalOpened?.("publish.refusal");
|
||||
this.host.declare(`declare void @scr_pub_refuse_at(ptr, ptr, ptr)`);
|
||||
this.defsOut.push(
|
||||
`define internal void ${sym}(ptr %o, ptr %ctx) ${FN_ATTRS} { ; refuse ${llvmComment(detail)}`,
|
||||
@@ -181,6 +200,7 @@ export class PublishWalkers {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return sym;
|
||||
}
|
||||
|
||||
|
||||
@@ -342,6 +342,7 @@ export function analyzeBorrowedReturns(emitter: LlEmitter): void {
|
||||
borrowsWithoutOwning: (e: IrExpr) => emitter.borrowsWithoutOwning(e),
|
||||
borrowedReturn: (callee: string) => emitter.borrowedReturns.has(callee),
|
||||
parameterAllowed: (id: string) => allowed.has(id),
|
||||
nodes: emitter.nodeIndex,
|
||||
};
|
||||
return emitter.referenceEffects.functions.has(fn.name)
|
||||
? host
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type IrUnionDef,
|
||||
} from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
import { borrowsMapMutationReceiver, borrowsMapReadInputs } from "./map-read-lifetimes.js";
|
||||
import { preservesRegexInputs, VirtualTargets } from "./reference-effects.js";
|
||||
import { borrowsStringInputs } from "./string-lifetimes.js";
|
||||
@@ -107,6 +108,7 @@ const INERT_LIB_CALLS = new Set([
|
||||
"regex.resultProp",
|
||||
"array.namedKeys",
|
||||
"array.hasNamedKey",
|
||||
"array.hasProperties",
|
||||
"process.cwd",
|
||||
"process.argv",
|
||||
"process.execArgv",
|
||||
@@ -118,6 +120,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. */
|
||||
@@ -225,6 +228,8 @@ export class ReferenceWrites {
|
||||
/** Fields no code writes once their object is constructed
|
||||
* (immutability.ts, from cycle-mutability's construction-only facts). */
|
||||
private readonly immutable: ImmutableFields | null = null,
|
||||
/** The module's node index, read instead of walking the bodies. */
|
||||
private readonly nodes?: IrNodeIndex,
|
||||
) {
|
||||
this.key(HEAP_KEY);
|
||||
this.unions = new Map((types.unions ?? []).map((u) => [u.id, u]));
|
||||
@@ -234,25 +239,25 @@ export class ReferenceWrites {
|
||||
const constructors = new Set(classes.map((cls) => `%${cls.name}.constructor`));
|
||||
this.constructors = constructors;
|
||||
this.freshConstructors = [...functions.values()].every((fn) =>
|
||||
fn.body.every((s) => this.onlySuperCalls(fn, s, constructors)),
|
||||
this.onlySuperCalls(fn, constructors),
|
||||
);
|
||||
this.callTargets = new CallTargets(functions, this.virtualTargets);
|
||||
this.callTargets = new CallTargets(functions, this.virtualTargets, nodes);
|
||||
this.solve(constructors);
|
||||
}
|
||||
|
||||
/** Every direct call of a constructor is a `super` call from a
|
||||
/** Every direct call of a constructor in `fn` is a `super` call from a
|
||||
* constructor on its own `this`. */
|
||||
private onlySuperCalls(fn: IrFunction, s: IrStmt, constructors: ReadonlySet<string>): boolean {
|
||||
private onlySuperCalls(fn: IrFunction, constructors: ReadonlySet<string>): boolean {
|
||||
const thisId = constructors.has(fn.name) ? fn.params[0]?.localId : undefined;
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "call" && constructors.has(e.callee)) {
|
||||
const self = e.args[0];
|
||||
if (thisId === undefined || !self || !isThisRef(self, thisId)) return false;
|
||||
let only = true;
|
||||
eachNodeOfKinds(fn, this.nodes?.scan(CALL_KINDS), (node) => {
|
||||
if (node.kind === "call" && constructors.has(node.callee)) {
|
||||
const self = node.args[0];
|
||||
if (thisId === undefined || !self || !isThisRef(self, thisId)) only = false;
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (node: IrStmt): boolean => everyStmtChild(node, expr, stmt);
|
||||
return stmt(s);
|
||||
return only;
|
||||
});
|
||||
return only;
|
||||
}
|
||||
|
||||
private key(k: string): number {
|
||||
@@ -297,7 +302,7 @@ export class ReferenceWrites {
|
||||
}
|
||||
const thisId = external && constructors.has(fn.name) ? (fn.params[0]?.localId ?? null) : null;
|
||||
const boxed = boxedLocals(fn);
|
||||
const privates = privateArrays(fn, boxed);
|
||||
const privates = privateArrays(fn, boxed, this.nodes);
|
||||
const sink: WriteSink = {
|
||||
add: (k) => {
|
||||
// Interned for queries; a callee's locals are private to it.
|
||||
@@ -747,7 +752,7 @@ export class ReferenceWrites {
|
||||
enter(fn: IrFunction): void {
|
||||
this.current = fn;
|
||||
this.currentBoxed = boxedLocals(fn);
|
||||
this.currentPrivateArrays = privateArrays(fn, this.currentBoxed);
|
||||
this.currentPrivateArrays = privateArrays(fn, this.currentBoxed, this.nodes);
|
||||
this.currentLocals = new Set(fn.locals.map((local) => local.id));
|
||||
this.inConstructor = this.constructors.has(fn.name);
|
||||
for (const p of fn.params) this.currentLocals.add(p.localId);
|
||||
@@ -971,6 +976,29 @@ function privateReceiver(arr: IrExpr, privates: ReadonlySet<string>): boolean {
|
||||
* element out nor let the array escape. */
|
||||
const PRIVATE_ARRAY_METHODS = new Set(["length", "push"]);
|
||||
|
||||
/** The nodes the scans below read. */
|
||||
const CALL_KINDS: readonly IrNodeKind[] = ["call"];
|
||||
const CALL_SITE_KINDS: readonly IrNodeKind[] = ["call", "new", "virtualCall", "closure"];
|
||||
const DEFINITION_KINDS: readonly IrNodeKind[] = ["assignExpr", "incDec", "varDecl", "assign"];
|
||||
|
||||
/** The nodes privateArrays reads. */
|
||||
const PRIVATE_ARRAY_KINDS: readonly IrNodeKind[] = [
|
||||
"varRef",
|
||||
"assignExpr",
|
||||
"incDec",
|
||||
"closure",
|
||||
"classRef",
|
||||
"arrIntrinsic",
|
||||
"arrayHas",
|
||||
"varDecl",
|
||||
"assign",
|
||||
"arraySet",
|
||||
"arraySetUndefined",
|
||||
"arrayDelete",
|
||||
"arraySetLength",
|
||||
"return",
|
||||
];
|
||||
|
||||
/** Array locals whose storage the function allocated itself and that
|
||||
* nothing else can reach before the function returns them: declared once
|
||||
* from an array literal or `new Array(n)`, never rebound, unboxed, and
|
||||
@@ -983,7 +1011,11 @@ const PRIVATE_ARRAY_METHODS = new Set(["length", "push"]);
|
||||
* stay out of the function's summary (a caller's borrows predate the
|
||||
* array). The literal map, filter and fused-pipeline helpers fill their
|
||||
* results this way. */
|
||||
function privateArrays(fn: IrFunction, boxed: ReadonlySet<string>): ReadonlySet<string> {
|
||||
function privateArrays(
|
||||
fn: IrFunction,
|
||||
boxed: ReadonlySet<string>,
|
||||
nodes: IrNodeIndex | undefined,
|
||||
): ReadonlySet<string> {
|
||||
if (fn.async || fn.generator) return new Set();
|
||||
const candidates = new Set<string>();
|
||||
for (const local of fn.locals)
|
||||
@@ -999,54 +1031,50 @@ function privateArrays(fn: IrFunction, boxed: ReadonlySet<string>): ReadonlySet<
|
||||
if (e.kind === "varRef" && candidates.has(e.localId))
|
||||
permitted.set(e, (permitted.get(e) ?? 0) + 1);
|
||||
};
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
switch (e.kind) {
|
||||
eachNodeOfKinds(fn, nodes?.scan(PRIVATE_ARRAY_KINDS), (node) => {
|
||||
switch (node.kind) {
|
||||
case "varRef": {
|
||||
const left = permitted.get(e) ?? 0;
|
||||
if (left === 0) rejected.add(e.localId);
|
||||
else permitted.set(e, left - 1);
|
||||
const left = permitted.get(node) ?? 0;
|
||||
if (left === 0) rejected.add(node.localId);
|
||||
else permitted.set(node, left - 1);
|
||||
break;
|
||||
}
|
||||
case "assignExpr":
|
||||
case "incDec":
|
||||
rejected.add(e.localId);
|
||||
rejected.add(node.localId);
|
||||
break;
|
||||
case "closure":
|
||||
case "classRef":
|
||||
for (const id of e.captures ?? []) rejected.add(id);
|
||||
for (const id of node.captures ?? []) rejected.add(id);
|
||||
break;
|
||||
case "arrIntrinsic":
|
||||
if (PRIVATE_ARRAY_METHODS.has(e.method)) permit(e.receiver);
|
||||
if (PRIVATE_ARRAY_METHODS.has(node.method)) permit(node.receiver);
|
||||
break;
|
||||
case "arrayHas":
|
||||
permit(e.arr);
|
||||
permit(node.arr);
|
||||
break;
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
switch (s.kind) {
|
||||
case "varDecl":
|
||||
declarations.set(s.localId, (declarations.get(s.localId) ?? 0) + 1);
|
||||
if (s.init === null || (s.init.kind !== "arrayLit" && s.init.kind !== "arrayNewLen"))
|
||||
rejected.add(s.localId);
|
||||
declarations.set(node.localId, (declarations.get(node.localId) ?? 0) + 1);
|
||||
if (
|
||||
node.init === null ||
|
||||
(node.init.kind !== "arrayLit" && node.init.kind !== "arrayNewLen")
|
||||
)
|
||||
rejected.add(node.localId);
|
||||
break;
|
||||
case "assign":
|
||||
rejected.add(s.localId);
|
||||
rejected.add(node.localId);
|
||||
break;
|
||||
case "arraySet":
|
||||
case "arraySetUndefined":
|
||||
case "arrayDelete":
|
||||
case "arraySetLength":
|
||||
permit(s.arr);
|
||||
permit(node.arr);
|
||||
break;
|
||||
case "return":
|
||||
if (s.value !== null) permit(s.value);
|
||||
if (node.value !== null) permit(node.value);
|
||||
break;
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
fn.body.every(stmt);
|
||||
});
|
||||
const out = new Set<string>();
|
||||
for (const id of candidates) if (!rejected.has(id) && declarations.get(id) === 1) out.add(id);
|
||||
return out;
|
||||
@@ -1079,30 +1107,30 @@ class CallTargets {
|
||||
constructor(
|
||||
private readonly functions: ReadonlyMap<string, IrFunction>,
|
||||
virtualTargets: VirtualTargets,
|
||||
private readonly nodes: IrNodeIndex | undefined,
|
||||
) {
|
||||
const site = (callee: string, caller: IrFunction, args: readonly IrExpr[]): void => {
|
||||
let list = this.sites.get(callee);
|
||||
if (!list) this.sites.set(callee, (list = []));
|
||||
list.push({ caller, args });
|
||||
};
|
||||
const scan = nodes?.scan(CALL_SITE_KINDS);
|
||||
for (const fn of functions.values()) {
|
||||
if (fn.captures !== undefined || fn.classCaptures !== undefined) this.opaque.add(fn.name);
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "call") site(e.callee, fn, e.args);
|
||||
else if (e.kind === "new") site(`%${e.className}.constructor`, fn, [e, ...e.args]);
|
||||
else if (e.kind === "virtualCall") {
|
||||
const targets = virtualTargets.targets(e.className, e.method);
|
||||
for (const t of targets ?? []) site(t, fn, e.args);
|
||||
} else if (e.kind === "closure") {
|
||||
this.opaque.add(e.fnName);
|
||||
let list = this.creators.get(e.fnName);
|
||||
if (!list) this.creators.set(e.fnName, (list = []));
|
||||
eachNodeOfKinds(fn, scan, (node) => {
|
||||
if (node.kind === "call") site(node.callee, fn, node.args);
|
||||
else if (node.kind === "new")
|
||||
site(`%${node.className}.constructor`, fn, [node, ...node.args]);
|
||||
else if (node.kind === "virtualCall") {
|
||||
const targets = virtualTargets.targets(node.className, node.method);
|
||||
for (const t of targets ?? []) site(t, fn, node.args);
|
||||
} else if (node.kind === "closure") {
|
||||
this.opaque.add(node.fnName);
|
||||
let list = this.creators.get(node.fnName);
|
||||
if (!list) this.creators.set(node.fnName, (list = []));
|
||||
list.push(fn);
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (st: IrStmt): boolean => everyStmtChild(st, expr, stmt);
|
||||
fn.body.every(stmt);
|
||||
});
|
||||
}
|
||||
// Fixpoint over function-typed parameter positions.
|
||||
const positions: [IrFunction, number][] = [];
|
||||
@@ -1223,18 +1251,13 @@ class CallTargets {
|
||||
defs = new Map();
|
||||
const boxed = new Set(fn.locals.filter((l) => l.boxed).map((l) => l.id));
|
||||
const counts = new Map<string, number>();
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "assignExpr" || e.kind === "incDec") counts.set(e.localId, 2);
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (st: IrStmt): boolean => {
|
||||
if (st.kind === "varDecl") {
|
||||
counts.set(st.localId, (counts.get(st.localId) ?? 0) + 1);
|
||||
defs!.set(st.localId, st.init);
|
||||
} else if (st.kind === "assign") counts.set(st.localId, 2);
|
||||
return everyStmtChild(st, expr, stmt);
|
||||
};
|
||||
fn.body.every(stmt);
|
||||
eachNodeOfKinds(fn, this.nodes?.scan(DEFINITION_KINDS), (node) => {
|
||||
if (node.kind === "assignExpr" || node.kind === "incDec") counts.set(node.localId, 2);
|
||||
else if (node.kind === "varDecl") {
|
||||
counts.set(node.localId, (counts.get(node.localId) ?? 0) + 1);
|
||||
defs!.set(node.localId, node.init);
|
||||
} else if (node.kind === "assign") counts.set(node.localId, 2);
|
||||
});
|
||||
for (const [local, n] of counts) if (n !== 1 || boxed.has(local)) defs.set(local, null);
|
||||
this.definitions.set(fn, defs);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
emitRecordShapes,
|
||||
inlineRcDecls,
|
||||
INLINE_DYN_RELEASES_PER_FUNCTION,
|
||||
INLINE_STR_RELEASES_PER_FUNCTION,
|
||||
inlineRcOption,
|
||||
limitInlineDynReleases,
|
||||
limitInlineReleases,
|
||||
releaseSym,
|
||||
retainSym,
|
||||
type ShapeHost,
|
||||
@@ -132,10 +133,10 @@ describe("LLVM runtime RC symbols", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("release builds inline only string retains and dyn retains and releases", () => {
|
||||
test("release builds inline only string and dyn retains and releases and Map retains", () => {
|
||||
const { host } = declarationHost(64, "release");
|
||||
expect(retainSym(host, { kind: "string" })).toBe("@sc_rc_retain_str");
|
||||
expect(releaseSym(host, { kind: "string" })).toBe("@scr_str_release");
|
||||
expect(releaseSym(host, { kind: "string" })).toBe("@sc_rc_release_str");
|
||||
expect(retainSym(host, { kind: "dyn" })).toBe("@sc_rc_retain_dyn");
|
||||
expect(releaseSym(host, { kind: "dyn" })).toBe("@sc_rc_release_dyn");
|
||||
expect(releaseSym(host, { kind: "union", unionId: "u0" } as never)).toBe("@scr_union_release");
|
||||
@@ -251,12 +252,12 @@ describe("LLVM runtime RC symbols", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("inline dyn release budget", () => {
|
||||
const body = (name: string, sites: number): string =>
|
||||
describe("inline release budgets", () => {
|
||||
const body = (name: string, sites: number, family = "dyn"): string =>
|
||||
[
|
||||
`define internal void @${name}(ptr %v) #0 {`,
|
||||
"entry:",
|
||||
...Array.from({ length: sites }, () => " call void @sc_rc_release_dyn(ptr %v)"),
|
||||
...Array.from({ length: sites }, () => ` call void @sc_rc_release_${family}(ptr %v)`),
|
||||
" ret void",
|
||||
"}",
|
||||
"",
|
||||
@@ -266,7 +267,9 @@ describe("inline dyn release budget", () => {
|
||||
const small = body("small", INLINE_DYN_RELEASES_PER_FUNCTION);
|
||||
const large = body("large", INLINE_DYN_RELEASES_PER_FUNCTION + 1);
|
||||
// main's lines are separate parts.
|
||||
const limited = limitInlineDynReleases([`; header\n${small}`, ...large.split("\n")]).join("\n");
|
||||
const limited = limitInlineReleases([`; header\n${small}`, ...large.split("\n")], "speed").join(
|
||||
"\n",
|
||||
);
|
||||
expect(limited.startsWith("; header\n")).toBe(true);
|
||||
expect(limited).toContain(small);
|
||||
const out = limited.slice(limited.indexOf("@large("));
|
||||
@@ -275,6 +278,20 @@ describe("inline dyn release budget", () => {
|
||||
INLINE_DYN_RELEASES_PER_FUNCTION + 1,
|
||||
);
|
||||
});
|
||||
|
||||
test("a function over the string budget calls the runtime string release at every site", () => {
|
||||
const small = body("small", INLINE_STR_RELEASES_PER_FUNCTION, "str");
|
||||
const large = body("large", INLINE_STR_RELEASES_PER_FUNCTION + 1, "str");
|
||||
const limited = limitInlineReleases([small, large], "release").join("\n");
|
||||
expect(limited).toContain(small);
|
||||
// Speed emission keeps every inline string release.
|
||||
expect(limitInlineReleases([small, large], "speed").join("\n")).toBe([small, large].join("\n"));
|
||||
const out = limited.slice(limited.indexOf("@large("));
|
||||
expect(out).not.toContain("@sc_rc_release_str(");
|
||||
expect(out.split("call void @scr_str_release(ptr %v)").length - 1).toBe(
|
||||
INLINE_STR_RELEASES_PER_FUNCTION + 1,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("emitted object releases", () => {
|
||||
|
||||
@@ -68,9 +68,20 @@ const CYC_MAX_TYPES = 32768;
|
||||
/** What the tables need from the emitter: the extern-declaration ledger
|
||||
* and the module-wide cycle/shape indexes. */
|
||||
export interface ShapeHost {
|
||||
/** Chunk emitters only (emit-journal.ts): EmitJournal's allocated, built,
|
||||
* opened, used and entry. */
|
||||
journalAllocated?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined;
|
||||
journalBuilt?: (() => void) | undefined;
|
||||
journalOpened?: ((op: string) => void) | undefined;
|
||||
journalUsed?: ((name: string, op: string, args: readonly unknown[]) => void) | undefined;
|
||||
journalEntry?: ((table: string, key: string, value: unknown) => void) | undefined;
|
||||
/** The hierarchy root of a class (a program class or a runtime base),
|
||||
* or undefined for classes outside the class graph. */
|
||||
classRootOf?(className: string): string | undefined;
|
||||
/** Whether releasing a value of this type does nothing: instances of a
|
||||
* program-lifetime class without lifetime regions (classes.ts
|
||||
* programLifetimeClass). */
|
||||
releasesNothing?(t: IrType): boolean;
|
||||
/** Whether arrays of `elem` hold 32-bit references to compact class
|
||||
* instances (SCR_ELEM_CREF; LlEmitter.compactArrayElem). */
|
||||
compactArrayElem?(elem: IrType): boolean;
|
||||
@@ -392,6 +403,7 @@ function nullableRetainSym(host: ShapeHost, arm: IrType): string {
|
||||
const sym = `@sc_nretain_${arm.kind}`;
|
||||
if (!host.nullableUnions.retainWrappers.has(sym))
|
||||
host.nullableUnions.retainWrappers.set(sym, retainSym(host, arm));
|
||||
host.journalEntry?.("retainWrapper", sym, host.nullableUnions.retainWrappers.get(sym));
|
||||
return sym;
|
||||
}
|
||||
|
||||
@@ -570,15 +582,17 @@ 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 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. */
|
||||
* with a fast path. `release` mirrors only the string retain and release,
|
||||
* the Map retain 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). String-heavy programs have many string release sites, so a
|
||||
* function with more than INLINE_STR_RELEASES_PER_FUNCTION of them calls the
|
||||
* runtime release at each (limitInlineReleases). */
|
||||
export type InlineRcMode = "release" | "speed";
|
||||
|
||||
const RELEASE_RC_OPS = new Set([
|
||||
"str:retain",
|
||||
"str:release",
|
||||
"map:retain",
|
||||
"dyn:retain",
|
||||
"dyn:release",
|
||||
@@ -662,12 +676,34 @@ function inlineRcName(family: string, op: RcOp): string {
|
||||
* the inline helper mirrors, at every site. */
|
||||
export const INLINE_DYN_RELEASES_PER_FUNCTION = 256;
|
||||
|
||||
/** Apply {@link INLINE_DYN_RELEASES_PER_FUNCTION} to each function of an
|
||||
* emitted module, given as parts joined by newlines. A function can span
|
||||
* parts (main's lines) and a part can hold several functions. */
|
||||
export function limitInlineDynReleases(parts: readonly string[]): string[] {
|
||||
const call = `call void @${inlineRcName("dyn", "release")}(`;
|
||||
const runtime = `call void @${INLINE_RC_FAMILIES["dyn"]!.release}(`;
|
||||
/** Inlined string releases one function body keeps in `release` emission,
|
||||
* which inlines string releases (RELEASE_RC_OPS); the bodies with the most of them, such as
|
||||
* builders of string-heavy records and module initializers, would grow the
|
||||
* most for the least gain, so a body with more calls scr_str_release at
|
||||
* every site. */
|
||||
export const INLINE_STR_RELEASES_PER_FUNCTION = 8;
|
||||
|
||||
/** Apply {@link INLINE_DYN_RELEASES_PER_FUNCTION}, and in `release` emission
|
||||
* {@link INLINE_STR_RELEASES_PER_FUNCTION}, to each function of an emitted
|
||||
* module, given as parts joined by newlines. A function can span parts
|
||||
* (main's lines) and a part can hold several functions. */
|
||||
export function limitInlineReleases(
|
||||
parts: readonly string[],
|
||||
mode: InlineRcMode | undefined,
|
||||
): string[] {
|
||||
const dyn = limitInlineReleaseFamily(parts, "dyn", INLINE_DYN_RELEASES_PER_FUNCTION);
|
||||
return mode === "release"
|
||||
? limitInlineReleaseFamily(dyn, "str", INLINE_STR_RELEASES_PER_FUNCTION)
|
||||
: dyn;
|
||||
}
|
||||
|
||||
function limitInlineReleaseFamily(
|
||||
parts: readonly string[],
|
||||
family: "dyn" | "str",
|
||||
max: number,
|
||||
): string[] {
|
||||
const call = `call void @${inlineRcName(family, "release")}(`;
|
||||
const runtime = `call void @${INLINE_RC_FAMILIES[family]!.release}(`;
|
||||
// The function each release site belongs to: the last `define` line
|
||||
// before it, counted over the joined text.
|
||||
const sites = (
|
||||
@@ -693,9 +729,7 @@ export function limitInlineDynReleases(parts: readonly string[]): string[] {
|
||||
const counts = new Map<number, number>();
|
||||
let fn = 0;
|
||||
for (const part of parts) fn = sites(part, (_, f) => counts.set(f, (counts.get(f) ?? 0) + 1), fn);
|
||||
const over = new Set(
|
||||
[...counts].filter(([, n]) => n > INLINE_DYN_RELEASES_PER_FUNCTION).map(([f]) => f),
|
||||
);
|
||||
const over = new Set([...counts].filter(([, n]) => n > max).map(([f]) => f));
|
||||
if (over.size === 0) return [...parts];
|
||||
fn = 0;
|
||||
return parts.map((part) => {
|
||||
@@ -1087,7 +1121,9 @@ export function arrNewCall(
|
||||
* 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. */
|
||||
* SCR_ARR_OPS_COMPACT, and on 64-bit targets arrays whose element release
|
||||
* does nothing (ShapeHost.releasesNothing) as SCR_ARR_OPS_RELEASE_NOTHING,
|
||||
* so that destroying them only frees their storage. */
|
||||
export function arrOps(host: ShapeHost, elem: IrType): string {
|
||||
const v = vAdapters(host, elem);
|
||||
const trace = traceArg(host, elem);
|
||||
@@ -1099,14 +1135,23 @@ export function arrOps(host: ShapeHost, elem: IrType): string {
|
||||
);
|
||||
return `${name}.c`;
|
||||
}
|
||||
const nullable = host.nullableUnions.of(elem);
|
||||
const payload = nullable?.repr === "single" ? nullable.arm : elem;
|
||||
if (host.sizeType === "i64" && host.releasesNothing?.(payload) === true) {
|
||||
host.declare(
|
||||
`${name}.n = internal global { ptr, ptr, ptr, i32, i32 } { ptr ${v.retain}, ptr ${v.release}, ptr ${trace}, i32 0, i32 ${SCR_ARR_OPS_RELEASE_NOTHING} }`,
|
||||
);
|
||||
return `${name}.n`;
|
||||
}
|
||||
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. */
|
||||
/** scr_array.h SCR_ARR_OPS_COMPACT and SCR_ARR_OPS_RELEASE_NOTHING. */
|
||||
const SCR_ARR_OPS_COMPACT = 1;
|
||||
const SCR_ARR_OPS_RELEASE_NOTHING = 2;
|
||||
|
||||
/* ── capture boxes ────────────────────────────────────────────────────── */
|
||||
|
||||
|
||||
@@ -35,12 +35,12 @@ export interface SpeculativeLoopPlan {
|
||||
type SpeculativeLoop = Extract<IrStmt, { kind: "for" | "while" | "doWhile" }>;
|
||||
|
||||
/** The fast copy duplicates the loop; keep the duplicated code small. */
|
||||
const MAX_LOOP_NODES = 400;
|
||||
export const MAX_LOOP_NODES = 400;
|
||||
|
||||
/** Statements whose emission is plain control flow or a single write.
|
||||
* Nested loops, switches and try statements keep the loop on the ordinary
|
||||
* path (their emission keeps state that the two copies would not share). */
|
||||
const SIMPLE_STATEMENTS = new Set<string>([
|
||||
export const SIMPLE_STATEMENTS: ReadonlySet<string> = new Set<string>([
|
||||
"varDecl",
|
||||
"assign",
|
||||
"exprStmt",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* is thread-local.
|
||||
*/
|
||||
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { exactIndexLines, sizeToDouble } from "./common.js";
|
||||
import { wtf8Bytes } from "../literals.js";
|
||||
import { literalCapWord, literalCapWord32 } from "./string-key-hash.js";
|
||||
@@ -37,6 +38,66 @@ export const STRING_CHAR_AT_DECL = "declare ptr @scr_str_char_at(ptr, double)";
|
||||
/** Requests the integer-index charAt helper, as the marker above does. */
|
||||
export const STRING_CHAR_AT_INT_MARKER = "; uses @sc_str_char_at_int";
|
||||
|
||||
/** The proven-ASCII flag of string `s`, as an i1. */
|
||||
export function emitStringAsciiFlag(host: LlvmEmitterContext, s: string): string {
|
||||
const B = host.B;
|
||||
const wordPointer = B.tmp(),
|
||||
word = B.tmp(),
|
||||
ascii = B.tmp();
|
||||
B.line(`${wordPointer} = getelementptr inbounds %ScrStr, ptr ${s}, i32 0, i32 2`);
|
||||
B.line(`${word} = load i32, ptr ${wordPointer}`);
|
||||
B.line(`${ascii} = icmp slt i32 ${word}, 0`);
|
||||
return ascii;
|
||||
}
|
||||
|
||||
/** The byte length of string `s`, as the target's size type. */
|
||||
export function emitStringByteLength(host: LlvmEmitterContext, s: string): string {
|
||||
const B = host.B;
|
||||
const lengthPointer = B.tmp(),
|
||||
length = B.tmp();
|
||||
B.line(`${lengthPointer} = getelementptr inbounds %ScrStr, ptr ${s}, i32 0, i32 1`);
|
||||
B.line(`${length} = load ${host.sizeType}, ptr ${lengthPointer}`);
|
||||
return length;
|
||||
}
|
||||
|
||||
/** Unit `index` (an i64) of a proven-ASCII string `s` whose length covers
|
||||
* it: one byte load, as a double. */
|
||||
export function emitAsciiCharCode(host: LlvmEmitterContext, s: string, index: string): string {
|
||||
const B = host.B;
|
||||
const data = B.tmp(),
|
||||
pointer = B.tmp(),
|
||||
byte = B.tmp(),
|
||||
code = B.tmp();
|
||||
B.line(`${data} = getelementptr inbounds %ScrStr, ptr ${s}, i32 1`);
|
||||
B.line(`${pointer} = getelementptr inbounds i8, ptr ${data}, i64 ${index}`);
|
||||
B.line(`${byte} = load i8, ptr ${pointer}`);
|
||||
B.line(`${code} = uitofp i8 ${byte} to double`);
|
||||
return code;
|
||||
}
|
||||
|
||||
/** The one-character string of unit `index` (an i64) of a proven-ASCII
|
||||
* string `s` whose length covers it: the immortal `@sc_ascii_chars` entry
|
||||
* for its byte, as the inline charAt helpers answer. */
|
||||
export function emitAsciiChar(host: LlvmEmitterContext, s: string, index: string): string {
|
||||
// The table is defined with sc_str_char_at.
|
||||
host.declare(STRING_CHAR_AT_DECL);
|
||||
const B = host.B;
|
||||
const sz = host.sizeType;
|
||||
const data = B.tmp(),
|
||||
pointer = B.tmp(),
|
||||
byte = B.tmp(),
|
||||
code = B.tmp(),
|
||||
entry = B.tmp();
|
||||
B.line(`${data} = getelementptr inbounds %ScrStr, ptr ${s}, i32 1`);
|
||||
B.line(`${pointer} = getelementptr inbounds i8, ptr ${data}, i64 ${index}`);
|
||||
B.line(`${byte} = load i8, ptr ${pointer}`);
|
||||
B.line(`${code} = zext i8 ${byte} to i64`);
|
||||
B.line(
|
||||
`${entry} = getelementptr inbounds [128 x { ${sz}, ${sz}, ${sz}, [2 x i8] }], ptr @sc_ascii_chars, i64 0, i64 ${code}`,
|
||||
);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/** Load the proven-ASCII flag of `%s` into `%ascii`. */
|
||||
function asciiTest(): string[] {
|
||||
return [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isRefCounted, type IrExpr, type IrFunction, type IrLocal } from "../../ir/ir.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
|
||||
/** What the emitter knows about a projection's storage. */
|
||||
export interface WalkBorrowHost {
|
||||
@@ -22,8 +22,24 @@ export interface WalkBorrowHost {
|
||||
* (whose own rebinding releases nothing). Absent: the body preserves
|
||||
* every edge. */
|
||||
unwritten?(value: IrExpr, candidates: ReadonlySet<string>): boolean;
|
||||
/** The module's node index, read instead of walking the body. */
|
||||
nodes?: IrNodeIndex;
|
||||
}
|
||||
|
||||
/** The node kinds analyzeWalks reads from a body. */
|
||||
const WALK_KINDS: readonly IrNodeKind[] = [
|
||||
"varDecl",
|
||||
"assign",
|
||||
"forOf",
|
||||
"rethrow",
|
||||
"tryCatch",
|
||||
"return",
|
||||
"assignExpr",
|
||||
"incDec",
|
||||
"closure",
|
||||
"classRef",
|
||||
];
|
||||
|
||||
export interface WalkFacts {
|
||||
locals: ReadonlySet<string>;
|
||||
/** Every return yields a walk (or a throw), so the body can return its
|
||||
@@ -71,44 +87,36 @@ export function analyzeWalks(fn: IrFunction, host: WalkBorrowHost): WalkFacts {
|
||||
if (!list) definitions.set(id, (list = []));
|
||||
list.push(value);
|
||||
};
|
||||
everyStmtList(fn.body, {
|
||||
stmt: (s) => {
|
||||
switch (s.kind) {
|
||||
case "varDecl":
|
||||
define(s.localId, s.init);
|
||||
break;
|
||||
case "assign":
|
||||
define(s.localId, s.value);
|
||||
break;
|
||||
case "forOf":
|
||||
excluded.add(s.localId);
|
||||
break;
|
||||
case "rethrow":
|
||||
excluded.add(s.localId);
|
||||
break;
|
||||
case "tryCatch":
|
||||
guarded = true;
|
||||
if (s.catchLocalId !== null) excluded.add(s.catchLocalId);
|
||||
break;
|
||||
case "return":
|
||||
returns.push(s.value);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
expr: (e) => {
|
||||
switch (e.kind) {
|
||||
case "assignExpr":
|
||||
case "incDec":
|
||||
excluded.add(e.localId);
|
||||
break;
|
||||
case "closure":
|
||||
case "classRef":
|
||||
for (const id of e.captures ?? []) excluded.add(id);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
eachNodeOfKinds(fn, host.nodes?.scan(WALK_KINDS), (node) => {
|
||||
switch (node.kind) {
|
||||
case "varDecl":
|
||||
define(node.localId, node.init);
|
||||
break;
|
||||
case "assign":
|
||||
define(node.localId, node.value);
|
||||
break;
|
||||
case "forOf":
|
||||
excluded.add(node.localId);
|
||||
break;
|
||||
case "rethrow":
|
||||
excluded.add(node.localId);
|
||||
break;
|
||||
case "tryCatch":
|
||||
guarded = true;
|
||||
if (node.catchLocalId !== null) excluded.add(node.catchLocalId);
|
||||
break;
|
||||
case "return":
|
||||
returns.push(node.value);
|
||||
break;
|
||||
case "assignExpr":
|
||||
case "incDec":
|
||||
excluded.add(node.localId);
|
||||
break;
|
||||
case "closure":
|
||||
case "classRef":
|
||||
for (const id of node.captures ?? []) excluded.add(id);
|
||||
break;
|
||||
}
|
||||
});
|
||||
const plain = (id: string): boolean => {
|
||||
const local = locals.get(id);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { emitPerKeyOrder } from "./key-orders.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
/* Structure-walking helper EMITTERS for the LLVM backend — the .ll mirror
|
||||
@@ -62,6 +63,21 @@ export class LlWalkers {
|
||||
|
||||
constructor(private readonly host: WalkerHost) {}
|
||||
|
||||
/** Journal replay (emit-journal.ts): builds or finds the helper a chunk
|
||||
* emitter recorded, and answers its name. */
|
||||
replayHelper(op: string, args: readonly unknown[]): string {
|
||||
switch (op) {
|
||||
case "walkers.jsonWriteHelper":
|
||||
return this.jsonWriteHelper(args[0] as IrType);
|
||||
case "walkers.recordFieldWriter":
|
||||
return this.recordFieldWriter(args[0] as string);
|
||||
case "walkers.unionJoinHelper":
|
||||
return this.unionJoinHelper(args[0] as string);
|
||||
default:
|
||||
throw new InternalCompilerError(`llvm emitter bug: no walker replay ${op}`);
|
||||
}
|
||||
}
|
||||
|
||||
private get S(): "i32" | "i64" {
|
||||
return this.host.sizeType;
|
||||
}
|
||||
@@ -191,6 +207,7 @@ export class LlWalkers {
|
||||
if (existing) return existing;
|
||||
const name = `sc_jw_${this.jsonWriters.size}`;
|
||||
this.jsonWriters.set(key, name);
|
||||
this.host.journalAllocated?.(name, "walkers.jsonWriteHelper", [t]);
|
||||
const B = new BlockBuilder();
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
@@ -244,6 +261,7 @@ export class LlWalkers {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -311,78 +329,80 @@ export class LlWalkers {
|
||||
if (cyclic) this.jbLeave(B);
|
||||
return;
|
||||
}
|
||||
// Fields serialize in DECLARED order (JS insertion order); internal
|
||||
// Fields serialize in DECLARED order (JS insertion order), or in the
|
||||
// value's own order for a shape with orderVariants; internal
|
||||
// '%'-fields stay hidden — jsonWriteHelper's contract, ported comment
|
||||
// and all.
|
||||
const order = shape.declaredOrder ?? shape.fields.map((f) => f.name);
|
||||
const inOrder = new Set(order);
|
||||
if (shape.fields.some((f) => !inOrder.has(f.name) && !f.name.startsWith("%"))) {
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: declaredOrder of shape ${shapeId} omits a non-internal field`,
|
||||
);
|
||||
}
|
||||
const byName = new Map(shape.fields.map((f) => [f.name, f]));
|
||||
// Function-valued properties are omitted, as JSON.stringify omits them.
|
||||
const emitFields = order
|
||||
.map((n) => byName.get(n))
|
||||
.filter((f) => f !== undefined)
|
||||
.filter((f) => f.type.kind !== "func");
|
||||
const droppable =
|
||||
emitFields.some((f) => undefinedArmTag(f.type, this.host.unionsById) >= 0) ||
|
||||
!!shape.indexValue;
|
||||
this.putc(B, "%b", "123"); // '{'
|
||||
if (!droppable) {
|
||||
emitFields.forEach((f, i) => {
|
||||
this.puts(B, "%b", `${i > 0 ? "," : ""}${jsonObjectKeyLabel(f.name)}`);
|
||||
if (edgeable(f.type)) this.jbEdgeProp(B, f.name);
|
||||
const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type);
|
||||
B.line(
|
||||
`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`,
|
||||
emitPerKeyOrder(B, shape, fieldBase, "%v", (order) => {
|
||||
const inOrder = new Set(order);
|
||||
if (shape.fields.some((f) => !inOrder.has(f.name) && !f.name.startsWith("%"))) {
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: declaredOrder of shape ${shapeId} omits a non-internal field`,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const first = B.slot();
|
||||
B.entryAllocas.push(`${first} = alloca i1`);
|
||||
B.line(`store i1 true, ptr ${first}`);
|
||||
const comma = (): void => {
|
||||
const isf = B.tmp();
|
||||
const lc = B.newLabel("jwc.c");
|
||||
const lj = B.newLabel("jwc.j");
|
||||
B.line(`${isf} = load i1, ptr ${first}`);
|
||||
B.condBr(isf, lj, lc);
|
||||
B.startBlock(lc);
|
||||
this.putc(B, "%b", "44"); // ','
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
B.line(`store i1 false, ptr ${first}`);
|
||||
};
|
||||
for (const f of emitFields) {
|
||||
const utag = undefinedArmTag(f.type, this.host.unionsById);
|
||||
const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type);
|
||||
let skip: string | null = null;
|
||||
if (utag >= 0 && f.type.kind === "union") {
|
||||
// Undefined-valued field: dropped, like Node.
|
||||
const tag = this.unionTag(B, v, f.type.unionId);
|
||||
const isu = B.tmp();
|
||||
B.line(`${isu} = icmp eq i32 ${tag}, ${utag}`);
|
||||
const lw = B.newLabel("jwf.w");
|
||||
skip = B.newLabel("jwf.s");
|
||||
B.condBr(isu, skip, lw);
|
||||
B.startBlock(lw);
|
||||
}
|
||||
comma();
|
||||
this.puts(B, "%b", jsonObjectKeyLabel(f.name));
|
||||
if (edgeable(f.type)) this.jbEdgeProp(B, f.name);
|
||||
B.line(
|
||||
`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`,
|
||||
);
|
||||
if (skip !== null) {
|
||||
B.br(skip);
|
||||
B.startBlock(skip);
|
||||
}
|
||||
}
|
||||
if (shape.indexValue) this.emitOverflowEntries(B, shape, first, edgeable(shape.indexValue));
|
||||
}
|
||||
const byName = new Map(shape.fields.map((f) => [f.name, f]));
|
||||
// Function-valued properties are omitted, as JSON.stringify omits them.
|
||||
const emitFields = order
|
||||
.map((n) => byName.get(n))
|
||||
.filter((f) => f !== undefined)
|
||||
.filter((f) => f.type.kind !== "func");
|
||||
const droppable =
|
||||
emitFields.some((f) => undefinedArmTag(f.type, this.host.unionsById) >= 0) ||
|
||||
!!shape.indexValue;
|
||||
this.putc(B, "%b", "123"); // '{'
|
||||
if (!droppable) {
|
||||
emitFields.forEach((f, i) => {
|
||||
this.puts(B, "%b", `${i > 0 ? "," : ""}${jsonObjectKeyLabel(f.name)}`);
|
||||
if (edgeable(f.type)) this.jbEdgeProp(B, f.name);
|
||||
const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type);
|
||||
B.line(
|
||||
`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const first = B.slot();
|
||||
B.entryAllocas.push(`${first} = alloca i1`);
|
||||
B.line(`store i1 true, ptr ${first}`);
|
||||
const comma = (): void => {
|
||||
const isf = B.tmp();
|
||||
const lc = B.newLabel("jwc.c");
|
||||
const lj = B.newLabel("jwc.j");
|
||||
B.line(`${isf} = load i1, ptr ${first}`);
|
||||
B.condBr(isf, lj, lc);
|
||||
B.startBlock(lc);
|
||||
this.putc(B, "%b", "44"); // ','
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
B.line(`store i1 false, ptr ${first}`);
|
||||
};
|
||||
for (const f of emitFields) {
|
||||
const utag = undefinedArmTag(f.type, this.host.unionsById);
|
||||
const v = this.loadField(B, "%v", shapeId, fieldIndex.get(f.name)!, f.type);
|
||||
let skip: string | null = null;
|
||||
if (utag >= 0 && f.type.kind === "union") {
|
||||
// Undefined-valued field: dropped, like Node.
|
||||
const tag = this.unionTag(B, v, f.type.unionId);
|
||||
const isu = B.tmp();
|
||||
B.line(`${isu} = icmp eq i32 ${tag}, ${utag}`);
|
||||
const lw = B.newLabel("jwf.w");
|
||||
skip = B.newLabel("jwf.s");
|
||||
B.condBr(isu, skip, lw);
|
||||
B.startBlock(lw);
|
||||
}
|
||||
comma();
|
||||
this.puts(B, "%b", jsonObjectKeyLabel(f.name));
|
||||
if (edgeable(f.type)) this.jbEdgeProp(B, f.name);
|
||||
B.line(
|
||||
`call void @${this.jsonWriteHelper(f.type)}(ptr %b, ${this.valTy(f.type)} ${v}) ; ${llvmCommentText(f.name)}`,
|
||||
);
|
||||
if (skip !== null) {
|
||||
B.br(skip);
|
||||
B.startBlock(skip);
|
||||
}
|
||||
}
|
||||
if (shape.indexValue) this.emitOverflowEntries(B, shape, first, edgeable(shape.indexValue));
|
||||
}
|
||||
});
|
||||
this.putc(B, "%b", "125"); // '}'
|
||||
if (cyclic) this.jbLeave(B);
|
||||
}
|
||||
@@ -457,6 +477,7 @@ export class LlWalkers {
|
||||
throw new InternalCompilerError(`llvm emitter bug: field writer of unknown shape ${shapeId}`);
|
||||
const name = `sc_jwf_${this.recordFieldWriters.size}`;
|
||||
this.recordFieldWriters.set(shapeId, name);
|
||||
this.host.journalAllocated?.(name, "walkers.recordFieldWriter", [shapeId]);
|
||||
const fieldBase = this.host.nullableUnions.fieldBase(shapeId);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + fieldBase]));
|
||||
const byName = new Map(shape.fields.map((f) => [f.name, f]));
|
||||
@@ -508,6 +529,7 @@ export class LlWalkers {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -852,6 +874,7 @@ export class LlWalkers {
|
||||
if (!def) throw new InternalCompilerError(`llvm emitter bug: join of unknown union ${unionId}`);
|
||||
const name = `sc_uj_${this.unionJoinFns.size}`;
|
||||
this.unionJoinFns.set(unionId, name);
|
||||
this.host.journalAllocated?.(name, "walkers.unionJoinHelper", [unionId]);
|
||||
|
||||
const host = this.host;
|
||||
host.declare(`declare void @scr_jb_init(ptr)`);
|
||||
@@ -946,6 +969,7 @@ export class LlWalkers {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { IrExpr } from "../../ir/ir.js";
|
||||
import type { IrExpr, IrFunction, IrStmt } from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import { integerArithmeticRange, type IntegerRange } from "../../ir/integer-ranges.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import type { LlInteger } from "./integer-values.js";
|
||||
@@ -47,6 +48,7 @@ export function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerR
|
||||
return ELEMENT_RANGES.i32!;
|
||||
// A UTF-16 code unit, or NaN past either end.
|
||||
if (e.kind === "strIntrinsic" && e.method === "charCodeAt") return { min: 0, max: 65535 };
|
||||
if (e.kind === "varRef") return host.integerOrNaNLocals.get(e.localId) ?? null;
|
||||
if (e.kind === "bin" && (e.op === "+" || e.op === "-")) {
|
||||
const left = integerOrNaNRange(host, e.left);
|
||||
const right = left && integerOrNaNRange(host, e.right);
|
||||
@@ -55,6 +57,34 @@ export function integerOrNaNRange(host: LlvmEmitterContext, e: IrExpr): IntegerR
|
||||
return null;
|
||||
}
|
||||
|
||||
/** An index that is an exact integer or NaN (`integerOrNaNRange`): the integer as an i64, and an
|
||||
* i1 that is false when the index is NaN. An index check then needs no round trip through a double:
|
||||
* the integer is the value's ToUint32 view where the range proves it holds the whole value, or one
|
||||
* conversion, which is exact for a safe integer and frozen, since the conversion of NaN is poison.
|
||||
* Null for any other index. */
|
||||
export function integerOrNaNIndex(
|
||||
host: LlvmEmitterContext,
|
||||
value: LlValue,
|
||||
expr: IrExpr | undefined,
|
||||
): { wide: string; ordered: string } | null {
|
||||
const range = expr ? integerOrNaNRange(host, expr) : null;
|
||||
if (!range) return null;
|
||||
const B = host.B;
|
||||
const ordered = B.tmp();
|
||||
const wide = B.tmp();
|
||||
B.line(`${ordered} = fcmp ord double ${value.name}, 0.0`);
|
||||
if (value.uint32 !== undefined && range.min >= 0 && range.max <= 4294967295) {
|
||||
B.line(`${wide} = zext i32 ${value.uint32} to i64`);
|
||||
} else if (value.uint32 !== undefined && range.min >= -2147483648 && range.max <= 2147483647) {
|
||||
B.line(`${wide} = sext i32 ${value.uint32} to i64`);
|
||||
} else {
|
||||
const raw = B.tmp();
|
||||
B.line(`${raw} = fptosi double ${value.name} to i64`);
|
||||
B.line(`${wide} = freeze i64 ${raw}`);
|
||||
}
|
||||
return { wide, ordered };
|
||||
}
|
||||
|
||||
const ELEMENT_RANGES: Partial<Record<string, IntegerRange>> = {
|
||||
u8: { min: 0, max: 255 },
|
||||
u8c: { min: 0, max: 255 },
|
||||
@@ -131,3 +161,85 @@ export function emitWrappingSum(
|
||||
B.line(`${out} = select i1 ${valid}, i32 ${sum}, i32 0`);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The number locals of `fn` that only hold exact integers (never -0) or NaN, with the range of
|
||||
* those integers: locals declared once with an initializer, neither parameters nor captured or
|
||||
* boxed, whose initializer and every later write `integerOrNaNRange` covers. A local joins once
|
||||
* the values it is written depend only on locals that have joined, so `x = x + 1` keeps `x` out:
|
||||
* its values have no bound. Uses the function's `integerRanges` and `int32Elements`. */
|
||||
export function findIntegerOrNaNLocals(
|
||||
host: LlvmEmitterContext,
|
||||
fn: IrFunction,
|
||||
): ReadonlyMap<string, IntegerRange> {
|
||||
const found = new Map<string, IntegerRange>();
|
||||
if (fn.async || fn.generator) return found;
|
||||
const excluded = new Set<string>();
|
||||
for (const param of fn.params) excluded.add(param.localId);
|
||||
for (const capture of fn.captures ?? []) excluded.add(capture.localId);
|
||||
for (const capture of fn.classCaptures ?? []) excluded.add(capture.localId);
|
||||
const candidates = new Set<string>();
|
||||
for (const local of fn.locals)
|
||||
if (local.type.kind === "f64" && !local.boxed && !local.tdz && !excluded.has(local.id))
|
||||
candidates.add(local.id);
|
||||
if (candidates.size === 0) return found;
|
||||
const declared = new Set<string>();
|
||||
const writes = new Map<string, IrExpr[]>();
|
||||
const drop = (id: string): void => {
|
||||
candidates.delete(id);
|
||||
writes.delete(id);
|
||||
};
|
||||
const write = (id: string, value: IrExpr): void => {
|
||||
if (!candidates.has(id)) return;
|
||||
const list = writes.get(id);
|
||||
if (list) list.push(value);
|
||||
else writes.set(id, [value]);
|
||||
};
|
||||
function walkExpr(e: IrExpr): boolean {
|
||||
if (e.kind === "assignExpr") write(e.localId, e.value);
|
||||
else if (e.kind === "incDec") drop(e.localId);
|
||||
return everyExprChild(e, walkExpr, walkStmt);
|
||||
}
|
||||
function walkStmt(s: IrStmt): boolean {
|
||||
if (s.kind === "assign") write(s.localId, s.value);
|
||||
else if (s.kind === "varDecl") {
|
||||
if (s.init === null || declared.has(s.localId)) drop(s.localId);
|
||||
else {
|
||||
declared.add(s.localId);
|
||||
write(s.localId, s.init);
|
||||
}
|
||||
} else if (s.kind === "forOf") drop(s.localId);
|
||||
else if (s.kind === "tryCatch" && s.catchLocalId) drop(s.catchLocalId);
|
||||
return everyStmtChild(s, walkExpr, walkStmt);
|
||||
}
|
||||
for (const s of fn.body) walkStmt(s);
|
||||
// `integerOrNaNRange` reads the joined locals while the next ones are tested.
|
||||
const previous = host.integerOrNaNLocals;
|
||||
host.integerOrNaNLocals = found;
|
||||
try {
|
||||
let joined = true;
|
||||
while (joined) {
|
||||
joined = false;
|
||||
for (const [id, values] of writes) {
|
||||
if (found.has(id) || !declared.has(id)) continue;
|
||||
let range: IntegerRange | null = null;
|
||||
for (const value of values) {
|
||||
const next = integerOrNaNRange(host, value);
|
||||
if (next === null) {
|
||||
range = null;
|
||||
break;
|
||||
}
|
||||
range =
|
||||
range === null
|
||||
? next
|
||||
: { min: Math.min(range.min, next.min), max: Math.max(range.max, next.max) };
|
||||
}
|
||||
if (range === null) continue;
|
||||
found.set(id, range);
|
||||
joined = true;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
host.integerOrNaNLocals = previous;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import type { IrExpr, IrStmt, IrModule, IrType, SrcLoc } from "../ir/ir.js";
|
||||
import { isFfiCallbackParam, MAY_THROW_ARR_METHODS, MAY_THROW_BYTES_METHODS } from "../ir/ir.js";
|
||||
import { MAY_THROW_LIB_FNS } from "../ir/builtin-effects.js";
|
||||
import { everyExprChild, everyStmtChild, everyStmtList } from "../ir/traverse.js";
|
||||
import { everyExprChild, everyStmtChild } from "../ir/traverse.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex } from "../ir/node-index.js";
|
||||
import { hasRetainedFfiCallback } from "./ffi-callbacks.js";
|
||||
import { computePublishedTypes, writesPublished } from "./publication.js";
|
||||
|
||||
@@ -35,7 +36,14 @@ 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>;
|
||||
/** The module's node index, read instead of walking the bodies. */
|
||||
nodes?: IrNodeIndex;
|
||||
} = {},
|
||||
): {
|
||||
fns: Set<string>;
|
||||
/** With `explain`: the reason each may-throw function was marked. */
|
||||
@@ -47,10 +55,12 @@ export function computeMayThrow(
|
||||
* entry (and may therefore throw the stack-overflow RangeError). */
|
||||
stackChecks?: Set<string>;
|
||||
} {
|
||||
const terminations = mod.workers === true ? workerTerminationPolls(mod) : null;
|
||||
const stackChecks = options.stackChecks === true ? stackCheckedFunctions(mod) : null;
|
||||
const terminations = mod.workers === true ? workerTerminationPolls(mod, options.nodes) : null;
|
||||
const stackChecks =
|
||||
options.stackChecks === true ? stackCheckedFunctions(mod, options.nodes) : 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[];
|
||||
@@ -892,7 +902,19 @@ function boundedNode(rec: IrExpr | IrStmt): boolean {
|
||||
* finishes after finitely many steps or reaches a polling callee. `polls`
|
||||
* names the functions that poll (and therefore may unwind with the
|
||||
* termination sentinel); `entry` the subset that polls on entry. */
|
||||
export function workerTerminationPolls(mod: IrModule): {
|
||||
/** The kinds call-graph census below can skip: boundedNode answers true
|
||||
* for them, and the census reads nothing else from them. */
|
||||
const CENSUS_SKIPPED_KINDS: ReadonlySet<string> = new Set(
|
||||
[...BOUNDED_KINDS].filter(
|
||||
(kind) =>
|
||||
!["while", "doWhile", "for", "forOf", "call", "new", "virtualCall", "closure"].includes(kind),
|
||||
),
|
||||
);
|
||||
|
||||
export function workerTerminationPolls(
|
||||
mod: IrModule,
|
||||
nodes?: IrNodeIndex,
|
||||
): {
|
||||
entry: Set<string>;
|
||||
polls: Set<string>;
|
||||
} {
|
||||
@@ -908,11 +930,12 @@ export function workerTerminationPolls(mod: IrModule): {
|
||||
const edges = new Map<string, string[]>();
|
||||
const entry = new Set<string>();
|
||||
const polls = new Set<string>();
|
||||
const scan = nodes?.scanExcept(CENSUS_SKIPPED_KINDS);
|
||||
for (const fn of mod.functions) {
|
||||
const out: string[] = [];
|
||||
let bounded = fn.async !== true && fn.generator === undefined && fn.name !== mod.entry;
|
||||
let loops = false;
|
||||
const visit = (rec: IrExpr | IrStmt): boolean => {
|
||||
const visit = (rec: IrExpr | IrStmt): void => {
|
||||
if (!boundedNode(rec)) bounded = false;
|
||||
switch (rec.kind) {
|
||||
case "while":
|
||||
@@ -931,9 +954,8 @@ export function workerTerminationPolls(mod: IrModule): {
|
||||
out.push(...(methodImpls.get(rec.method) ?? []));
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
everyStmtList(fn.body, { expr: visit, stmt: visit });
|
||||
eachNodeOfKinds(fn, scan, visit);
|
||||
edges.set(
|
||||
fn.name,
|
||||
out.filter((callee) => names.has(callee)),
|
||||
@@ -954,7 +976,7 @@ export function workerTerminationPolls(mod: IrModule): {
|
||||
* library call; see boundedNode) also reaches every function that such an
|
||||
* operation can invoke: closure targets and class members. Non-recursive
|
||||
* functions, including leaf callbacks, stay check-free. */
|
||||
export function stackCheckedFunctions(mod: IrModule): Set<string> {
|
||||
export function stackCheckedFunctions(mod: IrModule, nodes?: IrNodeIndex): Set<string> {
|
||||
const reentry = "\0reentry";
|
||||
const methodImpls = new Map<string, string[]>();
|
||||
const memberPrefixes = new Set<string>();
|
||||
@@ -969,12 +991,13 @@ export function stackCheckedFunctions(mod: IrModule): Set<string> {
|
||||
const names = new Set(mod.functions.map((fn) => fn.name));
|
||||
const edges = new Map<string, string[]>();
|
||||
const targets = new Set<string>();
|
||||
const scan = nodes?.scanExcept(CENSUS_SKIPPED_KINDS);
|
||||
for (const fn of mod.functions) {
|
||||
const dot = fn.name.indexOf(".");
|
||||
if (dot > 0 && memberPrefixes.has(fn.name.slice(0, dot + 1))) targets.add(fn.name);
|
||||
const out: string[] = [];
|
||||
let reenters = false;
|
||||
const visit = (rec: IrExpr | IrStmt): boolean => {
|
||||
const visit = (rec: IrExpr | IrStmt): void => {
|
||||
if (!boundedNode(rec)) reenters = true;
|
||||
switch (rec.kind) {
|
||||
case "call":
|
||||
@@ -990,9 +1013,8 @@ export function stackCheckedFunctions(mod: IrModule): Set<string> {
|
||||
targets.add(rec.fnName);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
everyStmtList(fn.body, { expr: visit, stmt: visit });
|
||||
eachNodeOfKinds(fn, scan, visit);
|
||||
const callees = out.filter((callee) => names.has(callee));
|
||||
if (reenters) callees.push(reentry);
|
||||
edges.set(fn.name, callees);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user