mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Call toJSON on foreign elements and merge main
This commit is contained in:
@@ -9,6 +9,7 @@ node_modules/
|
||||
!tests/corpus/*/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/
|
||||
!tests/coverage-fixtures/wasm/node_modules/
|
||||
!tests/coverage-fixtures/node-islands*/node_modules/
|
||||
!tests/coverage-fixtures/dynamic-tier/package/node_modules/
|
||||
!tests/fixtures/npm-static/nested/inner/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/*/node_modules/
|
||||
|
||||
@@ -20,6 +20,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `json-records` | cpu | `JSON.stringify`, typed `JSON.parse`, Map aggregation |
|
||||
| `json-unions` | cpu | `JSON.parse` into discriminated unions: an event log with eight kinds and nested expression trees with nine, evaluated after the cast |
|
||||
| `keyed-option-writes` | cpu | class methods whose names are also keys of option records written through element accesses: `enabled[name] = true` over a union of literal names and `totals["count"] = n` |
|
||||
| `line-reader` | cpu | `readline` over a file stream: 'line' events for a 67 MB access log (every 50th line not ASCII), with a status count, a path tally and byte totals |
|
||||
| `log-lines` | cpu | 200k immediate `console.log`/`process.stdout.write` lines into the runner's pipe |
|
||||
| `log-summary`, `inventory-report` | cpu | the multi-module applications from `benchmarks/builds`, on 300k-line inputs |
|
||||
| `number-maps` | cpu | number-keyed Map/Set: a dense-id table, sparse ids in many small maps, get/has/set mixes |
|
||||
@@ -29,6 +30,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `record-boundaries` | cpu | records built from unchecked array reads passed to and returned from annotated functions |
|
||||
| `records-sort` | cpu | comparator sorts, group-by, `reduce`/`filter` |
|
||||
| `regex-logs` | cpu | regex `exec` with groups, `test`, global `replace` |
|
||||
| `regex-replacers` | cpu | regex `replace`/`replaceAll` with inline callbacks whose parameters have no annotations: dictionary placeholders, snake_case to camelCase, date reordering, optional captures, capitalization, and a string search that reads the offset |
|
||||
| `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds |
|
||||
| `template-render` | cpu | template literals, escaping, `map`/`join` |
|
||||
| `throw-chains` | cpu | exceptions caught 2, 5 and 20 recursive calls above the throw, through direct calls and through a callback, and a backtracking parser that throws on a failed alternative |
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"entry": "workloads/line-reader.ts",
|
||||
"input": "line-reader",
|
||||
"args": [
|
||||
"{input}"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Line-by-line processing of a log file with readline over a file stream:
|
||||
// 'line' events for about 67 MB of access-log lines, with light work per
|
||||
// line (a status count, a path tally, byte totals).
|
||||
import { createReadStream } from "node:fs";
|
||||
import { createInterface } from "node:readline";
|
||||
|
||||
const path = process.argv[2]!;
|
||||
const statuses = new Map<string, number>();
|
||||
const paths = new Map<string, number>();
|
||||
let lines = 0;
|
||||
let chars = 0;
|
||||
let bytes = 0;
|
||||
let longest = 0;
|
||||
|
||||
const input = createInterface({ input: createReadStream(path), crlfDelay: Infinity });
|
||||
input.on("line", (line: string) => {
|
||||
lines++;
|
||||
chars += line.length;
|
||||
if (line.length > longest) longest = line.length;
|
||||
const quote = line.indexOf('"');
|
||||
const space = line.indexOf(" ", quote);
|
||||
const path = line.slice(space + 1, line.indexOf("/", line.indexOf("/", space) + 1));
|
||||
paths.set(path, (paths.get(path) ?? 0) + 1);
|
||||
const close = line.indexOf('"', space);
|
||||
const status = line.slice(close + 2, close + 5);
|
||||
statuses.set(status, (statuses.get(status) ?? 0) + 1);
|
||||
bytes += Number(line.slice(close + 6, line.indexOf(" ", close + 6)));
|
||||
});
|
||||
input.on("close", () => {
|
||||
console.log(`${lines} lines, ${chars} characters, longest ${longest}`);
|
||||
console.log(`bytes ${bytes}`);
|
||||
for (const status of [...statuses.keys()].sort()) console.log(`status ${status}: ${statuses.get(status)}`);
|
||||
for (const prefix of [...paths.keys()].sort()) console.log(`path ${prefix}: ${paths.get(prefix)}`);
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"entry": "workloads/regex-replacers.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Regex replace and replaceAll with inline callbacks, written as they
|
||||
// usually are, without parameter annotations: template placeholders looked
|
||||
// up in a dictionary, snake_case to camelCase, date reordering, optional
|
||||
// version parts, capitalization and a string search with its offset.
|
||||
let seed = 7;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
function pick<T>(items: readonly T[]): T {
|
||||
return items[Math.floor(random() * items.length)]!;
|
||||
}
|
||||
|
||||
const words = ["user", "order", "item", "cart", "price", "total", "status", "created", "updated", "id"];
|
||||
const names = ["Ada", "Grace", "Linus", "Barbara", "Ken", "Margaret"];
|
||||
const vars: Record<string, string> = {
|
||||
name: "world",
|
||||
city: "Lisbon",
|
||||
day: "Tuesday",
|
||||
plan: "pro",
|
||||
count: "12",
|
||||
};
|
||||
const keys = [...Object.keys(vars), "missing"];
|
||||
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
const count = Math.floor(20000 * scale);
|
||||
const templates: string[] = [];
|
||||
const identifiers: string[] = [];
|
||||
const dates: string[] = [];
|
||||
const versions: string[] = [];
|
||||
const sentences: string[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
templates.push(`Hello {${pick(keys)}}, your {${pick(keys)}} plan renews on {${pick(keys)}} in {${pick(keys)}}.`);
|
||||
identifiers.push(`${pick(words)}_${pick(words)}_${pick(words)}${random() < 0.5 ? "_" + pick(words) : ""}`);
|
||||
const year = 1990 + Math.floor(random() * 40);
|
||||
const month = String(1 + Math.floor(random() * 12)).padStart(2, "0");
|
||||
const day = String(1 + Math.floor(random() * 28)).padStart(2, "0");
|
||||
dates.push(`from ${year}-${month}-${day} to ${year + 1}-${month}-${day} (${pick(names)})`);
|
||||
versions.push(`v${i % 9}.${i % 13}${random() < 0.5 ? "." + (i % 7) : ""} and v${i % 5}.${i % 3}`);
|
||||
sentences.push(`${pick(names)} said the ${pick(words)} and the ${pick(words)} were ready for ${pick(names)}`);
|
||||
}
|
||||
|
||||
let length = 0;
|
||||
let checksum = 0;
|
||||
for (let round = 0; round < 4; round++) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const filled = templates[i]!.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`);
|
||||
const camel = identifiers[i]!.replace(/_([a-z])/g, (_m, c) => c.toUpperCase());
|
||||
const reordered = dates[i]!.replace(/(\d{4})-(\d{2})-(\d{2})/g, (_m, y, m, d) => `${d}/${m}/${y}`);
|
||||
const padded = versions[i]!.replace(/v(\d+)\.(\d+)(?:\.(\d+))?/g, (_m, major, minor, patch) => `v${major}.${minor}.${patch ?? "0"}`);
|
||||
const titled = sentences[i]!.replace(/\b[a-z]/g, (c) => c.toUpperCase());
|
||||
const marked = sentences[i]!.replaceAll("the", (m, at) => (at % 2 === 0 ? m.toUpperCase() : m));
|
||||
length += filled.length + camel.length + reordered.length + padded.length + titled.length + marked.length;
|
||||
checksum = (checksum * 31 + camel.charCodeAt(camel.length - 1) + padded.charCodeAt(padded.length - 1)) >>> 0;
|
||||
}
|
||||
}
|
||||
console.log(templates[0]!.replace(/\{(\w+)\}/g, (_m, key) => vars[key] ?? `<${key}>`));
|
||||
console.log(identifiers[1]!.replace(/_([a-z])/g, (_m, c) => c.toUpperCase()));
|
||||
console.log(versions[2]!.replace(/v(\d+)\.(\d+)(?:\.(\d+))?/g, (_m, major, minor, patch) => `v${major}.${minor}.${patch ?? "0"}`));
|
||||
console.log("length", length, "checksum", checksum);
|
||||
@@ -245,7 +245,7 @@ $ scriptc build scale.ts
|
||||
1 error.
|
||||
```
|
||||
|
||||
A copy such as `slice()` or a spread is checked as if it were the same array as its source. The results of callbacks are not followed, so an array built by `map` or `Array.from` with a mapping function cannot be `Int32[]`; build it with a loop that stores `value | 0`. scriptc refuses an `Int32[]` slot whose array can come from code it cannot see, such as `JSON.parse`, dynamic values or the callers of a function used as a value, and an array that can reach one and escapes to such code, such as a call through a function value (SC7003).
|
||||
A copy such as `slice()` or a spread is checked as if it were the same array as its source. The results of a callback are followed only when scriptc copies it into the loop that runs the array method: a callback written at a `map` or `filter` call whose captured variables are not reassigned, such as `xs.map((x) => (x * 2) | 0)`. Other callback results are not followed, so for example an array built by `Array.from` with a mapping function cannot be `Int32[]`; build it with a loop that stores `value | 0`. scriptc refuses an `Int32[]` slot whose array can come from code it cannot see, such as `JSON.parse`, dynamic values or the callers of a function used as a value, and an array that can reach one and escapes to such code, such as a call through a function value (SC7003).
|
||||
|
||||
## Immutable
|
||||
|
||||
|
||||
@@ -220,6 +220,14 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/tls-ciphers.mdx</include>
|
||||
|
||||
### TLS certificates
|
||||
|
||||
<include>../limitations/tls-certificates.mdx</include>
|
||||
|
||||
### TLS sessions
|
||||
|
||||
<include>../limitations/tls-sessions.mdx</include>
|
||||
|
||||
### Console and process streams
|
||||
|
||||
<include>../limitations/console-and-process-streams.mdx</include>
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`.
|
||||
`crypto.sign`, `crypto.verify`, `createSign`, and `createVerify` support RSA PKCS#1 v1.5 and PSS padding and ECDSA with DER and IEEE P1363 encodings over SHA-1 and SHA-2 digests, and Ed25519 through `crypto.sign` and `crypto.verify` with a `null` or `undefined` algorithm.
|
||||
Ed25519 signing, verification, and private keys link when a string literal in the program names `ed25519` (in any case); without one they throw a coded SC2020 error, while Ed25519 public keys still import and export.
|
||||
X509Certificate supports `publicKey`, `verify()`, `checkIssued()`, `checkPrivateKey()`, `checkHost()`, `checkEmail()`, `checkIP()`, `toLegacyObject()`, `toString()`, and `toJSON()`, and `String()`, template literals, and `+` give its PEM text as `toString()` does.
|
||||
`publicKey`, `verify()`, and `checkPrivateKey()` are refused in wasm32-wasi builds, and an Ed25519 certificate's `verify()` needs the `ed25519` literal like the other Ed25519 operations.
|
||||
Own-key walks over a certificate (`Object.keys()`, `Object.entries()`, spread, `for...in`, and `hasOwnProperty`) are refused, since Node keeps its fields as prototype getters.
|
||||
`JSON.stringify()` and `console.log()` of a certificate, a certificate stored in an `unknown` or `any` slot, and its methods on a union-typed value are also refused.
|
||||
Sign and Verify values accumulate `update()` data and are not writable streams.
|
||||
Encrypted private keys (`passphrase`, `cipher`), the other key types (`rsa-pss`, `dsa`, `ed448`, `x448`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused.
|
||||
Comparing keys of different types with `equals()` answers false; Node also leaves an OpenSSL error queued, which a later key operation can report.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[Contract types](/docs/contracts) are checked only where they are written as the annotation of a parameter, variable, class instance field or function result, `Acyclic` only on classes, interfaces and object type aliases, and `ProgramLifetime` only in the `implements` clause of a class declaration; other aliases, interface members and nested type positions are reported.
|
||||
In programs that use worker threads, a surrounding `try`/`catch` does not discharge `NoThrow`.
|
||||
`Int32` refuses parameters of functions used as values and fields of classes whose instances are stored in `any` or `unknown` values.
|
||||
`Int32[]` refuses arrays that can come from or escape to code scriptc cannot see, does not follow callback results (such as `map` results), and checks a copy made by `slice` or a spread as if it were its source array.
|
||||
`Int32[]` refuses arrays that can come from or escape to code scriptc cannot see, follows callback results only for callbacks written at a `map` or `filter` call that scriptc runs inline, and checks a copy made by `slice` or a spread as if it were its source array.
|
||||
Element reads that find a value are int32s, but a scalar `Int32` slot still needs `a[i] | 0`, because a read can miss and produce `undefined`.
|
||||
`Immutable` refuses values stored in `any` or `unknown` or that another reference can still change.
|
||||
`ProgramLifetime` applies to root classes of executables only; instances the program drops are never reclaimed unless they belong to a released [lifetime region](/docs/experimental#lifetimeregion).
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
`tlsSocket.getPeerCertificate()`, `getCertificate()`, `getPeerX509Certificate()`, and `getX509Certificate()` follow Node, including the `detailed` issuer chain, on sockets whose type is known.
|
||||
On a TLS socket held in an `unknown` or `any` value, or in an untyped JavaScript parameter, they throw an error that names the call as not supported.
|
||||
Client certificates follow Node: `tls.createServer`, `https.createServer`, and `new tls.TLSSocket` take `requestCert`, `rejectUnauthorized`, and `ca`, and `tls.connect` and `https.request` take `cert` and `key`.
|
||||
A server's `authorized` and `authorizationError` report the client certificate's verification as Node does, and a server, client, or `tls.createSecureContext` given a `ca` also sends the chain it completes, as OpenSSL's automatic chain does.
|
||||
A server verifies client certificates against the `ca` of the context it serves, including contexts from `addContext()` and `setSecureContext()`: an empty `ca` (an empty Buffer or array) trusts no certificate, and an omitted `ca` trusts the default CAs.
|
||||
The same holds for a client's `ca` in `tls.connect`, `https.request`, and `http2.connect`, where an empty string is no `ca`, as in Node.
|
||||
In `http2.connect`, a `ca` held in an `unknown` or `any` value that is an empty string is refused.
|
||||
`requestCert` beside a TLSSocket's `secureContext` or an `SNICallback`, client certificates on http2 secure servers, and a `cert` without a `key` (or the reverse) are refused.
|
||||
`server.addContext()` and `server.setSecureContext()` follow Node on tls and https servers, including wildcard names, which match one label without regard to case.
|
||||
A name with regular-expression groups or alternation, or one outside ASCII, is refused, as are both methods on a server that is not a tls or https server.
|
||||
The `'tlsClientError'` event reports Node's error for a failed handshake: OpenSSL's code and reason, or `socket hang up` for a client that leaves.
|
||||
OpenSSL's messages leave out the thread identifier Node prints before `error:` and the source location after the reason.
|
||||
A failure whose OpenSSL reason the runtime does not know, such as a malformed client hello, is reported as an error with code `SC2020` that says so.
|
||||
The X509Certificate those accessors return has the same refusals as any other certificate (see Crypto), so in JavaScript narrow it, for example with `if (cert)`, before reading its fields.
|
||||
@@ -0,0 +1,3 @@
|
||||
TLS servers send no session tickets, so a connection to them cannot be resumed.
|
||||
A client's `'session'` event fires once per connection, when its handshake finishes and before `secureConnect`.
|
||||
Node fires it for each ticket the server sends, after `secureConnect`, and OpenSSL servers send two.
|
||||
@@ -13822,9 +13822,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -13845,9 +13845,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -13868,9 +13868,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -13891,9 +13891,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -13914,9 +13914,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -13937,9 +13937,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -13960,9 +13960,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -13983,9 +13983,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -14006,9 +14006,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -14029,9 +14029,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -14236,9 +14236,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -23790,9 +23790,9 @@
|
||||
"anchor": "err_tls_required_server_name",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67484,9 +67484,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67599,9 +67599,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67783,9 +67783,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67921,9 +67921,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68013,9 +68013,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68059,9 +68059,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68174,9 +68174,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68312,9 +68312,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68335,9 +68335,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -19394,6 +19394,8 @@
|
||||
"tests": [
|
||||
"tests/corpus/1358-crypto-random.ts",
|
||||
"tests/corpus/1534-crypto-hash-chain.ts",
|
||||
"tests/corpus/1568-x509-fingerprint.ts",
|
||||
"tests/corpus/1572-x509-validity-date-gettime.ts",
|
||||
"tests/corpus/2556-crypto-introspection.ts",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
"tests/corpus/5250-cipher-aes-gcm.ts",
|
||||
@@ -19436,6 +19438,7 @@
|
||||
"tests/corpus/6709-dh-generate.ts",
|
||||
"tests/corpus/6710-dh-encodings.cjs",
|
||||
"tests/corpus/6711-dh-handles.ts",
|
||||
"tests/corpus/6936-x509-certificate-fields.ts",
|
||||
"tests/corpus/7055-cipher-streams.ts",
|
||||
"tests/corpus/7056-cipher-stream-errors.ts",
|
||||
"tests/corpus/7057-cipher-streams.mjs",
|
||||
@@ -19443,6 +19446,10 @@
|
||||
"tests/corpus/7082-crypto-key-objects.ts",
|
||||
"tests/corpus/7083-crypto-sign-verify.ts",
|
||||
"tests/corpus/7084-crypto-key-pairs.cjs",
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/corpus/7122-webcrypto-node-crypto.ts",
|
||||
"tests/corpus/7123-webcrypto-node-crypto.cjs",
|
||||
"tests/corpus/7150-cipher-legacy-modes.ts",
|
||||
@@ -19461,6 +19468,7 @@
|
||||
"tests/fixtures/node-types/dh.ts",
|
||||
"tests/fixtures/node-types/ecdh.ts",
|
||||
"tests/fixtures/node-types/keys.ts",
|
||||
"tests/fixtures/node-types/x509-methods.ts",
|
||||
"tests/harness/npm-static.test.ts"
|
||||
]
|
||||
},
|
||||
@@ -23784,8 +23792,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:X509Certificate",
|
||||
"tests": [
|
||||
"tests/corpus/1568-x509-fingerprint.ts",
|
||||
"tests/corpus/1572-x509-validity-date-gettime.ts",
|
||||
"tests/corpus/6936-x509-certificate-fields.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23807,8 +23820,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.checkEmail",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23830,8 +23850,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.checkHost",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23853,8 +23880,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.checkIP",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23876,8 +23910,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.checkIssued",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23899,8 +23940,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.checkPrivateKey",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23922,8 +23970,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.toJSON",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23945,8 +24000,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.toLegacyObject",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23968,8 +24030,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.toString",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -23991,8 +24060,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:x509.verify",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -24198,8 +24274,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:crypto"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:crypto.publicKey",
|
||||
"tests": [
|
||||
"tests/corpus/7090-x509-check-names.ts",
|
||||
"tests/corpus/7091-x509-certificate-keys.ts",
|
||||
"tests/corpus/7092-x509-legacy-object.ts",
|
||||
"tests/corpus/7093-x509-methods-js.mjs",
|
||||
"tests/fixtures/node-types/x509-methods.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -32679,6 +32762,7 @@
|
||||
"tests": [
|
||||
"tests/corpus/6702-ecdh-errors.js",
|
||||
"tests/corpus/6708-dh-errors.js",
|
||||
"tests/corpus/7440-tls-server-add-context.ts",
|
||||
"tests/corpus/7714-zlib-brotli-compress.cjs",
|
||||
"tests/corpus/caught-system-error-fields.ts",
|
||||
"tests/corpus/error-stack-frames.ts",
|
||||
@@ -39012,8 +39096,11 @@
|
||||
"anchor": "err_tls_required_server_name",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:errors"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:ERR_TLS_REQUIRED_SERVER_NAME",
|
||||
"tests": [
|
||||
"tests/corpus/7440-tls-server-add-context.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -95877,6 +95964,17 @@
|
||||
"tests/corpus/6722-tls-min-max-version.ts",
|
||||
"tests/corpus/7087-tls-socket-wrap.ts",
|
||||
"tests/corpus/7088-tls-socket-connect-tunnel.ts",
|
||||
"tests/corpus/7095-tls-peer-certificates.ts",
|
||||
"tests/corpus/7096-tls-peer-certificates.mjs",
|
||||
"tests/corpus/7097-tls-client-certificates.ts",
|
||||
"tests/corpus/7098-tls-client-certificates-paths.ts",
|
||||
"tests/corpus/7099-tls-client-certificates.mjs",
|
||||
"tests/corpus/7440-tls-server-add-context.ts",
|
||||
"tests/corpus/7441-tls-client-error.ts",
|
||||
"tests/corpus/7442-tls-server-contexts.mjs",
|
||||
"tests/fixtures/node-types/tls-client-certificates.ts",
|
||||
"tests/fixtures/node-types/tls-peer-certificates.ts",
|
||||
"tests/fixtures/node-types/tls-server-contexts.ts",
|
||||
"tests/fixtures/node-types/tls-socket-wrap.ts"
|
||||
]
|
||||
},
|
||||
@@ -96245,8 +96343,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:server.addContext",
|
||||
"tests": [
|
||||
"tests/corpus/7440-tls-server-add-context.ts",
|
||||
"tests/corpus/7441-tls-client-error.ts",
|
||||
"tests/corpus/7442-tls-server-contexts.mjs",
|
||||
"tests/fixtures/node-types/tls-server-contexts.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96372,8 +96476,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:server.setSecureContext",
|
||||
"tests": [
|
||||
"tests/corpus/7440-tls-server-add-context.ts",
|
||||
"tests/corpus/7441-tls-client-error.ts",
|
||||
"tests/corpus/7442-tls-server-contexts.mjs",
|
||||
"tests/fixtures/node-types/tls-server-contexts.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96556,8 +96666,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:'tlsClientError'",
|
||||
"tests": [
|
||||
"tests/corpus/7440-tls-server-add-context.ts",
|
||||
"tests/corpus/7441-tls-client-error.ts",
|
||||
"tests/corpus/7442-tls-server-contexts.mjs",
|
||||
"tests/fixtures/node-types/tls-server-contexts.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96699,8 +96815,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:tlsSocket.getCertificate",
|
||||
"tests": [
|
||||
"tests/corpus/7095-tls-peer-certificates.ts",
|
||||
"tests/corpus/7096-tls-peer-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-peer-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96791,8 +96912,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:tlsSocket.getPeerCertificate",
|
||||
"tests": [
|
||||
"tests/corpus/7095-tls-peer-certificates.ts",
|
||||
"tests/corpus/7096-tls-peer-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-peer-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96860,8 +96986,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:tlsSocket.getPeerX509Certificate",
|
||||
"tests": [
|
||||
"tests/corpus/7095-tls-peer-certificates.ts",
|
||||
"tests/corpus/7096-tls-peer-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-peer-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -96978,8 +97109,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:tlsSocket.getX509Certificate",
|
||||
"tests": [
|
||||
"tests/corpus/7095-tls-peer-certificates.ts",
|
||||
"tests/corpus/7096-tls-peer-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-peer-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -97119,8 +97255,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:tlsSocket.authorizationError",
|
||||
"tests": [
|
||||
"tests/corpus/7097-tls-client-certificates.ts",
|
||||
"tests/corpus/7098-tls-client-certificates-paths.ts",
|
||||
"tests/corpus/7099-tls-client-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-client-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -97142,8 +97284,14 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:tls"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:tls.authorized",
|
||||
"tests": [
|
||||
"tests/corpus/7097-tls-client-certificates.ts",
|
||||
"tests/corpus/7098-tls-client-certificates-paths.ts",
|
||||
"tests/corpus/7099-tls-client-certificates.mjs",
|
||||
"tests/fixtures/node-types/tls-client-certificates.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
|
||||
@@ -208,6 +208,8 @@
|
||||
"stdlib.abort": ["tests/harness/fetch-conformance.test.ts"]
|
||||
},
|
||||
"features": [
|
||||
{ "chapter": "tls", "parent": "Class: `tls.TLSSocket`", "signatures": ["`authorized` Type: {boolean}"], "status": "partial", "evidence": ["tests/corpus/7097-tls-client-certificates.ts", "tests/corpus/7098-tls-client-certificates-paths.ts", "tests/corpus/7099-tls-client-certificates.mjs", "tests/fixtures/node-types/tls-client-certificates.ts"] },
|
||||
{ "chapter": "crypto", "parent": "Class: `X509Certificate`", "signatures": ["`publicKey` Type: {KeyObject}"], "status": "partial", "evidence": ["tests/corpus/7090-x509-check-names.ts", "tests/corpus/7091-x509-certificate-keys.ts", "tests/corpus/7092-x509-legacy-object.ts", "tests/corpus/7093-x509-methods-js.mjs", "tests/fixtures/node-types/x509-methods.ts"] },
|
||||
{ "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.generateKey", "subtle.exportKey", "subtle.encrypt", "subtle.decrypt", "subtle.wrapKey", "subtle.unwrapKey", "subtle.deriveBits", "subtle.deriveKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7130-webcrypto-wrap-key.ts", "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts", "tests/corpus/7081-webcrypto-ed25519.mjs", "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"] },
|
||||
{ "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
|
||||
{ "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
|
||||
@@ -436,6 +438,11 @@
|
||||
{ "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["crypto.webcrypto", "subtle"], "status": "partial", "evidence": ["tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs"] },
|
||||
{ "chapter": "tls", "symbols": ["server.addContext", "server.setSecureContext", "'tlsClientError'"], "status": "partial", "evidence": ["tests/corpus/7440-tls-server-add-context.ts", "tests/corpus/7441-tls-client-error.ts", "tests/corpus/7442-tls-server-contexts.mjs", "tests/fixtures/node-types/tls-server-contexts.ts"] },
|
||||
{ "chapter": "errors", "symbols": ["ERR_TLS_REQUIRED_SERVER_NAME"], "status": "partial", "evidence": ["tests/corpus/7440-tls-server-add-context.ts"] },
|
||||
{ "chapter": "tls", "symbols": ["tlsSocket.authorizationError"], "status": "partial", "evidence": ["tests/corpus/7097-tls-client-certificates.ts", "tests/corpus/7098-tls-client-certificates-paths.ts", "tests/corpus/7099-tls-client-certificates.mjs", "tests/fixtures/node-types/tls-client-certificates.ts"] },
|
||||
{ "chapter": "tls", "symbols": ["tlsSocket.getPeerCertificate", "tlsSocket.getCertificate", "tlsSocket.getPeerX509Certificate", "tlsSocket.getX509Certificate"], "status": "partial", "evidence": ["tests/corpus/7095-tls-peer-certificates.ts", "tests/corpus/7096-tls-peer-certificates.mjs", "tests/fixtures/node-types/tls-peer-certificates.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["x509.checkEmail", "x509.checkHost", "x509.checkIP", "x509.checkIssued", "x509.checkPrivateKey", "x509.toJSON", "x509.toLegacyObject", "x509.toString", "x509.verify"], "status": "partial", "evidence": ["tests/corpus/7090-x509-check-names.ts", "tests/corpus/7091-x509-certificate-keys.ts", "tests/corpus/7092-x509-legacy-object.ts", "tests/corpus/7093-x509-methods-js.mjs", "tests/fixtures/node-types/x509-methods.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["ECDH", "ecdh.computeSecret", "ecdh.generateKeys", "ecdh.getPrivateKey", "ecdh.getPublicKey", "ecdh.setPrivateKey", "ecdh.setPublicKey"], "status": "partial", "evidence": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["DiffieHellman", "DiffieHellmanGroup", "diffieHellman.computeSecret", "diffieHellman.generateKeys", "diffieHellman.getGenerator", "diffieHellman.getPrime", "diffieHellman.getPrivateKey", "diffieHellman.getPublicKey", "diffieHellman.setPrivateKey", "diffieHellman.setPublicKey", "diffieHellman.verifyError"], "status": "partial", "evidence": ["tests/corpus/6706-dh-groups.ts", "tests/corpus/6707-dh-explicit-params.ts", "tests/corpus/6708-dh-errors.js", "tests/corpus/6709-dh-generate.ts", "tests/corpus/6710-dh-encodings.cjs", "tests/corpus/6711-dh-handles.ts", "tests/fixtures/node-types/dh.ts"] },
|
||||
{ "chapter": "errors", "symbols": ["ERR_CRYPTO_UNKNOWN_DH_GROUP", "ERR_CRYPTO_INVALID_KEYLEN", "ERR_CRYPTO_INVALID_STATE"], "status": "partial", "evidence": ["tests/corpus/6708-dh-errors.js"] },
|
||||
@@ -446,6 +453,7 @@
|
||||
{ "symbols": ["crypto.createPrivateKey", "crypto.createPublicKey", "crypto.generateKeyPair", "crypto.generateKeyPairSync", "crypto.sign", "crypto.verify", "crypto.createSign", "crypto.createVerify", "Sign", "sign.update", "sign.sign", "Verify", "verify.update", "verify.verify", "asymmetricKeyType", "asymmetricKeyDetails"], "status": "partial", "evidence": ["tests/corpus/7080-crypto-ed25519.mjs", "tests/corpus/7082-crypto-key-objects.ts", "tests/corpus/7083-crypto-sign-verify.ts", "tests/corpus/7084-crypto-key-pairs.cjs", "tests/fixtures/node-types/keys.ts"] },
|
||||
{ "symbols": ["crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] },
|
||||
{ "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] },
|
||||
{ "symbols": ["X509Certificate"], "status": "partial", "evidence": ["tests/corpus/1568-x509-fingerprint.ts", "tests/corpus/1572-x509-validity-date-gettime.ts", "tests/corpus/6936-x509-certificate-fields.ts"] },
|
||||
{ "symbols": ["crypto.randomInt"], "status": "partial", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/2890-crypto-static-utility.ts"] },
|
||||
{ "symbols": ["crypto.hkdf", "crypto.hkdfSync", "crypto.scrypt", "crypto.scryptSync"], "status": "partial", "evidence": ["tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/5438-crypto-kdf-digests.ts"] },
|
||||
{ "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] },
|
||||
|
||||
@@ -72,7 +72,9 @@ async function tryFastPath(): Promise<number | null> {
|
||||
values.sparse ||
|
||||
// The full compiler resolves `native` against the build host.
|
||||
values["target-cpu"] !== undefined ||
|
||||
(values["external-types"] ?? []).length > 0
|
||||
(values["external-types"] ?? []).length > 0 ||
|
||||
// The full CLI validates the Node island policy flags.
|
||||
hasNodeIslandFlags(values)
|
||||
)
|
||||
return null;
|
||||
const backend = values.backend;
|
||||
@@ -182,6 +184,21 @@ async function tryFastPath(): Promise<number | null> {
|
||||
});
|
||||
}
|
||||
|
||||
function hasNodeIslandFlags(
|
||||
values: ReturnType<
|
||||
typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }>
|
||||
>["values"],
|
||||
): boolean {
|
||||
return (
|
||||
values["node-islands"] !== undefined ||
|
||||
(values["node-island"] ?? []).length > 0 ||
|
||||
(values.native ?? []).length > 0 ||
|
||||
values.node !== undefined ||
|
||||
values["explain-island"] !== undefined ||
|
||||
values["strict-user-js"]
|
||||
);
|
||||
}
|
||||
|
||||
/** An unchanged program's coverage verdict replays from the verdict cache
|
||||
* without loading the compiler graph. Anything else (a miss, a comment-only
|
||||
* edit, options the replay does not cover) defers to the full CLI. */
|
||||
@@ -212,7 +229,9 @@ async function coverageFastPath(
|
||||
values["provenance-sources"] ||
|
||||
values["emit-ir"] ||
|
||||
(values["npm-static"] ?? []).length > 0 ||
|
||||
(values["external-types"] ?? []).length > 0
|
||||
(values["external-types"] ?? []).length > 0 ||
|
||||
// The replay covers the default Node island policy only.
|
||||
hasNodeIslandFlags(values)
|
||||
)
|
||||
return null;
|
||||
let startup: typeof import("@scriptc/compiler/startup-cache");
|
||||
|
||||
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
minimum_os: "14.0",
|
||||
},
|
||||
program: { object, entry_symbol: "main" },
|
||||
runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" },
|
||||
runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" },
|
||||
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_v43");
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v44");
|
||||
});
|
||||
});
|
||||
|
||||
+100
-8
@@ -3551,17 +3551,45 @@ declare module "crypto" {
|
||||
keylen: number,
|
||||
callback: (error: Error | null, derivedKey: ArrayBuffer) => void,
|
||||
): void;
|
||||
/* The lowered X509Certificate surface is the data-record slice:
|
||||
* fingerprint (the SHA-1 of the DER, uppercase colon-separated) and
|
||||
* the validFrom/validTo validity window (Node's ASN1_TIME_print
|
||||
* strings — "Jul 1 00:00:00 2026 GMT"; the cert-expiry idiom composes
|
||||
* them with new Date(...).getTime()). All compute at construction;
|
||||
* unparseable input throws Node's ERR_OSSL_PEM_NO_START_LINE Error. */
|
||||
/* The lowered X509Certificate surface is the data-record slice: the
|
||||
* fields below, all computed at construction (raw decodes the input,
|
||||
* and the others read raw). validFrom/validTo are Node's
|
||||
* ASN1_TIME_print strings ("Jul 1 00:00:00 2026 GMT"). Unparseable
|
||||
* input throws Node's ERR_OSSL_PEM_NO_START_LINE Error. */
|
||||
export class X509Certificate {
|
||||
constructor(buffer: Uint8Array | string);
|
||||
readonly ca: boolean;
|
||||
readonly fingerprint: string;
|
||||
readonly fingerprint256: string;
|
||||
readonly fingerprint512: string;
|
||||
readonly subject: string;
|
||||
readonly subjectAltName: string | undefined;
|
||||
readonly infoAccess: string | undefined;
|
||||
readonly issuer: string;
|
||||
readonly raw: Buffer;
|
||||
readonly serialNumber: string;
|
||||
readonly signatureAlgorithm: string | undefined;
|
||||
readonly signatureAlgorithmOid: string;
|
||||
readonly validFrom: string;
|
||||
readonly validFromDate: Date;
|
||||
readonly validTo: string;
|
||||
readonly validToDate: Date;
|
||||
/* the certificate's public key; every read answers the same KeyObject */
|
||||
readonly publicKey: KeyObject;
|
||||
verify(publicKey: KeyObject): boolean;
|
||||
checkPrivateKey(privateKey: KeyObject): boolean;
|
||||
checkIssued(otherCert: X509Certificate): boolean;
|
||||
checkHost(name: string, options?: X509CheckOptions): string | undefined;
|
||||
checkEmail(email: string, options?: X509CheckOptions): string | undefined;
|
||||
checkIP(ip: string): string | undefined;
|
||||
toLegacyObject(): import("tls").PeerCertificate;
|
||||
}
|
||||
export interface X509CheckOptions {
|
||||
subject?: "always" | "default" | "never";
|
||||
wildcards?: boolean;
|
||||
partialWildcards?: boolean;
|
||||
multiLabelWildcards?: boolean;
|
||||
singleLabelSubdomains?: boolean;
|
||||
}
|
||||
/* The crypto introspection statics: getFips() answers 0 (a compiled
|
||||
* binary is never a FIPS build), the three name lists bake as fresh
|
||||
@@ -5473,6 +5501,11 @@ declare module "net" {
|
||||
/* The demux route: hand an accepted socket to another server. */
|
||||
emit(event: "connection", socket: Socket): boolean;
|
||||
on(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void;
|
||||
/* tls and https servers: a handshake that failed */
|
||||
on(
|
||||
event: "tlsClientError",
|
||||
listener: (err: Error, tlsSocket: import("tls").TLSSocket) => void,
|
||||
): void;
|
||||
on(event: "close" | "listening", listener: () => void): void;
|
||||
on(event: "drop", listener: (data?: DropArgument) => void): void;
|
||||
on(event: "timeout", listener: (socket: Socket) => void): void;
|
||||
@@ -5492,6 +5525,11 @@ declare module "net" {
|
||||
): void;
|
||||
/* addListener IS on (Node aliases them) — the suite spells both. */
|
||||
addListener(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void;
|
||||
/* tls and https servers: a handshake that failed */
|
||||
addListener(
|
||||
event: "tlsClientError",
|
||||
listener: (err: Error, tlsSocket: import("tls").TLSSocket) => void,
|
||||
): void;
|
||||
addListener(event: "close" | "listening", listener: () => void): void;
|
||||
addListener(event: "drop", listener: (data?: DropArgument) => void): void;
|
||||
addListener(event: "timeout", listener: (socket: Socket) => void): void;
|
||||
@@ -5505,6 +5543,11 @@ declare module "net" {
|
||||
listener: (req: import("http").IncomingMessage, res: import("http").ServerResponse) => void,
|
||||
): void;
|
||||
once(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void;
|
||||
/* tls and https servers: a handshake that failed */
|
||||
once(
|
||||
event: "tlsClientError",
|
||||
listener: (err: Error, tlsSocket: import("tls").TLSSocket) => void,
|
||||
): void;
|
||||
once(event: "close" | "listening", listener: () => void): void;
|
||||
once(event: "drop", listener: (data?: DropArgument) => void): void;
|
||||
once(event: "timeout", listener: (socket: Socket) => void): void;
|
||||
@@ -5958,8 +6001,15 @@ declare module "node:http" {
|
||||
* are PEM strings or Buffers. Handshake failures tear the socket down
|
||||
* without emitting an event, Node's default for 'tlsClientError' with no listener. */
|
||||
declare module "tls" {
|
||||
import { Server, Socket } from "net";
|
||||
export { Server };
|
||||
import { Server as NetServer, Socket } from "net";
|
||||
/** tls.Server: a net server whose connections handshake first. */
|
||||
export interface Server extends NetServer {
|
||||
/* serve `context` to clients whose SNI name matches `hostname` ('*'
|
||||
* matches within one label); the newest match wins */
|
||||
addContext(hostname: string, context: TlsOptions | SecureContext): void;
|
||||
/* new connections serve a context made from `options` */
|
||||
setSecureContext(options: TlsOptions): void;
|
||||
}
|
||||
export interface TlsOptions {
|
||||
/* Optional in the DECLARATION only (real Node programs build these
|
||||
* literals with every TLS knob): the lowering still requires both —
|
||||
@@ -5993,7 +6043,47 @@ declare module "tls" {
|
||||
getCipher(): CipherNameAndProtocol;
|
||||
/* whether the handshake resumed a cached session */
|
||||
isSessionReused(): boolean;
|
||||
/* the peer's certificate: {} before the handshake or without one,
|
||||
* null once the socket is destroyed; detailed adds issuerCertificate */
|
||||
getPeerCertificate(detailed: true): DetailedPeerCertificate;
|
||||
getPeerCertificate(detailed?: false): PeerCertificate;
|
||||
getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate;
|
||||
/* the certificate this socket serves, the same way */
|
||||
getCertificate(): PeerCertificate | object | null;
|
||||
/* a new X509Certificate on each call, or undefined without one */
|
||||
getPeerX509Certificate(): import("crypto").X509Certificate | undefined;
|
||||
getX509Certificate(): import("crypto").X509Certificate | undefined;
|
||||
}
|
||||
/* A certificate as getPeerCertificate() and X509Certificate's
|
||||
* toLegacyObject() answer it: subject and issuer map attribute short
|
||||
* names to values, infoAccess its "<method> - <type>" keys to values. */
|
||||
export interface PeerCertificate {
|
||||
subject: { [attribute: string]: string | string[] };
|
||||
issuer: { [attribute: string]: string | string[] };
|
||||
subjectaltname?: string;
|
||||
infoAccess?: { [key: string]: string[] };
|
||||
ca: boolean;
|
||||
modulus?: string;
|
||||
exponent?: string;
|
||||
pubkey?: Buffer;
|
||||
bits?: number;
|
||||
valid_from: string;
|
||||
valid_to: string;
|
||||
fingerprint: string;
|
||||
fingerprint256: string;
|
||||
fingerprint512: string;
|
||||
ext_key_usage?: string[];
|
||||
serialNumber: string;
|
||||
raw: Buffer;
|
||||
asn1Curve?: string;
|
||||
nistCurve?: string;
|
||||
}
|
||||
/* getPeerCertificate(true): each certificate links to its issuer; a
|
||||
* self-issued root is its own issuerCertificate */
|
||||
export interface DetailedPeerCertificate extends PeerCertificate {
|
||||
issuerCertificate: DetailedPeerCertificate;
|
||||
}
|
||||
|
||||
/** new tls.TLSSocket(socket, options): the server-mode wrap of an open
|
||||
* socket (a server connection or an http CONNECT socket). The options
|
||||
* record reads at runtime; isServer, cert, key, secureContext,
|
||||
@@ -6048,6 +6138,8 @@ declare module "tls" {
|
||||
/** An opaque parsed cert/key pair — what an SNI callback answers with
|
||||
* (the value is a runtime handle; the members are runtime-internal). */
|
||||
export interface SecureContext {}
|
||||
/* @types/node's name for createSecureContext's options */
|
||||
export type SecureContextOptions = TlsOptions;
|
||||
export function createSecureContext(options: TlsOptions): SecureContext;
|
||||
/** The CA-store introspection surface (scr_tls_ca.c): per-type cached
|
||||
* PEM string arrays. The host bundle stands in for Node's compiled-in
|
||||
|
||||
@@ -139,10 +139,12 @@ void scriptc_native_bound_cycle_growth(void) {
|
||||
/* The runtime's nursery trigger (scr_rc.h). */
|
||||
void scr_cyc_set_nursery_threshold(long threshold);
|
||||
|
||||
/* Runs nursery passes every 1024 new candidates instead of every 256. It
|
||||
* has to be in place before the compiler's modules initialize: set from
|
||||
* main it leaves the build's collection work as it is. SCR_CYCLE_THRESHOLD
|
||||
* still overrides it. */
|
||||
/* Runs nursery passes every 1024 new candidates instead of every 256. Much
|
||||
* of a build's cyclic garbage outlives 256 releases: in the shorter window
|
||||
* it survives a nursery pass, is promoted, and only full passes over the
|
||||
* whole live heap find it. A constructor sets it before the compiler's
|
||||
* modules initialize, so every pass of a build uses it.
|
||||
* SCR_CYCLE_THRESHOLD still overrides it. */
|
||||
__attribute__((constructor)) static void scriptc_native_widen_cycle_nursery(void) {
|
||||
scr_cyc_set_nursery_threshold(1024);
|
||||
}
|
||||
|
||||
@@ -64,8 +64,11 @@ static int child_main(int argc, char **argv) {
|
||||
#endif
|
||||
int listening = argc >= 9 && strcmp(argv[5], "--listen") == 0 && strcmp(argv[7], "--idle-exit") == 0;
|
||||
int fixed = listening ? 9 : 5;
|
||||
if (argc < fixed || argc > fixed + 1 || strcmp(argv[1], "--api") != 0 || strcmp(argv[2], "--cwd") != 0 ||
|
||||
strncmp(argv[4], "--callbacks=", 12) != 0 || (argc == fixed + 1 && strcmp(argv[fixed], "--timing") != 0))
|
||||
int at = fixed, timing = 0, libraries = 0;
|
||||
if (at < argc && strcmp(argv[at], "--timing") == 0) { timing = 1; at++; }
|
||||
if (at < argc && strcmp(argv[at], "--default-libraries-from-disk") == 0) { libraries = 1; at++; }
|
||||
if (argc < fixed || at != argc || strcmp(argv[1], "--api") != 0 || strcmp(argv[2], "--cwd") != 0 ||
|
||||
strncmp(argv[4], "--callbacks=", 12) != 0)
|
||||
return 71;
|
||||
const char *mode = argv[3];
|
||||
#ifndef _WIN32
|
||||
@@ -76,7 +79,8 @@ static int child_main(int argc, char **argv) {
|
||||
fputc('\n', stdout);
|
||||
fputs(argv[4], stdout);
|
||||
fputc('\n', stdout);
|
||||
fputs(argc == 6 ? "timing\n" : "plain\n", stdout);
|
||||
fputs(timing ? "timing\n" : "plain\n", stdout);
|
||||
if (libraries) fputs("libraries from disk\n", stdout);
|
||||
return ferror(stdout) ? 72 : 0;
|
||||
}
|
||||
if (strcmp(mode, "exit") == 0) return 0;
|
||||
|
||||
@@ -161,7 +161,7 @@ static wchar_t *command_line(wchar_t **arguments, size_t count) {
|
||||
}
|
||||
|
||||
static int start_process(Ts7Process *p, char **args, size_t argc) {
|
||||
wchar_t *wide[6] = {0};
|
||||
wchar_t *wide[7] = {0};
|
||||
wchar_t *line = NULL;
|
||||
HANDLE child_input = NULL, child_output = NULL, child_error = NULL;
|
||||
LPPROC_THREAD_ATTRIBUTE_LIST attributes = NULL;
|
||||
@@ -330,7 +330,7 @@ static void stop_process(Ts7Process *p) {
|
||||
uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing) {
|
||||
uint8_t timing, uint8_t libraries_from_disk) {
|
||||
if (next_id == 0) { error_text("server handle IDs are exhausted"); return 0; }
|
||||
char *exe = copy_argument(executable, executable_size, "");
|
||||
if (exe == NULL) return 0;
|
||||
@@ -346,8 +346,12 @@ uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size,
|
||||
}
|
||||
exit_registered = 1;
|
||||
}
|
||||
char *args[] = {exe, "--api", "--cwd", directory, callback_flag, timing ? "--timing" : NULL, NULL};
|
||||
int started = start_process(p, args, timing ? 6 : 5);
|
||||
char *args[8] = {exe, "--api", "--cwd", directory, callback_flag};
|
||||
size_t argc = 5;
|
||||
if (timing) args[argc++] = "--timing";
|
||||
if (libraries_from_disk) args[argc++] = "--default-libraries-from-disk";
|
||||
args[argc] = NULL;
|
||||
int started = start_process(p, args, argc);
|
||||
free(exe); free(directory); free(callback_flag);
|
||||
if (!started) { free(p); return 0; }
|
||||
p->id = next_id++;
|
||||
@@ -361,9 +365,10 @@ uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
|
||||
const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing, uint32_t idle_exit, uint32_t memory) {
|
||||
uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit,
|
||||
uint32_t memory) {
|
||||
(void)name; (void)name_size; (void)executable; (void)executable_size; (void)cwd; (void)cwd_size;
|
||||
(void)callbacks; (void)callbacks_size; (void)timing; (void)idle_exit; (void)memory;
|
||||
(void)callbacks; (void)callbacks_size; (void)timing; (void)libraries_from_disk; (void)idle_exit; (void)memory;
|
||||
error_text("a shared checker server needs Unix sockets");
|
||||
return 0;
|
||||
}
|
||||
@@ -455,7 +460,8 @@ uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
|
||||
const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing, uint32_t idle_exit, uint32_t memory) {
|
||||
uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit,
|
||||
uint32_t memory) {
|
||||
if (next_id == 0) { error_text("server handle IDs are exhausted"); return 0; }
|
||||
char *server = copy_argument(name, name_size, "");
|
||||
if (server == NULL) return 0;
|
||||
@@ -491,8 +497,11 @@ uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
|
||||
(void)snprintf(limit, sizeof(limit), "TSC_TS_LISTEN_MEMORY=%lu", (unsigned long)memory);
|
||||
pid_t pid = -1;
|
||||
if (callback_flag != NULL) {
|
||||
char *args[] = {exe, "--api", "--cwd", dir, callback_flag, "--listen", socket_path, "--idle-exit", idle,
|
||||
timing ? "--timing" : NULL, NULL};
|
||||
char *args[12] = {exe, "--api", "--cwd", dir, callback_flag, "--listen", socket_path, "--idle-exit", idle};
|
||||
size_t argc = 9;
|
||||
if (timing) args[argc++] = "--timing";
|
||||
if (libraries_from_disk) args[argc++] = "--default-libraries-from-disk";
|
||||
args[argc] = NULL;
|
||||
pid = start_server(args, limit);
|
||||
}
|
||||
free(exe); free(dir); free(callback_flag);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"ffi_format": 6,
|
||||
"functions": [
|
||||
{ "name": "ts7NativeOpen", "symbol": "scriptc_ts7_open", "params": ["string", "string", "string", "bool"], "returns": "u32" },
|
||||
{ "name": "ts7NativeAttach", "symbol": "scriptc_ts7_attach", "params": ["string", "string", "string", "string", "bool", "u32", "u32"], "returns": "u32" },
|
||||
{ "name": "ts7NativeOpen", "symbol": "scriptc_ts7_open", "params": ["string", "string", "string", "bool", "bool"], "returns": "u32" },
|
||||
{ "name": "ts7NativeAttach", "symbol": "scriptc_ts7_attach", "params": ["string", "string", "string", "string", "bool", "bool", "u32", "u32"], "returns": "u32" },
|
||||
{ "name": "ts7NativeRead", "symbol": "scriptc_ts7_read", "params": ["u32", "mutable-bytes", "u32", "u32"], "returns": "i32" },
|
||||
{ "name": "ts7NativeWrite", "symbol": "scriptc_ts7_write", "params": ["u32", "bytes", "u32", "u32"], "returns": "i32" },
|
||||
{ "name": "ts7NativePid", "symbol": "scriptc_ts7_pid", "params": ["u32"], "returns": "u32" },
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
uint32_t scriptc_ts7_open(const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing);
|
||||
uint8_t timing, uint8_t libraries_from_disk);
|
||||
/* Connects to the shared checker server `name` (`--api --listen`) in this
|
||||
* user's private server directory, starting it in its own process group
|
||||
* with `--idle-exit idle_exit` and a limit of `memory` megabytes
|
||||
@@ -25,7 +25,8 @@ uint32_t scriptc_ts7_attach(const uint8_t *name, size_t name_size,
|
||||
const uint8_t *executable, size_t executable_size,
|
||||
const uint8_t *cwd, size_t cwd_size,
|
||||
const uint8_t *callbacks, size_t callbacks_size,
|
||||
uint8_t timing, uint32_t idle_exit, uint32_t memory);
|
||||
uint8_t timing, uint8_t libraries_from_disk, uint32_t idle_exit,
|
||||
uint32_t memory);
|
||||
int32_t scriptc_ts7_read(uint32_t handle, uint8_t *bytes, size_t size,
|
||||
uint32_t offset, uint32_t length);
|
||||
int32_t scriptc_ts7_write(uint32_t handle, const uint8_t *bytes, size_t size,
|
||||
|
||||
@@ -21,12 +21,14 @@
|
||||
static const char *executable;
|
||||
static const char *callbacks = "readFile,fileExists";
|
||||
|
||||
static uint32_t start(const char *mode, int timing) {
|
||||
static uint32_t start_with(const char *mode, int timing, int libraries) {
|
||||
return scriptc_ts7_open((const uint8_t *)executable, strlen(executable),
|
||||
(const uint8_t *)mode, strlen(mode),
|
||||
(const uint8_t *)callbacks, strlen(callbacks), (uint8_t)timing);
|
||||
(const uint8_t *)callbacks, strlen(callbacks), (uint8_t)timing, (uint8_t)libraries);
|
||||
}
|
||||
|
||||
static uint32_t start(const char *mode, int timing) { return start_with(mode, timing, 0); }
|
||||
|
||||
static void fail_message(void) {
|
||||
uint8_t message[1024];
|
||||
int32_t size = scriptc_ts7_error(message, sizeof(message));
|
||||
@@ -83,11 +85,13 @@ static void argument_cases(void) {
|
||||
"", "a b", "double\"quote", "end\\", "many\\\\\"quotes\\\\", "a;&$()`'b", "h\xc3\xa9llo \xf0\x9f\x8c\x8d",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
|
||||
for (int timing = 0; timing <= 1; timing++) {
|
||||
for (int flags = 0; flags < 4; flags++) {
|
||||
int timing = flags & 1, libraries = flags >> 1;
|
||||
char mode[256], expected[512];
|
||||
snprintf(mode, sizeof(mode), "argv:%s", values[i]);
|
||||
snprintf(expected, sizeof(expected), "%s\n--callbacks=%s\n%s\n", values[i], callbacks, timing ? "timing" : "plain");
|
||||
uint32_t id = start(mode, timing);
|
||||
snprintf(expected, sizeof(expected), "%s\n--callbacks=%s\n%s\n%s", values[i], callbacks,
|
||||
timing ? "timing" : "plain", libraries ? "libraries from disk\n" : "");
|
||||
uint32_t id = start_with(mode, timing, libraries);
|
||||
assert(id != 0);
|
||||
uint8_t actual[512];
|
||||
read_exact(id, actual, strlen(expected));
|
||||
@@ -147,10 +151,10 @@ static void failure_cases(void) {
|
||||
executable = "/scriptc/does-not-exist/tsgo";
|
||||
for (int i = 0; i < 30; i++) { assert(start("echo", 0) == 0); fail_message(); }
|
||||
executable = saved;
|
||||
assert(scriptc_ts7_open(NULL, 0, bytes, 1, bytes, 1, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)"abc\0def", 7, bytes, 1, bytes, 1, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), NULL, 0, bytes, 1, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), bytes, 1, NULL, 0, 0) == 0);
|
||||
assert(scriptc_ts7_open(NULL, 0, bytes, 1, bytes, 1, 0, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)"abc\0def", 7, bytes, 1, bytes, 1, 0, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), NULL, 0, bytes, 1, 0, 0) == 0);
|
||||
assert(scriptc_ts7_open((const uint8_t *)executable, strlen(executable), bytes, 1, NULL, 0, 0, 0) == 0);
|
||||
for (int i = 0; i < 30; i++) {
|
||||
uint32_t id = start("exit", 0);
|
||||
assert(id != 0);
|
||||
@@ -177,7 +181,7 @@ static void failure_cases(void) {
|
||||
static uint32_t attach(const char *name, const char *mode, uint32_t memory) {
|
||||
return scriptc_ts7_attach((const uint8_t *)name, strlen(name), (const uint8_t *)executable, strlen(executable),
|
||||
(const uint8_t *)mode, strlen(mode), (const uint8_t *)callbacks, strlen(callbacks),
|
||||
0, 1, memory);
|
||||
0, 0, 1, memory);
|
||||
}
|
||||
|
||||
static void expect_line(uint32_t id, const char *expected) {
|
||||
|
||||
@@ -63,6 +63,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
dh: features.dh,
|
||||
pki: features.pki,
|
||||
ed25519: features.ed25519,
|
||||
x509: features.x509,
|
||||
webcrypto: features.webcrypto,
|
||||
perfLoop: features.perfLoop,
|
||||
};
|
||||
|
||||
@@ -64,6 +64,7 @@ export async function compilePackedLibrary(
|
||||
dh: false,
|
||||
pki: false,
|
||||
ed25519: false,
|
||||
x509: options.x509 ?? false,
|
||||
webcrypto: false,
|
||||
perfLoop: false,
|
||||
tlsCa: false,
|
||||
@@ -152,7 +153,7 @@ export async function compilePackedLibrary(
|
||||
...(await Promise.all(
|
||||
shards
|
||||
.slice(start, start + 4)
|
||||
.map((shard, offset) => emit(shard.source, `shard-${start + offset}.o`)),
|
||||
.map((shard, offset) => emit(shard.source(), `shard-${start + offset}.o`)),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* one directly or through a binding read from it. The choice never changes
|
||||
* behavior. */
|
||||
|
||||
import type { IrExpr, IrModule } from "../../ir/ir.js";
|
||||
import type { IrExpr, IrModule, IrStmt } from "../../ir/ir.js";
|
||||
import { eachNodeOfKinds, type IrNodeIndex, type IrNodeKind } from "../../ir/node-index.js";
|
||||
|
||||
const APPENDING_METHODS: ReadonlySet<string> = new Set([
|
||||
@@ -120,3 +120,36 @@ export function findAppendedArrayLiterals(
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** `const xs = []` directly followed by a capacity hint for `xs` (an
|
||||
* `arraySetLength` with `reserve`) whose length reads only literals and
|
||||
* other locals: the literal can be constructed with that capacity, and the
|
||||
* hint then has nothing left to do. */
|
||||
export function reservedArrayLength(
|
||||
stmts: readonly IrStmt[],
|
||||
i: number,
|
||||
): { literal: IrExpr; hint: IrStmt & { kind: "arraySetLength" } } | null {
|
||||
const decl = stmts[i];
|
||||
const next = stmts[i + 1];
|
||||
if (decl?.kind !== "varDecl" || decl.init === null || !isEmptyLiteral(decl.init)) return null;
|
||||
if (next?.kind !== "arraySetLength" || next.reserve !== true) return null;
|
||||
if (next.arr.kind !== "varRef" || next.arr.localId !== decl.localId) return null;
|
||||
return plainLength(next.length, decl.localId) ? { literal: decl.init, hint: next } : null;
|
||||
}
|
||||
|
||||
function plainLength(e: IrExpr, array: string): boolean {
|
||||
switch (e.kind) {
|
||||
case "numLit":
|
||||
return true;
|
||||
case "varRef":
|
||||
return e.localId !== array;
|
||||
case "bin":
|
||||
return plainLength(e.left, array) && plainLength(e.right, array);
|
||||
case "ternary":
|
||||
return (
|
||||
plainLength(e.cond, array) && plainLength(e.then, array) && plainLength(e.else_, array)
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,13 +317,19 @@ test("worker programs test each receiver's shared storage once in the guard", ()
|
||||
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
|
||||
const guard = body.slice(0, body.search(/^bytes\.version\.(fast|slow)\d+:$/m));
|
||||
expect(guard.match(/icmp eq ptr %\S+, null/g)).toHaveLength(2);
|
||||
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0];
|
||||
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.fast\.shared\d+:$/m.exec(body)![0];
|
||||
expect(fast).not.toContain("bytes.shared");
|
||||
expect(fast).not.toMatch(/(load|store) atomic double/);
|
||||
// The checked copy keeps the shared-memory path for each access.
|
||||
// The checked copy keeps the shared-memory path for each access, and so
|
||||
// does the copy for shared views whose bounds hold.
|
||||
const slow = body.slice(body.search(/^bytes\.version\.slow\d+:$/m), body.indexOf(fast));
|
||||
expect(slow).toContain("load atomic double");
|
||||
expect(slow).toContain("store atomic double");
|
||||
const shared = /^bytes\.version\.fast\.shared\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(
|
||||
body,
|
||||
)![0];
|
||||
expect(shared).toContain("load atomic double");
|
||||
expect(shared).toContain("store atomic double");
|
||||
});
|
||||
|
||||
test("index temps of a version's counter are integers in the fast copy", () => {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "./counted-loops.js";
|
||||
import { emitBytesData, emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import {
|
||||
emitStringAsciiFlag,
|
||||
emitStringByteLength,
|
||||
@@ -85,8 +86,15 @@ export interface BytesLoopVersion {
|
||||
/** 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. */
|
||||
/** Receivers the guard requires to have no shared storage for the fast
|
||||
* copy. */
|
||||
ownedReceivers: number[];
|
||||
/** Worker programs: when the bounds hold but a receiver in
|
||||
* `ownedReceivers` has shared storage, the loop runs a second fast copy
|
||||
* instead of the checked one. It skips the bounds checks of `bounds` and
|
||||
* keeps shared-safe accesses (emitBytesLoopVersionGuard's `sharedFast`). Set
|
||||
* when the version proves bounds as well as ownership. */
|
||||
sharedCopy: boolean;
|
||||
/** The string reads among `bounds`. */
|
||||
stringReads: ReadonlySet<Node>;
|
||||
/** The version's one string receiver, when the loop may also run over
|
||||
@@ -752,6 +760,7 @@ export function matchBytesLoopVersion(
|
||||
owned: ownedNodes,
|
||||
receivers: order.map((index) => receivers[index]!),
|
||||
ownedReceivers: ownedKept.map(([receiver]) => renumber.get(receiver)!),
|
||||
sharedCopy: ownedKept.length > 0 && bounds.size > 0,
|
||||
stringReads,
|
||||
unitsReceiver,
|
||||
unitLengths: unitLengths ?? new Set<IrExpr>(),
|
||||
@@ -1102,6 +1111,10 @@ function findWhileCounterAccesses(
|
||||
* storage, and to `slow` otherwise (including a counted loop whose
|
||||
* integer form is not established, or a module receiver not yet set).
|
||||
*
|
||||
* With `sharedFast` (a version with `sharedCopy`), the ownership tests are
|
||||
* separate: when everything else holds but a view has shared storage, the
|
||||
* guard branches to `sharedFast` instead of `slow`.
|
||||
*
|
||||
* With `unitsFast`, the version's units receiver may instead be a string
|
||||
* that is not ASCII: the guard has the runtime publish it as the scan
|
||||
* receiver (one read), tests the same accesses against its UTF-16 length,
|
||||
@@ -1115,6 +1128,7 @@ export function emitBytesLoopVersionGuard(
|
||||
fast: string,
|
||||
slow: string,
|
||||
unitsFast?: string,
|
||||
sharedFast?: string,
|
||||
): { units: string | null; length: string | null } | null {
|
||||
const B = host.B;
|
||||
const next = (cond: string): void => {
|
||||
@@ -1146,7 +1160,7 @@ export function emitBytesLoopVersionGuard(
|
||||
};
|
||||
const lengths = receivers.map((receiver, index) => {
|
||||
if (index === unitsIndex) {
|
||||
const read = emitUnitsLength(host, receiver);
|
||||
const read = emitUnitsLength(host, receiver, () => emitUnitsSpan(host, version, index));
|
||||
units.read = read;
|
||||
return read.length;
|
||||
}
|
||||
@@ -1165,10 +1179,13 @@ export function emitBytesLoopVersionGuard(
|
||||
if (string && index !== unitsIndex)
|
||||
conditions.push(emitStringAsciiFlag(host, receivers[index]!));
|
||||
});
|
||||
// Without `sharedFast`, shared storage sends the loop to the checked copy
|
||||
// with the other failed tests; with it, only to the shared fast copy.
|
||||
const ownership: string[] = [];
|
||||
for (const index of version.ownedReceivers) {
|
||||
const owned = B.tmp();
|
||||
B.line(`${owned} = icmp eq ptr ${emitBytesSharedStorage(host, receivers[index]!)}, null`);
|
||||
conditions.push(owned);
|
||||
(sharedFast !== undefined ? ownership : conditions).push(owned);
|
||||
}
|
||||
for (const check of version.fixed) {
|
||||
const ok = B.tmp();
|
||||
@@ -1351,7 +1368,22 @@ export function emitBytesLoopVersionGuard(
|
||||
const length = version.unitsReceiver !== null ? lengths[version.unitsReceiver]! : null;
|
||||
const taken = units.read;
|
||||
const target = taken ? B.newLabel("bytes.version.units") : fast;
|
||||
if (valid === "true") B.br(target);
|
||||
if (sharedFast !== undefined && taken)
|
||||
throw new InternalCompilerError("llvm emitter bug: a shared fast copy with a units copy");
|
||||
if (sharedFast !== undefined && ownership.length > 0) {
|
||||
// The bounds hold: unshared views take the owned copy.
|
||||
const split = B.newLabel("bytes.version.owned");
|
||||
let owned = ownership[0]!;
|
||||
for (const cond of ownership.slice(1)) {
|
||||
const all = B.tmp();
|
||||
B.line(`${all} = and i1 ${owned}, ${cond}`);
|
||||
owned = all;
|
||||
}
|
||||
if (valid === "true") B.br(split);
|
||||
else B.condBr(valid, split, slow);
|
||||
B.startBlock(split);
|
||||
B.condBr(owned, target, sharedFast);
|
||||
} else if (valid === "true") B.br(target);
|
||||
else B.condBr(valid, target, slow);
|
||||
if (!taken) return { units: null, length };
|
||||
B.startBlock(target);
|
||||
@@ -1359,15 +1391,49 @@ export function emitBytesLoopVersionGuard(
|
||||
return { units: taken.pointer, length };
|
||||
}
|
||||
|
||||
/** The range of indexes a version's loop reads from its units receiver, as
|
||||
* two doubles: a counted loop's start and limit, or a while loop's one
|
||||
* counter and the receiver's bound. Both NaN when the version gives no such
|
||||
* range. The runtime converts the receiver at once only when the range is
|
||||
* long enough to pay for it (scr_str_scan_units_for). */
|
||||
function emitUnitsSpan(
|
||||
host: LlvmEmitterContext,
|
||||
version: BytesLoopVersion,
|
||||
receiver: number,
|
||||
): { from: string; to: string } {
|
||||
// Locals and literals only: they read no string (a `length` of the
|
||||
// receiver would measure it). Anything else leaves the range unbounded.
|
||||
const number = (expr: IrExpr): string | null => {
|
||||
if (expr.kind !== "varRef" && expr.kind !== "numLit") return null;
|
||||
const value = host.emitExpr(expr);
|
||||
return value.type.kind === "f64" ? value.name : null;
|
||||
};
|
||||
const unbounded = { from: "0x7FF8000000000000", to: "0x7FF8000000000000" };
|
||||
const counted = version.counted;
|
||||
let from: IrExpr, to: IrExpr;
|
||||
if (counted) [from, to] = [counted.start, counted.limit];
|
||||
else {
|
||||
const bounded = version.bounded.filter((check) => check.receiver === receiver);
|
||||
if (version.counters.length !== 1 || bounded.length !== 1 || version.paired !== null)
|
||||
return unbounded;
|
||||
[from, to] = [version.counters[0]!.ref, bounded[0]!.bound];
|
||||
}
|
||||
const start = number(from),
|
||||
limit = number(to);
|
||||
return start !== null && limit !== null ? { from: start, to: limit } : unbounded;
|
||||
}
|
||||
|
||||
/** The length of a version's units receiver `s` in UTF-16 units: its byte
|
||||
* length when it is ASCII; otherwise, once it is the scan receiver, the
|
||||
* published unit count (0 when the runtime does not hold its units). When
|
||||
* `s` is not already the scan receiver, the runtime converts it to units
|
||||
* and publishes it (scr_str_scan_units). Also the units pointer, whether
|
||||
* the loop is to read it, and whether the published receiver is `s`. */
|
||||
* and publishes it if the loop's `span` makes that pay
|
||||
* (scr_str_scan_units_for). Also the units pointer, whether the loop is to
|
||||
* read it, and whether the published receiver is `s`. */
|
||||
function emitUnitsLength(
|
||||
host: LlvmEmitterContext,
|
||||
s: string,
|
||||
span: () => { from: string; to: string },
|
||||
): { length: string; pointer: string; taken: string; published: string } {
|
||||
const B = host.B;
|
||||
const sz = host.sizeType;
|
||||
@@ -1381,14 +1447,15 @@ function emitUnitsLength(
|
||||
B.startBlock(publish);
|
||||
// When `s` is already the scan receiver (an earlier loop over it published
|
||||
// it), its units stay published until the runtime clears the scan: skip
|
||||
// the call.
|
||||
// the call. Loops re-entered over one string take that path; the weights
|
||||
// keep the call and its reach out of the loop's code.
|
||||
const held = B.tmp(),
|
||||
holds = B.tmp();
|
||||
B.line(`${held} = load ptr, ptr @scr_str_scan`);
|
||||
B.line(`${holds} = icmp eq ptr ${held}, ${s}`);
|
||||
const call = B.newLabel("bytes.version.publish.units"),
|
||||
load = B.newLabel("bytes.version.publish.load");
|
||||
B.condBr(holds, load, call);
|
||||
B.condBr(holds, load, call, `, !prof !{!"branch_weights", i32 1000, i32 1}`);
|
||||
B.startBlock(call);
|
||||
host.declare(STRING_SCAN_UNITS_DECL);
|
||||
const receiver = B.tmp(),
|
||||
@@ -1398,7 +1465,8 @@ function emitUnitsLength(
|
||||
countSlot = B.tmp(),
|
||||
count = B.tmp(),
|
||||
published = B.tmp();
|
||||
B.line(`call void @scr_str_scan_units(ptr ${s})`);
|
||||
const { from: first, to: bound } = span();
|
||||
B.line(`call void @scr_str_scan_units_for(ptr ${s}, double ${first}, double ${bound})`);
|
||||
B.br(load);
|
||||
B.startBlock(load);
|
||||
B.line(`${receiver} = load ptr, ptr @scr_str_scan`);
|
||||
|
||||
@@ -13,9 +13,37 @@ import {
|
||||
import { SourceLocations } from "../source-locations.js";
|
||||
import { mangleFunction } from "../mangle.js";
|
||||
|
||||
/** DWARF describes source bindings at their actual native storage locations. */
|
||||
/** A request a range emitter made of its debug info that added nodes
|
||||
* (LlvmDebugInfo.record), with the ids it answered. */
|
||||
export type DebugRequest =
|
||||
| { readonly op: "function"; readonly fn: string; readonly result: string }
|
||||
| {
|
||||
readonly op: "local";
|
||||
readonly local: IrLocal;
|
||||
readonly fn: string;
|
||||
readonly arg: number;
|
||||
readonly captured: boolean;
|
||||
readonly result: { variable: string; location: string };
|
||||
}
|
||||
| {
|
||||
readonly op: "location";
|
||||
readonly loc: SrcLoc;
|
||||
readonly fn: string;
|
||||
readonly result: string;
|
||||
};
|
||||
|
||||
/** DWARF describes source bindings at their actual native storage locations.
|
||||
*
|
||||
* Nodes are numbered in request order. A range emitter (emit-module.ts)
|
||||
* records the requests that added nodes; the emitter that merges the range
|
||||
* replays them in order, so every node gets the number serial emission
|
||||
* gives it, and renames the range's text from the ids the range answered to
|
||||
* its own. A request that added no node answered an id that existed before
|
||||
* the range started or that an earlier recorded request created. */
|
||||
export class LlvmDebugInfo {
|
||||
private readonly nodes: string[] = [];
|
||||
/** The requests that added nodes, while recording (record). */
|
||||
private requests: DebugRequest[] | null = null;
|
||||
private readonly files = new Map<string, string>();
|
||||
private readonly locations = new Map<string, string>();
|
||||
private readonly scopes = new Map<string, string>();
|
||||
@@ -73,7 +101,53 @@ export class LlvmDebugInfo {
|
||||
return unit;
|
||||
}
|
||||
|
||||
/** Records the requests that add nodes from here on. */
|
||||
record(): void {
|
||||
this.requests = [];
|
||||
}
|
||||
|
||||
/** The requests recorded since record. */
|
||||
recorded(): readonly DebugRequest[] {
|
||||
return this.requests ?? [];
|
||||
}
|
||||
|
||||
/** Replays a range's recorded requests in order and adds to `ids` the
|
||||
* rename from the ids the range answered to this debug info's. */
|
||||
replay(
|
||||
requests: readonly DebugRequest[],
|
||||
functionNamed: (name: string) => IrFunction,
|
||||
ids: Map<string, string>,
|
||||
): void {
|
||||
const own = (id: string, to: string | null | undefined): void => {
|
||||
if (to === null || to === undefined)
|
||||
throw new Error(`llvm emitter bug: debug request for ${id} added no node on replay`);
|
||||
if (to !== id) ids.set(id, to);
|
||||
};
|
||||
for (const request of requests) {
|
||||
if (request.op === "function") {
|
||||
own(request.result, this.function(functionNamed(request.fn)));
|
||||
continue;
|
||||
}
|
||||
const fn = ids.get(request.fn) ?? request.fn;
|
||||
if (request.op === "location") {
|
||||
own(request.result, this.location(request.loc, fn));
|
||||
continue;
|
||||
}
|
||||
const local = this.local(request.local, fn, request.arg, request.captured);
|
||||
own(request.result.variable, local?.variable);
|
||||
own(request.result.location, local?.location);
|
||||
}
|
||||
}
|
||||
|
||||
function(fn: IrFunction): string | null {
|
||||
const before = this.nodes.length;
|
||||
const id = this.functionNode(fn);
|
||||
if (this.requests !== null && id !== null && this.nodes.length > before)
|
||||
this.requests.push({ op: "function", fn: fn.name, result: id });
|
||||
return id;
|
||||
}
|
||||
|
||||
private functionNode(fn: IrFunction): string | null {
|
||||
const pos = this.source.position(fn.loc);
|
||||
if (pos === null) return null;
|
||||
const file = this.file(pos.file);
|
||||
@@ -230,6 +304,26 @@ export class LlvmDebugInfo {
|
||||
fn: string | null,
|
||||
arg = 0,
|
||||
captured = false,
|
||||
): { variable: string; expression: string; location: string } | null {
|
||||
const before = this.nodes.length;
|
||||
const result = this.localNodes(local, fn, arg, captured);
|
||||
if (this.requests !== null && fn !== null && result !== null && this.nodes.length > before)
|
||||
this.requests.push({
|
||||
op: "local",
|
||||
local,
|
||||
fn,
|
||||
arg,
|
||||
captured,
|
||||
result: { variable: result.variable, location: result.location },
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private localNodes(
|
||||
local: IrLocal,
|
||||
fn: string | null,
|
||||
arg: number,
|
||||
captured: boolean,
|
||||
): { variable: string; expression: string; location: string } | null {
|
||||
if (fn === null || local.source === undefined) return null;
|
||||
const pos = this.source.position(local.source.loc);
|
||||
@@ -279,6 +373,14 @@ export class LlvmDebugInfo {
|
||||
}
|
||||
|
||||
location(loc: SrcLoc, fn: string | null): string | null {
|
||||
const before = this.nodes.length;
|
||||
const id = this.locationNode(loc, fn);
|
||||
if (this.requests !== null && fn !== null && id !== null && this.nodes.length > before)
|
||||
this.requests.push({ op: "location", loc, fn, result: id });
|
||||
return id;
|
||||
}
|
||||
|
||||
private locationNode(loc: SrcLoc, fn: string | null): string | null {
|
||||
if (fn === null) return null;
|
||||
const pos = this.source.position(loc);
|
||||
if (pos === null) return null;
|
||||
|
||||
@@ -66,6 +66,25 @@ import { recordMayKeepSource, recordSourceFieldOrder } from "./record-sources.js
|
||||
import type { WalkerHost } from "./walkers.js";
|
||||
import type { LlStreamTypedRefAdapter } from "./expr-context.js";
|
||||
|
||||
/** typeKey with `?` after every `(T | undefined)[]` (optionalElements),
|
||||
* at any depth of arrays, Maps, Sets and promises: checked conversions to
|
||||
* `number[][]` and `(number | undefined)[][]` need separate helpers, though
|
||||
* the two types share a typeKey. */
|
||||
function checkedConversionKey(t: IrType): string {
|
||||
switch (t.kind) {
|
||||
case "array":
|
||||
return `array<${checkedConversionKey(t.elem)}>${t.optionalElements ? "?" : ""}`;
|
||||
case "map":
|
||||
return `map<${checkedConversionKey(t.key)},${checkedConversionKey(t.value)}>`;
|
||||
case "set":
|
||||
return `set<${checkedConversionKey(t.elem)}>`;
|
||||
case "promise":
|
||||
return `promise<${checkedConversionKey(t.inner)}>`;
|
||||
default:
|
||||
return typeKey(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether to-dyn of a record shape keeps the record itself: an empty shape
|
||||
* and an event-listener record (a `handleEvent` function field) box their
|
||||
* identity. Every other shape, tuples included, copies its fields into a
|
||||
@@ -153,6 +172,18 @@ export const DYN_KIND = {
|
||||
SYMBOL: 15,
|
||||
} as const;
|
||||
|
||||
/** Union arm kinds whose matchers reject every plain dyn object
|
||||
* (trustedArmByLiteral). */
|
||||
const NON_OBJECT_ARM_KINDS: ReadonlySet<IrType["kind"]> = new Set<IrType["kind"]>([
|
||||
"f64",
|
||||
"string",
|
||||
"bool",
|
||||
"nullT",
|
||||
"undefinedT",
|
||||
"bigint",
|
||||
"symbol",
|
||||
]);
|
||||
|
||||
/** What the dyn helpers need beyond the walker host: interned immortal
|
||||
* unit instances (undefined-armed dynCheck targets build them). */
|
||||
export interface DynHost extends WalkerHost {
|
||||
@@ -394,6 +425,51 @@ export class LlDyn {
|
||||
B.line(`call void @scr_dyn_build_add(ptr %d, ptr @${name}, ptr ${value})`);
|
||||
}
|
||||
|
||||
/** Whether a builder of `def` may pick a plain object's arm by its literal
|
||||
* discriminator alone inside a trusted build (scr_dyn_trust_enter): the
|
||||
* arms' literals are disjoint, and every arm without a literal has a kind
|
||||
* no plain object matches. The value then matched exactly one arm with a
|
||||
* literal, the arm whose literal it carries. */
|
||||
private trustedArmByLiteral(def: IrUnionDef): boolean {
|
||||
const discriminant = def.discriminant;
|
||||
if (!discriminant) return false;
|
||||
const seen = new Set<string>();
|
||||
for (const { values } of discriminant.cases) {
|
||||
for (const value of values) {
|
||||
const key = `${typeof value}:${String(value)}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
}
|
||||
}
|
||||
return def.arms.every(
|
||||
(arm, tag) =>
|
||||
discriminant.cases.some((candidate) => candidate.tag === tag) ||
|
||||
NON_OBJECT_ARM_KINDS.has(arm.kind),
|
||||
);
|
||||
}
|
||||
|
||||
/** Per arm of `def`, the dyn kind of the values a builder may convert into
|
||||
* that arm without matching its structure inside a trusted build, or null.
|
||||
* A value there already matched one of the union's arms, so the arm is
|
||||
* known when it is the only arm that takes values of its kind: the arm
|
||||
* whose literal a plain object carries (trustedArmByLiteral), or the one
|
||||
* arm a plain object or an array can match at all. */
|
||||
private trustedArmKinds(def: IrUnionDef): (number | null)[] {
|
||||
const byLiteral = this.trustedArmByLiteral(def);
|
||||
const objectArms = def.arms.flatMap((arm, tag) =>
|
||||
NON_OBJECT_ARM_KINDS.has(arm.kind) ? [] : [tag],
|
||||
);
|
||||
return def.arms.map((arm, tag) => {
|
||||
if (def.discriminant?.cases.some((candidate) => candidate.tag === tag))
|
||||
return byLiteral ? DYN_KIND.OBJ : null;
|
||||
if (objectArms.length !== 1 || objectArms[0] !== tag) return null;
|
||||
if (arm.kind === "array") return DYN_KIND.ARR;
|
||||
if (arm.kind === "record" && this.host.recordsById.get(arm.shapeId)?.tuple !== true)
|
||||
return DYN_KIND.OBJ;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/** 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
|
||||
@@ -461,6 +537,7 @@ export class LlDyn {
|
||||
none: string,
|
||||
preserveRefs: boolean,
|
||||
table: string,
|
||||
trustedKinds: readonly (number | null)[] = def.arms.map(() => null),
|
||||
): void {
|
||||
const discriminant = def.discriminant!;
|
||||
const literalsOf = (tag: number): readonly (string | number | boolean)[] | null =>
|
||||
@@ -500,7 +577,7 @@ export class LlDyn {
|
||||
positions.forEach((position, index) => {
|
||||
const tag = order[position]!;
|
||||
const failed = B.newLabel("du.c.f");
|
||||
this.unionArmStructure(B, def, tag, hits[tag]!, failed, preserveRefs);
|
||||
this.unionArmStructure(B, def, tag, hits[tag]!, failed, preserveRefs, trustedKinds[tag]!);
|
||||
B.startBlock(failed);
|
||||
const next = index === positions.length - 1 ? none : B.newLabel("du.c.n");
|
||||
const same = B.tmp();
|
||||
@@ -523,12 +600,23 @@ export class LlDyn {
|
||||
tryCase(-1);
|
||||
order.forEach((tag, position) => {
|
||||
B.startBlock(from[position]!);
|
||||
this.unionArmMatch(B, def, tag, hits[tag]!, after(position), preserveRefs);
|
||||
this.unionArmMatch(
|
||||
B,
|
||||
def,
|
||||
tag,
|
||||
hits[tag]!,
|
||||
after(position),
|
||||
preserveRefs,
|
||||
trustedKinds[tag]!,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Branch to a variant only after its literal discriminator and complete
|
||||
* structure match. Use the same path for predicates and builders. */
|
||||
* structure match. Use the same path for predicates and builders. With a
|
||||
* `trustedKind` (builders, trustedArmKinds), a value of that dyn kind
|
||||
* inside a trusted build skips the structure match once its literal
|
||||
* matched. */
|
||||
private unionArmMatch(
|
||||
B: BlockBuilder,
|
||||
def: IrUnionDef,
|
||||
@@ -536,6 +624,7 @@ export class LlDyn {
|
||||
yes: string,
|
||||
no: string,
|
||||
preserveRefs = false,
|
||||
trustedKind: number | null = null,
|
||||
): void {
|
||||
const guard = def.discriminant?.cases.find((candidate) => candidate.tag === tag);
|
||||
if (guard && def.discriminant) {
|
||||
@@ -564,10 +653,12 @@ export class LlDyn {
|
||||
});
|
||||
B.startBlock(structure);
|
||||
}
|
||||
this.unionArmStructure(B, def, tag, yes, no, preserveRefs);
|
||||
this.unionArmStructure(B, def, tag, yes, no, preserveRefs, trustedKind);
|
||||
}
|
||||
|
||||
/** The part of unionArmMatch past the arm's literal test. */
|
||||
/** The part of unionArmMatch past the arm's literal test. With a
|
||||
* `trustedKind`, a value of that dyn kind inside a trusted build skips the
|
||||
* structure match. */
|
||||
private unionArmStructure(
|
||||
B: BlockBuilder,
|
||||
def: IrUnionDef,
|
||||
@@ -575,7 +666,21 @@ export class LlDyn {
|
||||
yes: string,
|
||||
no: string,
|
||||
preserveRefs: boolean,
|
||||
trustedKind: number | null = null,
|
||||
): void {
|
||||
if (trustedKind !== null) {
|
||||
this.host.declare(`declare zeroext i1 @scr_dyn_trusted_build()`);
|
||||
const kind = this.kindOf(B, "%d");
|
||||
const sameKind = B.tmp();
|
||||
const trusted = B.tmp();
|
||||
const skip = B.tmp();
|
||||
B.line(`${sameKind} = icmp eq i32 ${kind}, ${trustedKind}`);
|
||||
B.line(`${trusted} = call zeroext i1 @scr_dyn_trusted_build()`);
|
||||
B.line(`${skip} = and i1 ${sameKind}, ${trusted}`);
|
||||
const full = B.newLabel("du.full");
|
||||
B.condBr(skip, yes, full);
|
||||
B.startBlock(full);
|
||||
}
|
||||
const matches = B.tmp();
|
||||
B.line(
|
||||
`${matches} = call zeroext i1 @${this.dynMatchHelper(def.arms[tag]!, preserveRefs)}(ptr %d)`,
|
||||
@@ -673,6 +778,13 @@ export class LlDyn {
|
||||
/* ── dyn node plumbing ─────────────────────────────────────────────── */
|
||||
|
||||
/** Loads a dyn node's kind tag (after the size_t rc word). */
|
||||
/** i1: the checked-dynamic value `d` is undefined. */
|
||||
isUndefined(B: BlockBuilder, d: string): string {
|
||||
const is = B.tmp();
|
||||
B.line(`${is} = icmp eq i32 ${this.kindOf(B, d)}, ${DYN_KIND.UNDEF}`);
|
||||
return is;
|
||||
}
|
||||
|
||||
private kindOf(B: BlockBuilder, d: string): string {
|
||||
const p = B.tmp();
|
||||
const k = B.tmp();
|
||||
@@ -1059,7 +1171,7 @@ export class LlDyn {
|
||||
|
||||
/** `sc_dm_<n>(ptr d) -> i1` — does this dyn fit T? Never throws. */
|
||||
dynMatchHelper(t: IrType, preserveRefs = false): string {
|
||||
const key = typeKey(t);
|
||||
const key = checkedConversionKey(t);
|
||||
const matcherKey = preserveRefs ? `identity:${key}` : key;
|
||||
const existing = this.dynMatchers.get(matcherKey);
|
||||
if (existing) return existing;
|
||||
@@ -1374,7 +1486,7 @@ export class LlDyn {
|
||||
B.startBlock(l0);
|
||||
const n = this.lenOf(B, "%d");
|
||||
const items = this.itemsOf(B, "%d");
|
||||
this.i64Loop(B, "dm.el", n, (i, brNext) => {
|
||||
this.i64Loop(B, "dm.el", n, (i, brNext, next) => {
|
||||
this.host.declare(`declare zeroext i1 @scr_dyn_arr_has_index(ptr, ${this.S})`);
|
||||
const present = B.tmp();
|
||||
B.line(`${present} = call zeroext i1 @scr_dyn_arr_has_index(ptr %d, ${this.S} ${i})`);
|
||||
@@ -1385,6 +1497,15 @@ export class LlDyn {
|
||||
brNext();
|
||||
B.startBlock(valueLabel);
|
||||
const e = this.itemAt(B, items, i);
|
||||
if (t.optionalElements) {
|
||||
// A `(T | undefined)[]` stored as T elements takes undefined
|
||||
// elements as well.
|
||||
const undef = B.tmp();
|
||||
const checkLabel = B.newLabel("dm.check");
|
||||
B.line(`${undef} = icmp eq i32 ${this.kindOf(B, e)}, ${DYN_KIND.UNDEF}`);
|
||||
B.condBr(undef, next, checkLabel);
|
||||
B.startBlock(checkLabel);
|
||||
}
|
||||
const ok = B.tmp();
|
||||
B.line(`${ok} = call zeroext i1 @${m}(ptr ${e})`);
|
||||
const lOk = B.newLabel("dm.eo");
|
||||
@@ -1480,8 +1601,9 @@ export class LlDyn {
|
||||
* fields, and at least one unless an index signature collects the
|
||||
* members), record members typed as unions of those leaves, records and
|
||||
* arrays, null and undefined (optional and nullable members; see
|
||||
* jsonUnionSupported), and index signatures whose value type is not a
|
||||
* union. Everything else keeps the checked-dynamic route alone. */
|
||||
* jsonUnionSupported), discriminated unions of records anywhere (see
|
||||
* jsonDiscriminatedSupported), and index signatures whose value type is
|
||||
* not a union. Everything else keeps the checked-dynamic route alone. */
|
||||
private jsonSchemaSupported(t: IrType, visiting: Set<string>, member = false): boolean {
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
@@ -1508,13 +1630,42 @@ export class LlDyn {
|
||||
(!index || this.jsonSchemaSupported(index, visiting))
|
||||
);
|
||||
}
|
||||
case "union":
|
||||
case "union": {
|
||||
const def = this.host.unionsById.get(t.unionId);
|
||||
if (def?.discriminant) return this.jsonDiscriminatedSupported(def, visiting);
|
||||
return member && this.jsonUnionSupported(t, visiting);
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** A discriminated union the one-pass parser writes directly, as a record
|
||||
* member, an array element or the whole document: record arms that each
|
||||
* name the string literals their discriminant member holds, every
|
||||
* literal naming one arm, plus null and undefined arms. An object then
|
||||
* selects the one arm whose literal its discriminant member holds, the
|
||||
* only arm the checked route's literal tests leave to match
|
||||
* (unionArmsByDiscriminant); an object holding no such literal declines. */
|
||||
private jsonDiscriminatedSupported(def: IrUnionDef, visiting: Set<string>): boolean {
|
||||
const discriminant = def.discriminant!;
|
||||
const literals = new Set<string>();
|
||||
let records = 0;
|
||||
const supported = def.arms.every((arm, tag) => {
|
||||
if (arm.kind === "nullT" || arm.kind === "undefinedT") return true;
|
||||
if (arm.kind !== "record") return false;
|
||||
records++;
|
||||
const guard = discriminant.cases.find((candidate) => candidate.tag === tag);
|
||||
if (!guard || guard.values.length === 0) return false;
|
||||
for (const value of guard.values) {
|
||||
if (typeof value !== "string" || literals.has(value)) return false;
|
||||
literals.add(value);
|
||||
}
|
||||
return this.jsonSchemaSupported(arm, visiting);
|
||||
});
|
||||
return supported && records >= 2;
|
||||
}
|
||||
|
||||
/** A union record member the one-pass parser writes directly: every arm
|
||||
* a number, boolean, string, null, undefined, or a supported record or
|
||||
* array, with at most one record and one array arm and no discriminant.
|
||||
@@ -1619,9 +1770,15 @@ export class LlDyn {
|
||||
body = `${S} 3, ${S} ${entries.length}, ptr ${fields}, ptr ${overflow.elem}, ptr @${mangleRecordNew(t.shapeId)}, ptr null, ptr ${vAdapters(host, t).release}, ptr null, ${S} ${overflow.offset}`;
|
||||
break;
|
||||
}
|
||||
case "union":
|
||||
body = `${S} 5, ${S} 0, ptr null, ptr null, ptr null, ptr null, ptr null, ptr @${this.jsonUnionSchema(t, name)}, ${S} 0`;
|
||||
case "union": {
|
||||
// A discriminated union can be a whole document, whose value the
|
||||
// parser releases when text follows it.
|
||||
const release = host.unionsById.get(t.unionId)?.discriminant
|
||||
? vAdapters(host, t).release
|
||||
: "null";
|
||||
body = `${S} 5, ${S} 0, ptr null, ptr null, ptr null, ptr null, ptr ${release}, ptr @${this.jsonUnionSchema(t, name)}, ${S} 0`;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new InternalCompilerError(`llvm emitter bug: JSON schema of ${t.kind}`);
|
||||
}
|
||||
@@ -1636,7 +1793,11 @@ export class LlDyn {
|
||||
/** The ScrJsonUnion descriptor of a union member (scr_json.h layout):
|
||||
* its slot representation, the arm each JSON value kind selects, and the
|
||||
* values the runtime stores for null and for a missing member. Boxed
|
||||
* unions build their boxes through `<name>_box`. */
|
||||
* unions build their boxes through `<name>_box`. A discriminated union
|
||||
* also lists its object arms by discriminant literal (`<name>_cases`),
|
||||
* and, because it can be an array element or a whole document, whose
|
||||
* values are boxes unless the union is a pointer union, it keeps its null
|
||||
* value and box whatever its field representation. */
|
||||
private jsonUnionSchema(t: IrType & { kind: "union" }, name: string): string {
|
||||
const host = this.host;
|
||||
const S = host.sizeType;
|
||||
@@ -1655,10 +1816,11 @@ export class LlDyn {
|
||||
);
|
||||
const nullTag = tags[0]!;
|
||||
const absentTag = tags[6]!;
|
||||
const nullValue = !inline && nullTag >= 0 ? host.unitInstanceRef(t.unionId, nullTag) : "null";
|
||||
const absentValue = !inline && absentTag >= 0 ? host.absentInstanceRef(t.unionId) : "null";
|
||||
const values = !inline || def.discriminant !== undefined;
|
||||
const nullValue = values && nullTag >= 0 ? host.unitInstanceRef(t.unionId, nullTag) : "null";
|
||||
const absentValue = values && absentTag >= 0 ? host.absentInstanceRef(t.unionId) : "null";
|
||||
let box = "null";
|
||||
if (repr === 2) {
|
||||
if (repr === 2 || (def.discriminant && !nullable)) {
|
||||
box = `@${name}_box`;
|
||||
const lines = [`define internal ptr ${box}(i32 %tag, i64 %bits) ${FN_ATTRS} {`, `entry:`];
|
||||
const cases: string[] = [];
|
||||
@@ -1699,9 +1861,27 @@ export class LlDyn {
|
||||
``,
|
||||
);
|
||||
}
|
||||
let cases = "null";
|
||||
if (def.discriminant) {
|
||||
const discriminant = def.discriminant;
|
||||
const caseTy = `{ ptr, ${S}, ${S}, ptr }`;
|
||||
const entries = discriminant.cases.flatMap((guard) =>
|
||||
guard.values.map(
|
||||
(value) =>
|
||||
`${caseTy} { ptr ${host.cstr(String(value))}, ${S} ${Buffer.byteLength(String(value), "utf8")}, ${S} ${guard.tag}, ptr @${this.jsonSchema(def.arms[guard.tag]!)} }`,
|
||||
),
|
||||
);
|
||||
const table = `@${name}_case_table`;
|
||||
cases = `@${name}_cases`;
|
||||
this.defs.push(
|
||||
`${table} = internal constant [${entries.length} x ${caseTy}] [${entries.join(", ")}]`,
|
||||
`${cases} = internal constant { ptr, ${S}, ${S}, ptr } { ptr ${host.cstr(discriminant.field)}, ${S} ${Buffer.byteLength(discriminant.field, "utf8")}, ${S} ${entries.length}, ptr ${table} }`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
const descriptor = `${name}_union`;
|
||||
this.defs.push(
|
||||
`@${descriptor} = internal constant { ${S}, [7 x ${S}], [7 x ptr], ptr, ptr, ptr } { ${S} ${repr}, [7 x ${S}] [${tags.map((tag) => `${S} ${tag}`).join(", ")}], [7 x ptr] [${arms.join(", ")}], ptr ${nullValue}, ptr ${absentValue}, ptr ${box} }`,
|
||||
`@${descriptor} = internal constant { ${S}, [7 x ${S}], [7 x ptr], ptr, ptr, ptr, ptr } { ${S} ${repr}, [7 x ${S}] [${tags.map((tag) => `${S} ${tag}`).join(", ")}], [7 x ptr] [${arms.join(", ")}], ptr ${nullValue}, ptr ${absentValue}, ptr ${box}, ptr ${cases} }`,
|
||||
``,
|
||||
);
|
||||
return descriptor;
|
||||
@@ -1751,7 +1931,12 @@ export class LlDyn {
|
||||
* dynCheck builder — which reports the exact error. Returns +1, or null
|
||||
* with the pending flag set. */
|
||||
jsonParseHelper(t: IrType): string | null {
|
||||
if (t.kind !== "record" && t.kind !== "array") return null;
|
||||
if (
|
||||
t.kind !== "record" &&
|
||||
t.kind !== "array" &&
|
||||
!(t.kind === "union" && this.host.unionsById.get(t.unionId)?.discriminant)
|
||||
)
|
||||
return null;
|
||||
const key = typeKey(t);
|
||||
const existing = this.jsonParsers.get(key);
|
||||
if (existing) return existing;
|
||||
@@ -1877,7 +2062,8 @@ export class LlDyn {
|
||||
}
|
||||
|
||||
dynCheckHelper(t: IrType, preserveRefs = false): string {
|
||||
const key = typeKey(t);
|
||||
// A `(T | undefined)[]` stored as T elements also takes undefined ones.
|
||||
const key = checkedConversionKey(t);
|
||||
const builderKey = preserveRefs ? `identity:${key}` : key;
|
||||
const existing = this.dynBuilders.get(builderKey);
|
||||
if (existing) return existing;
|
||||
@@ -2684,6 +2870,21 @@ export class LlDyn {
|
||||
B.line(`${ip} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 2`);
|
||||
B.line(`store ${host.sizeType} ${i}, ptr ${ip}`);
|
||||
const e = this.itemAt(B, items, i);
|
||||
if (t.optionalElements) {
|
||||
// An undefined element is a present undefined element.
|
||||
const undef = B.tmp();
|
||||
const storeUndefined = B.newLabel("dca.undef");
|
||||
const checkLabel = B.newLabel("dca.check");
|
||||
B.line(`${undef} = icmp eq i32 ${this.kindOf(B, e)}, ${DYN_KIND.UNDEF}`);
|
||||
B.condBr(undef, storeUndefined, checkLabel);
|
||||
B.startBlock(storeUndefined);
|
||||
host.declare(`declare void @scr_arr_set_undefined(ptr, double)`);
|
||||
const at = B.tmp();
|
||||
B.line(`${at} = ${sizeToDouble(this.S)} ${this.S} ${i} to double`);
|
||||
B.line(`call void @scr_arr_set_undefined(ptr ${a}, double ${at})`);
|
||||
B.br(doneLabel);
|
||||
B.startBlock(checkLabel);
|
||||
}
|
||||
const v = B.tmp();
|
||||
B.line(`${v} = call ${this.valTy(elem)} @${c}(ptr ${e}, ptr ${pathSlot})`);
|
||||
this.pendingBail(
|
||||
@@ -2710,7 +2911,18 @@ export class LlDyn {
|
||||
`llvm emitter bug: dynCheck of unknown union ${t.unionId}`,
|
||||
);
|
||||
// Arms in unionArmOrder, first FULL match wins. The matched arm's
|
||||
// builder can no longer fail.
|
||||
// builder can no longer fail, and it runs as a trusted build: a
|
||||
// nested union inside it picks the arm trustedArmKinds names without
|
||||
// matching the structure that this match already checked.
|
||||
const trustedKinds = preserveRefs ? def.arms.map(() => null) : this.trustedArmKinds(def);
|
||||
const trustedCall = (call: () => void): void => {
|
||||
host.declare(`declare zeroext i1 @scr_dyn_trust_enter()`);
|
||||
host.declare(`declare void @scr_dyn_trust_exit(i1 zeroext)`);
|
||||
const previous = B.tmp();
|
||||
B.line(`${previous} = call zeroext i1 @scr_dyn_trust_enter()`);
|
||||
call();
|
||||
B.line(`call void @scr_dyn_trust_exit(i1 zeroext ${previous})`);
|
||||
};
|
||||
const build = (i: number): void => {
|
||||
const arm = def.arms[i]!;
|
||||
if (arm.kind === "undefinedT" || arm.kind === "nullT") {
|
||||
@@ -2738,14 +2950,22 @@ export class LlDyn {
|
||||
} else if (host.nullableUnions.has(t.unionId)) {
|
||||
// A nullable union is its (+1) reference payload.
|
||||
const x = B.tmp();
|
||||
B.line(`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`);
|
||||
trustedCall(() =>
|
||||
B.line(
|
||||
`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`,
|
||||
),
|
||||
);
|
||||
B.terminate(`ret ptr ${x}`);
|
||||
} else {
|
||||
const rc = vAdapters(host, arm);
|
||||
host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
|
||||
const x = B.tmp();
|
||||
const u = B.tmp();
|
||||
B.line(`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`);
|
||||
trustedCall(() =>
|
||||
B.line(
|
||||
`${x} = call ptr @${this.dynCheckHelper(arm, preserveRefs)}(ptr %d, ptr %path)`,
|
||||
),
|
||||
);
|
||||
B.line(
|
||||
`${u} = call ptr @scr_union_new_ref(i32 ${i}, ptr ${x}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host, arm)})`,
|
||||
);
|
||||
@@ -2766,6 +2986,7 @@ export class LlDyn {
|
||||
unmatched,
|
||||
preserveRefs,
|
||||
`@${name}.cases`,
|
||||
trustedKinds,
|
||||
);
|
||||
for (const i of order) {
|
||||
B.startBlock(hits[i]!);
|
||||
@@ -2776,7 +2997,7 @@ export class LlDyn {
|
||||
for (const i of order) {
|
||||
const lHit = B.newLabel("dcu.h");
|
||||
const lNext = B.newLabel("dcu.n");
|
||||
this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs);
|
||||
this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs, trustedKinds[i]!);
|
||||
B.startBlock(lHit);
|
||||
build(i);
|
||||
B.startBlock(lNext);
|
||||
|
||||
@@ -20,7 +20,12 @@ import { constructorClass } from "../../ir/final-fields.js";
|
||||
import { findLoopArrayBorrows } from "./loop-array-borrows.js";
|
||||
import { findCallArrayReads, findLocalArrayReads } from "./local-array-reads.js";
|
||||
import { findScalarStringSlices, findScratchStringSlices } from "./string-slices.js";
|
||||
import { findMapReadLifetimes, findReadNumberMapWrites } from "./map-read-lifetimes.js";
|
||||
import {
|
||||
findMapReadBorrows,
|
||||
findMapReadLifetimes,
|
||||
findReadNumberMapWrites,
|
||||
} from "./map-read-lifetimes.js";
|
||||
import { borrowableMapGet } from "./expr-containers.js";
|
||||
import { findMapEntryFusions } from "./map-entry-fusion.js";
|
||||
import { findLocalStackUnions } from "./stack-unions.js";
|
||||
import { findLocalStackObjects, findReassignedStackObjects } from "./stack-objects.js";
|
||||
@@ -200,6 +205,17 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
fn,
|
||||
emitter.boxedUnionsById,
|
||||
emitter.callLifetimes,
|
||||
emitter.referenceEffects,
|
||||
);
|
||||
const stable = emitter.callLifetimes.bindings.get(fn.name);
|
||||
emitter.borrowedMapGets = findMapReadBorrows(
|
||||
fn,
|
||||
emitter.referenceEffects,
|
||||
(decl) =>
|
||||
decl.init !== null &&
|
||||
stable?.has(decl.localId) === true &&
|
||||
!emitter.mapReadLifetimes.locals.has(decl.localId) &&
|
||||
borrowableMapGet(emitter, decl.init),
|
||||
);
|
||||
emitter.readNumberMapWrites = findReadNumberMapWrites(fn);
|
||||
emitter.numberMapKeyIntegers = new Map();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* 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 { DebugRequest } from "./debug-info.js";
|
||||
import type { LlEmitter } from "./emitter.js";
|
||||
|
||||
/** One event of a range's emission, in order. */
|
||||
@@ -97,6 +98,8 @@ export class EmitJournal {
|
||||
* worker process can write it. */
|
||||
export interface ChunkResult {
|
||||
readonly entries: readonly JournalEntry[];
|
||||
/** The requests that added debug metadata nodes (LlvmDebugInfo.record). */
|
||||
readonly debugRequests: readonly DebugRequest[];
|
||||
readonly flags: {
|
||||
readonly usesCoroutines: boolean;
|
||||
readonly usesTimers: boolean;
|
||||
@@ -152,23 +155,83 @@ export function replayJournal(
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
* sigil), field alias tags (`tags`) and debug metadata ids (`debugIds`,
|
||||
* `!N`). 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>,
|
||||
debugIds: ReadonlyMap<string, string> = new Map(),
|
||||
): (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;
|
||||
if (prefixes.size === 0 && tags.size === 0 && debugIds.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);
|
||||
if (debugIds.size > 0) out = renameDebugIds(out, debugIds);
|
||||
return out;
|
||||
};
|
||||
}
|
||||
|
||||
/** Renames metadata references `!N` outside string literals and comments.
|
||||
* Debug builds attach no alias tags, so every such reference in a
|
||||
* function's text is a debug metadata id. A string literal or a comment
|
||||
* (`;` outside a string literal) ends at the end of its line; a quote
|
||||
* inside a string literal is escaped (`\22`). */
|
||||
function renameDebugIds(text: string, ids: ReadonlyMap<string, string>): string {
|
||||
const parts: string[] = [];
|
||||
let copied = 0;
|
||||
// The line of the reference being looked at, and whether the reference
|
||||
// is inside a string literal or a comment there. The next quote and
|
||||
// semicolon positions only move forward, so each is found once.
|
||||
let lineEnd = -1;
|
||||
let quoted = false;
|
||||
let comment = false;
|
||||
let quote = text.indexOf('"');
|
||||
let semicolon = text.indexOf(";");
|
||||
for (let at = text.indexOf("!"); at >= 0; at = text.indexOf("!", at + 1)) {
|
||||
if (at > lineEnd) {
|
||||
lineEnd = text.indexOf("\n", at);
|
||||
if (lineEnd < 0) lineEnd = text.length;
|
||||
quoted = false;
|
||||
comment = false;
|
||||
// Quotes and semicolons of earlier lines end with them.
|
||||
if ((quote >= 0 && quote < at) || (semicolon >= 0 && semicolon < at)) {
|
||||
const lineStart = text.lastIndexOf("\n", at) + 1;
|
||||
while (quote >= 0 && quote < lineStart) quote = text.indexOf('"', quote + 1);
|
||||
while (semicolon >= 0 && semicolon < lineStart)
|
||||
semicolon = text.indexOf(";", semicolon + 1);
|
||||
}
|
||||
}
|
||||
while (!comment) {
|
||||
const nextQuote = quote >= 0 && quote < at ? quote : -1;
|
||||
const nextSemicolon = semicolon >= 0 && semicolon < at ? semicolon : -1;
|
||||
if (nextQuote < 0 && nextSemicolon < 0) break;
|
||||
if (nextQuote >= 0 && (nextSemicolon < 0 || nextQuote < nextSemicolon)) {
|
||||
quoted = !quoted;
|
||||
quote = text.indexOf('"', quote + 1);
|
||||
} else {
|
||||
if (!quoted) comment = true;
|
||||
semicolon = text.indexOf(";", semicolon + 1);
|
||||
}
|
||||
}
|
||||
if (quoted || comment) continue;
|
||||
if (at > 0 && isNameChar(text.charCodeAt(at - 1))) continue;
|
||||
let end = at + 1;
|
||||
while (end < text.length && isDigit(text.charCodeAt(end))) end++;
|
||||
if (end === at + 1 || (end < text.length && isNameChar(text.charCodeAt(end)))) continue;
|
||||
const own = ids.get(text.slice(at, end));
|
||||
if (own === undefined) continue;
|
||||
parts.push(text.slice(copied, at), own);
|
||||
copied = end;
|
||||
}
|
||||
if (copied === 0) return text;
|
||||
parts.push(text.slice(copied));
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
function renameNames(
|
||||
text: string,
|
||||
names: ReadonlyMap<string, string>,
|
||||
|
||||
@@ -102,10 +102,10 @@ function emitFunctionsInChunks(emitter: LlEmitter, chunks: number): string[] {
|
||||
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 {
|
||||
/** Modules with fewer functions emit in one process, and compute their
|
||||
* analyses in it: forking and merging would cost more than the workers save
|
||||
* (SCRIPTC_EMIT_WORKER_FUNCTIONS lowers it, for tests). */
|
||||
export function workerMinFunctions(): number {
|
||||
const value = Number(process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] ?? "2000");
|
||||
return Number.isFinite(value) && value > 0 ? value : 2000;
|
||||
}
|
||||
@@ -134,6 +134,7 @@ function emitFunctionsOnWorkers(emitter: LlEmitter, workers: number): string[] |
|
||||
let code = 1;
|
||||
try {
|
||||
emitter.journal = new EmitJournal();
|
||||
emitter.debug?.record();
|
||||
const texts: string[] = [];
|
||||
for (const fn of functions.slice(range * size, (range + 1) * size)) {
|
||||
texts.push(emitter.emitFunction(fn));
|
||||
@@ -230,13 +231,10 @@ const PARTITION_GROUP_END = "; scriptc-group";
|
||||
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.
|
||||
// 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) ??
|
||||
(chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ??
|
||||
emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) ??
|
||||
[];
|
||||
if (fnDefs.length === 0)
|
||||
for (const fn of emitter.mod.functions) {
|
||||
@@ -1677,8 +1675,8 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
// order; other differing words decide by their first differing byte,
|
||||
// and strings shorter than a word by their first byte. Otherwise a
|
||||
// proven-ASCII string on either side orders by bytes (scr_str_cmp), and
|
||||
// other pairs call scr_str_cmp_u16_bytes, which only reads the two
|
||||
// strings, so LLVM can merge repeated comparisons of one pair. Callers
|
||||
// other pairs call scr_str_cmp_u16_bytes. Both only read the two strings,
|
||||
// so LLVM can merge repeated comparisons of one pair. Callers
|
||||
// only test the sign of the result.
|
||||
if (emitter.decls.has(`declare i32 @scr_str_cmp_u16(ptr, ptr)`)) {
|
||||
const fn = "scr_str_cmp_u16";
|
||||
@@ -1808,7 +1806,7 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
` %anyascii = icmp slt i32 %cws, 0`,
|
||||
` br i1 %anyascii, label %memorder, label %units`,
|
||||
`memorder:`,
|
||||
` %m = call i32 @scr_str_cmp(ptr %a, ptr %b)`,
|
||||
` %m = call i32 @scr_str_cmp(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`,
|
||||
` ret i32 %m`,
|
||||
`units:`,
|
||||
` %c = call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)`,
|
||||
|
||||
@@ -32,7 +32,7 @@ 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 { charAtUnitRead, emitDiscardedSplice } from "./expr-containers.js";
|
||||
import { charAtUnitRead, emitBorrowedMapGet, emitDiscardedSplice } from "./expr-containers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import {
|
||||
emitDenseArrayGet,
|
||||
@@ -105,6 +105,19 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
break;
|
||||
}
|
||||
const b = emitter.binding(s.localId);
|
||||
if (
|
||||
emitter.borrowedMapGets.has(s.localId) &&
|
||||
b.kind === "local" &&
|
||||
s.init?.kind === "mapIntrinsic"
|
||||
) {
|
||||
// The map entry's own reference: no retain now, no release at scope
|
||||
// exit (findMapReadBorrows).
|
||||
const borrowed = emitBorrowedMapGet(emitter, s.init);
|
||||
if (borrowed) {
|
||||
B.line(`store ptr ${borrowed.name}, ptr ${b.slot}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (emitter.walkBorrows.has(s.localId) && b.kind === "local") {
|
||||
// A borrowed walk pointer: no retain now, no release at scope exit.
|
||||
const v = s.init === null ? "null" : emitter.emitReadReceiver(s.init).name;
|
||||
@@ -465,6 +478,9 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
emitter.publishedTypes?.containerGuarded(s.arr.type) !== true
|
||||
)
|
||||
break;
|
||||
// The literal it follows was constructed with this capacity
|
||||
// (reservedArrayLength).
|
||||
if (emitter.consumedReserves.delete(s)) break;
|
||||
const arr = emitBorrowedInput(emitter, s.arr);
|
||||
const length = emitter.emitExpr(s.length);
|
||||
if (s.arr.type.kind !== "array")
|
||||
@@ -2061,7 +2077,8 @@ function emitCallbackVersionedLoop(
|
||||
/** The copy of a versioned loop that runs when its guard fails. Its loops
|
||||
* get no speculative or int32 kind copies, which would emit each loop again
|
||||
* for a path that rarely runs. Proven integer loops, which emit one copy,
|
||||
* are kept. */
|
||||
* are kept. A typed-array version whose guard requires thread-owned
|
||||
* storage does not use it (emitVersionedLoop). */
|
||||
function emitSlowCopy(emitter: LlEmitter, s: IrStmt): void {
|
||||
const copies = emitter.loopCopiesEnabled;
|
||||
emitter.loopCopiesEnabled = false;
|
||||
@@ -2122,7 +2139,9 @@ function declareAliasScopes(emitter: LlEmitter, scopes: readonly string[]): void
|
||||
* fast copy is only emitted when every resume point exists. */
|
||||
/** A typed-array loop version: the guard, then the checked copy, which
|
||||
* runs whenever the guard fails, and the copy whose accesses the guard
|
||||
* proves in bounds (or thread-owned). */
|
||||
* proves in bounds (or thread-owned). In worker programs a version that
|
||||
* proves both also has a copy for views with shared storage, in bounds but
|
||||
* not owned. */
|
||||
function emitVersionedLoop(
|
||||
emitter: LlEmitter,
|
||||
s: IrStmt & { kind: "for" | "while" },
|
||||
@@ -2134,7 +2153,11 @@ function emitVersionedLoop(
|
||||
done = B.newLabel("bytes.version.done");
|
||||
const unitsFast =
|
||||
version.unitsReceiver !== null ? B.newLabel("bytes.version.fast.units") : undefined;
|
||||
const guard = emitBytesLoopVersionGuard(emitter, version, fast, slow, unitsFast);
|
||||
const sharedFast =
|
||||
version.sharedCopy && unitsFast === undefined
|
||||
? B.newLabel("bytes.version.fast.shared")
|
||||
: undefined;
|
||||
const guard = emitBytesLoopVersionGuard(emitter, version, fast, slow, unitsFast, sharedFast);
|
||||
const units = guard?.units ?? null;
|
||||
const originalBounds = emitter.bytesBounds,
|
||||
originalOwned = emitter.bytesOwned,
|
||||
@@ -2145,7 +2168,11 @@ function emitVersionedLoop(
|
||||
LOOPS.of(emitter).bytesLoopVersioning.add(s);
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
emitSlowCopy(emitter, s);
|
||||
// A guard that requires thread-owned storage fails for every view of
|
||||
// shared memory, which worker programs pass routinely: that checked
|
||||
// copy keeps its loops' speculative and int32 kind copies.
|
||||
if (version.ownedReceivers.length > 0) emitter.emitStmt(s);
|
||||
else emitSlowCopy(emitter, s);
|
||||
B.br(done);
|
||||
if (version.owned.size > 0) emitter.bytesOwned = new Set([...originalOwned, ...version.owned]);
|
||||
if (version.ranges.size > 0)
|
||||
@@ -2169,6 +2196,16 @@ function emitVersionedLoop(
|
||||
]);
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
if (sharedFast !== undefined) {
|
||||
// The bounds hold and a view has shared storage: the accesses skip
|
||||
// their bounds checks and stay shared-safe, without alias scopes
|
||||
// (which worker programs give only thread-owned views).
|
||||
B.startBlock(sharedFast);
|
||||
emitter.bytesOwned = originalOwned;
|
||||
emitter.bytesAliasScopes = originalScopes;
|
||||
emitter.emitStmt(s);
|
||||
B.br(done);
|
||||
}
|
||||
if (unitsFast !== undefined && units !== null) {
|
||||
// The string reads take the receiver's UTF-16 units.
|
||||
B.startBlock(unitsFast);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { youngNurseryRequested } from "../young-nursery.js";
|
||||
import { llvmBytes as llBytes, wtf8Bytes } from "../literals.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { serializeModuleParts } from "../../ir/serialize.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
endsWithJump,
|
||||
@@ -88,6 +89,8 @@ import {
|
||||
fillAllocationLength,
|
||||
int32ArrayKey,
|
||||
selectInt32Arrays,
|
||||
type Int32ArrayProof,
|
||||
type Int32ArrayRevision,
|
||||
} from "../../ir/int32-arrays.js";
|
||||
import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
|
||||
import {
|
||||
@@ -103,8 +106,9 @@ import { resolveSelfCaptures } from "../../ir/self-captures.js";
|
||||
import {
|
||||
analyzeInt32Slots,
|
||||
dynamicFieldStores,
|
||||
Int32RevisionConflict,
|
||||
type DynamicFieldStores,
|
||||
type Int32Slots,
|
||||
Int32Slots,
|
||||
} from "../../ir/int32-slots.js";
|
||||
import { analyzeFinalFields, type FinalFields } from "../../ir/final-fields.js";
|
||||
import { foldConstantGlobals } from "../../ir/constant-globals.js";
|
||||
@@ -198,7 +202,7 @@ import { ReferenceEffects } from "./reference-effects.js";
|
||||
import { ReferenceWrites, type Writes } from "./reference-writes.js";
|
||||
import { LlvmDebugInfo } from "./debug-info.js";
|
||||
import { StackCallbacks } from "./stack-callbacks.js";
|
||||
import { findAppendedArrayLiterals } from "./appended-array-literals.js";
|
||||
import { findAppendedArrayLiterals, reservedArrayLength } from "./appended-array-literals.js";
|
||||
import { AmbientReceiverReaders, findConstantCallbacks } from "./constant-callbacks.js";
|
||||
import {
|
||||
findLentRecordFields,
|
||||
@@ -373,7 +377,9 @@ import {
|
||||
emitModuleParts,
|
||||
emitParts,
|
||||
helperDefs,
|
||||
workerMinFunctions,
|
||||
} from "./emit-module.js";
|
||||
import { forkedAnalysis } from "./forked-analysis.js";
|
||||
import {
|
||||
emitLoopCopies,
|
||||
emitSpeculativeWrite,
|
||||
@@ -470,14 +476,13 @@ export interface LlvmTargetOptions {
|
||||
* earlier emission found breaking its rules (UnwindAbiViolation). */
|
||||
unwindExcluded?: ReadonlySet<string>;
|
||||
/** Emit the function bodies in this many contiguous ranges, each by its
|
||||
* own emitter, and merge them in function order (emit-journal.ts). The
|
||||
* output is the same as one emitter's. Debug builds (`debugSources`)
|
||||
* emit in one emitter. */
|
||||
* own emitter, and merge them in function order (emit-journal.ts, and
|
||||
* debug-info.ts for debug metadata). The output is the same as one
|
||||
* emitter's. */
|
||||
functionChunks?: number;
|
||||
/** Emit the function bodies of a large module on this many forked worker
|
||||
* processes at a time (emit-module.ts), forked with `forkPrimitives`. The
|
||||
* output is the same as one emitter's. Debug builds (`debugSources`)
|
||||
* emit in one emitter. */
|
||||
* output is the same as one emitter's. */
|
||||
functionWorkers?: number;
|
||||
/** The host's process primitives, where it can fork (the native
|
||||
* compiler's). */
|
||||
@@ -552,15 +557,60 @@ export interface Int32SlotAnalysis {
|
||||
readonly slots: Int32Slots;
|
||||
}
|
||||
|
||||
/** The int32 slot analysis of `mod`, with the inputs LlEmitter gives it. */
|
||||
function analyzeModuleInt32Slots(mod: IrModule): Int32SlotAnalysis {
|
||||
/** The int32 slot analysis of `mod`, with the inputs LlEmitter gives it.
|
||||
* With `revise`, the analysis revises the functions of candidates that drop
|
||||
* (Int32SlotInputs.revise), and `mod` is the module it started from. */
|
||||
function analyzeModuleInt32Slots(
|
||||
mod: IrModule,
|
||||
revise?: (fnName: string, dropped: ReadonlySet<string>) => IrFunction,
|
||||
): Int32SlotAnalysis {
|
||||
const nodeIndex = new IrNodeIndex(mod);
|
||||
const initializerBindings = findInitializerBindings(mod, nodeIndex);
|
||||
const dynamicStores = dynamicFieldStores(mod, undefined, undefined, nodeIndex);
|
||||
const slots = analyzeInt32Slots(mod, { dynamicStores, initializerBindings, nodes: nodeIndex });
|
||||
const slots = analyzeInt32Slots(mod, {
|
||||
dynamicStores,
|
||||
initializerBindings,
|
||||
nodes: nodeIndex,
|
||||
...(revise === undefined ? {} : { revise }),
|
||||
});
|
||||
return { mod, nodeIndex, initializerBindings, dynamicStores, slots };
|
||||
}
|
||||
|
||||
/** The candidates an analysis keeps: int32 arrays and checked ones. */
|
||||
function keptInt32Arrays(analysis: Int32SlotAnalysis): ReadonlySet<string> {
|
||||
return new Set([...analysis.slots.int32Arrays, ...analysis.slots.checkedInt32Arrays]);
|
||||
}
|
||||
|
||||
/** An analysis that revised functions (analyzeModuleInt32Slots' `revise`)
|
||||
* as the analysis of `mod`, the module of the functions it ended with: the
|
||||
* node index of `mod`, and its initializer bindings and dynamic stores,
|
||||
* which must be the ones the analysis read. Null when they differ. */
|
||||
function revisedAnalysis(analysis: Int32SlotAnalysis, mod: IrModule): Int32SlotAnalysis | null {
|
||||
const nodeIndex = new IrNodeIndex(mod);
|
||||
const initializerBindings = findInitializerBindings(mod, nodeIndex);
|
||||
const dynamicStores = dynamicFieldStores(mod, undefined, undefined, nodeIndex);
|
||||
const read = analysis.initializerBindings;
|
||||
if (initializerBindings.size !== read.size) return null;
|
||||
for (const [name, globals] of initializerBindings) {
|
||||
const other = read.get(name);
|
||||
if (other?.length !== globals.length || globals.some((g, i) => other[i] !== g)) return null;
|
||||
}
|
||||
const stores = analysis.dynamicStores;
|
||||
if (
|
||||
dynamicStores.all !== stores.all ||
|
||||
dynamicStores.names.size !== stores.names.size ||
|
||||
[...dynamicStores.names].some((name) => !stores.names.has(name))
|
||||
)
|
||||
return null;
|
||||
return {
|
||||
mod,
|
||||
nodeIndex,
|
||||
initializerBindings,
|
||||
dynamicStores,
|
||||
slots: analysis.slots,
|
||||
};
|
||||
}
|
||||
|
||||
/** The functions whose calls the backend may emit as calls of their
|
||||
* result-slot entries (record-results.ts). */
|
||||
function resultEntryTargets(mod: IrModule): ReadonlySet<string> {
|
||||
@@ -575,57 +625,127 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
|
||||
// Facts are keyed by node identity: emit the same module whether it was
|
||||
// lowered in memory or read back from its serialized form.
|
||||
const mod = unshareNodes(input);
|
||||
// Each round of selectInt32Arrays analyzes the module it marks; the
|
||||
// emitter of the module it returns reuses that round's analysis.
|
||||
const rounds: { last: Int32SlotAnalysis | null } = { last: null };
|
||||
const proven = (marked: IrModule): ReadonlySet<string> => {
|
||||
const analysis = analyzeModuleInt32Slots(marked);
|
||||
rounds.last = analysis;
|
||||
return new Set([...analysis.slots.int32Arrays, ...analysis.slots.checkedInt32Arrays]);
|
||||
// Each proof selectInt32Arrays asks for analyzes a marked module; the
|
||||
// emitter of the module it returns reuses the last one. A proof that
|
||||
// revised functions analyzed the module of the functions it ended with.
|
||||
const rounds: {
|
||||
last: Int32SlotAnalysis | null;
|
||||
functions: readonly IrFunction[] | null;
|
||||
revising: boolean;
|
||||
} = { last: null, functions: null, revising: true };
|
||||
const proven = (marked: IrModule, revise: Int32ArrayRevision | null): Int32ArrayProof | null => {
|
||||
rounds.functions = null;
|
||||
if (revise === null || !rounds.revising) {
|
||||
rounds.last = analyzeModuleInt32Slots(marked);
|
||||
return { kept: keptInt32Arrays(rounds.last), functions: null };
|
||||
}
|
||||
const latest = new Map<string, IrFunction>();
|
||||
try {
|
||||
rounds.last = analyzeModuleInt32Slots(marked, (fnName, dropped) => {
|
||||
const fn = revise(fnName, dropped);
|
||||
latest.set(fnName, fn);
|
||||
return fn;
|
||||
});
|
||||
} catch (error) {
|
||||
if (!(error instanceof Int32RevisionConflict)) throw error;
|
||||
rounds.last = null;
|
||||
return null;
|
||||
}
|
||||
if (latest.size === 0) return { kept: keptInt32Arrays(rounds.last), functions: null };
|
||||
rounds.functions = marked.functions.map((fn) => latest.get(fn.name) ?? fn);
|
||||
return { kept: keptInt32Arrays(rounds.last), functions: rounds.functions };
|
||||
};
|
||||
// Keep source storage intact for debugger inspection in dev builds.
|
||||
const emitted =
|
||||
options.debugSources === undefined
|
||||
? selectInt32Arrays(
|
||||
versionParameters(
|
||||
addRecordResultEntries(
|
||||
scalarizeLiteralResults(
|
||||
scalarizeNumericRecords(
|
||||
specializeCallbackCalls(
|
||||
specializeNumericCalls(
|
||||
pairRowSums(
|
||||
inlineExpressionFunctions(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(
|
||||
dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (options.debugSources !== undefined) return new LlEmitter(mod, options, null, null);
|
||||
const passed = versionParameters(
|
||||
addRecordResultEntries(
|
||||
scalarizeLiteralResults(
|
||||
scalarizeNumericRecords(
|
||||
specializeCallbackCalls(
|
||||
specializeNumericCalls(
|
||||
pairRowSums(
|
||||
inlineExpressionFunctions(
|
||||
foldConstantGlobals(
|
||||
copyStableCaptures(
|
||||
resolveSelfCaptures(
|
||||
hoistFinalFieldReads(
|
||||
freezeInitOnlyGlobals(
|
||||
holdCharLocalsAsUnits(
|
||||
dropUnwrittenPropertyBags(runPairedWhileLoopsAsFor(mod)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
),
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
mangleFunction,
|
||||
),
|
||||
proven,
|
||||
resultEntryTargets,
|
||||
)
|
||||
: mod;
|
||||
const prepared = rounds.last;
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
let emitted = selectInt32Arrays(passed, proven, resultEntryTargets);
|
||||
let prepared = rounds.last;
|
||||
// A proof that revised functions analyzed the module of the functions it
|
||||
// ended with, which is the selected module unless selection fell back to
|
||||
// rounds: then the selected module was proved by its own round.
|
||||
if (prepared !== null && rounds.functions !== null && prepared.mod !== emitted) {
|
||||
const functions = rounds.functions;
|
||||
prepared =
|
||||
emitted !== passed &&
|
||||
emitted.functions.length === functions.length &&
|
||||
emitted.functions.every((fn, i) => fn === functions[i])
|
||||
? revisedAnalysis(prepared, emitted)
|
||||
: null;
|
||||
if (prepared === null && emitted !== passed) {
|
||||
rounds.revising = false;
|
||||
emitted = selectInt32Arrays(passed, proven, resultEntryTargets);
|
||||
prepared = rounds.last;
|
||||
}
|
||||
}
|
||||
if (process.env["SCRIPTC_INT32_VERIFY"] === "1")
|
||||
verifyInt32Selection(passed, emitted, prepared?.mod === emitted ? prepared : null);
|
||||
return new LlEmitter(emitted, options, null, prepared?.mod === emitted ? prepared : null);
|
||||
}
|
||||
|
||||
/** SCRIPTC_INT32_VERIFY=1: throws unless the selection with revisions
|
||||
* (`emitted`, and the analysis the emitter takes over) is the one rounds
|
||||
* without revisions give: the same module, and the same facts. */
|
||||
function verifyInt32Selection(
|
||||
passed: IrModule,
|
||||
emitted: IrModule,
|
||||
analysis: Int32SlotAnalysis | null,
|
||||
): void {
|
||||
let last: Int32SlotAnalysis | null = null;
|
||||
const reference = selectInt32Arrays(
|
||||
passed,
|
||||
(marked) => {
|
||||
last = analyzeModuleInt32Slots(marked);
|
||||
return { kept: keptInt32Arrays(last), functions: null };
|
||||
},
|
||||
resultEntryTargets,
|
||||
);
|
||||
const text = (mod: IrModule): string => serializeModuleParts(mod).join("");
|
||||
if (text(reference) !== text(emitted))
|
||||
throw new InternalCompilerError("int32 selection: the revised module differs from the rounds'");
|
||||
const facts = (mod: IrModule, prepared: Int32SlotAnalysis | null): string =>
|
||||
(prepared?.mod === mod ? prepared : analyzeModuleInt32Slots(mod)).slots.summary(mod);
|
||||
const expected = facts(reference, last);
|
||||
const actual = facts(emitted, analysis);
|
||||
if (expected !== actual) {
|
||||
const want = expected.split("\n");
|
||||
const got = actual.split("\n");
|
||||
const line = want.findIndex((entry, i) => entry !== got[i]);
|
||||
throw new InternalCompilerError(
|
||||
`int32 selection: the revised analysis differs from the rounds': ${want[line]?.slice(0, 400)} != ${got[line]?.slice(0, 400)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Emits the module's parts, emitting it again with the functions an
|
||||
* UnwindAbiViolation names on the pending-flag ABI. */
|
||||
function emitModuleText(input: IrModule, options: LlvmTargetOptions): string[] {
|
||||
@@ -1240,6 +1360,9 @@ export class LlEmitter {
|
||||
readonly checkedNarrows: CheckedNarrows;
|
||||
callArrayReads = new Map<IrExpr, LocalArrayRead>();
|
||||
mapReadLifetimes: MapReadLifetimes = { locals: new Map(), arguments: new Map() };
|
||||
/** Locals of the current function that hold a Map.get result as the
|
||||
* entry's own reference (findMapReadBorrows, borrowableMapGet). */
|
||||
borrowedMapGets: ReadonlySet<string> = new Set();
|
||||
readNumberMapWrites: ReadonlySet<IrExpr> = new Set();
|
||||
/** The integer views of the current function's number Map/Set keys, by
|
||||
* the key's double operand (noteNumberMapKey, map-number-lookup.ts). */
|
||||
@@ -1355,6 +1478,11 @@ export class LlEmitter {
|
||||
/** Locals whose array literal emitStmts left for the `await Promise.all`
|
||||
* that reads them (deferredPromiseAllLocal). */
|
||||
private readonly deferredPromiseAllEntries = new Map<string, ExprOf<"arrayLit">>();
|
||||
/** Empty array literals that a capacity hint follows directly
|
||||
* (reservedArrayLength), with that hint. Constructing the literal with
|
||||
* the hinted capacity consumes the hint, which then emits nothing. */
|
||||
readonly reservedArrayLengths = new Map<IrExpr, IrStmt & { kind: "arraySetLength" }>();
|
||||
readonly consumedReserves = new Set<IrStmt>();
|
||||
/** The promise a `return` emitted past its finally regions already
|
||||
* produced for its adopting await (borrowed from the return's parked
|
||||
* slot); that await takes it instead of evaluating its operand. */
|
||||
@@ -1438,7 +1566,12 @@ export class LlEmitter {
|
||||
/** Preorder intervals of the runtime error classes under THIS module's
|
||||
* class-forest numbering (main() stamps scr_error_vts with them, exactly
|
||||
* like the runtime ABI’s errorVtStampLines). */
|
||||
readonly errorIntervals: { kind: number; pre: number; post: number; lib: string }[] = [];
|
||||
readonly errorIntervals: {
|
||||
kind: number;
|
||||
pre: number;
|
||||
post: number;
|
||||
lib: string;
|
||||
}[] = [];
|
||||
/** The runtime emitter vtable's preorder interval, when the program
|
||||
* touches node:events (the class def rides the module exactly then) —
|
||||
* main() stamps scr_emitter_vt with it (emitterVtStampLines, ported). */
|
||||
@@ -1447,7 +1580,12 @@ export class LlEmitter {
|
||||
* ported): the defs ride every emitter-touching module (the frontend
|
||||
* collects the whole emitter-rooted tree), and scr_stream.c links on
|
||||
* the same predicate, so the stamps always have their globals. */
|
||||
readonly streamIntervals: { vt: string; pre: number; post: number; lib: string }[] = [];
|
||||
readonly streamIntervals: {
|
||||
vt: string;
|
||||
pre: number;
|
||||
post: number;
|
||||
lib: string;
|
||||
}[] = [];
|
||||
|
||||
// ── per-function state (reset in emitFunction) ─────────────────────────
|
||||
B = new BlockBuilder();
|
||||
@@ -1483,7 +1621,13 @@ export class LlEmitter {
|
||||
streamingSplitsEnabled = false;
|
||||
readonly initializerBindings: ReturnType<typeof findInitializerBindings>;
|
||||
/** Whole-program int32 storage and parameter/result facts. */
|
||||
readonly int32Slots: Int32Slots;
|
||||
get int32Slots(): Int32Slots {
|
||||
return (this.int32SlotsValue ??= this.joinInt32Slots());
|
||||
}
|
||||
private int32SlotsValue: Int32Slots | undefined;
|
||||
/** The join of the int32 slot analysis a forked process computes
|
||||
* (startInt32Slots). */
|
||||
private int32SlotsJoin: (() => Int32Slots) | undefined;
|
||||
/** The program has no runtime store into a checked-dynamic value
|
||||
* (dynamicFieldStores), so live views of classes and records never
|
||||
* commit back into their objects. Library builds keep their commits:
|
||||
@@ -1517,7 +1661,12 @@ export class LlEmitter {
|
||||
/** Active induction bindings retain their width independently of size_t. */
|
||||
integerLoopBindings = new Map<
|
||||
string,
|
||||
{ slot: string; type: "i32" | "i64"; signed?: boolean; range?: { min: number; max: number } }
|
||||
{
|
||||
slot: string;
|
||||
type: "i32" | "i64";
|
||||
signed?: boolean;
|
||||
range?: { min: number; max: number };
|
||||
}
|
||||
>();
|
||||
/** Constant number locals whose latest emitted declaration stored an
|
||||
* exact integer beside the double, such as `const j = i * 4` in a
|
||||
@@ -1600,7 +1749,11 @@ export class LlEmitter {
|
||||
* (the fast copy) or as double arrays (an ordinary copy entered only
|
||||
* after they widened), as `function\0local` keys, and where each of the
|
||||
* fast copy's writes resumes in the third copy. */
|
||||
int32Kinds: { int32: Set<string>; wide: Set<string>; resume: Map<IrStmt, string> } | null = null;
|
||||
int32Kinds: {
|
||||
int32: Set<string>;
|
||||
wide: Set<string>;
|
||||
resume: Map<IrStmt, string>;
|
||||
} | null = null;
|
||||
/** The resume label of the fast-copy write being emitted. */
|
||||
int32KindResume: string | null = null;
|
||||
/** The function being emitted, for speculative globals. */
|
||||
@@ -1674,7 +1827,12 @@ export class LlEmitter {
|
||||
* releases frames/scopes down to the recorded depths and branches to
|
||||
* `label` (the catch, or the exception-path finally) instead of
|
||||
* returning out of the function. Entering a try emits no code. */
|
||||
tryStack: { label: string; used: boolean; frameDepth: number; scopeDepth: number }[] = [];
|
||||
tryStack: {
|
||||
label: string;
|
||||
used: boolean;
|
||||
frameDepth: number;
|
||||
scopeDepth: number;
|
||||
}[] = [];
|
||||
/** Return type of the function being emitted — the unwind path returns
|
||||
* a dummy of this type (never read: callers check the flag first). */
|
||||
currentReturnType: IrType = VOID;
|
||||
@@ -1733,12 +1891,19 @@ export class LlEmitter {
|
||||
this.journal = journal;
|
||||
this.journaled =
|
||||
journal !== null ||
|
||||
((options.functionWorkers ?? 1) > 1 &&
|
||||
options.forkPrimitives !== undefined &&
|
||||
options.debugSources === undefined);
|
||||
((options.functionWorkers ?? 1) > 1 && options.forkPrimitives !== undefined);
|
||||
// One walk of every body, which the analyses below read instead of
|
||||
// walking the bodies themselves.
|
||||
this.nodeIndex = int32Analysis?.nodeIndex ?? new IrNodeIndex(mod);
|
||||
this.initializerBindings =
|
||||
int32Analysis?.initializerBindings ?? findInitializerBindings(mod, this.nodeIndex);
|
||||
// One walk finds the dynamic field stores every analysis below consults.
|
||||
const dynamicStores =
|
||||
int32Analysis?.dynamicStores ?? dynamicFieldStores(mod, undefined, undefined, this.nodeIndex);
|
||||
// Started before the other analyses, which may run while a forked
|
||||
// process computes it (startInt32Slots).
|
||||
if (int32Analysis !== null) this.int32SlotsValue = int32Analysis.slots;
|
||||
else this.startInt32Slots(dynamicStores);
|
||||
this.stackTraces = observesStacks(mod, this.nodeIndex);
|
||||
this.nullableUnions = new NullableUnions(
|
||||
mod,
|
||||
@@ -1757,18 +1922,6 @@ export class LlEmitter {
|
||||
this.nullableUnions.armTypes(),
|
||||
);
|
||||
this.constantNumericTables = findConstantNumericTables(mod);
|
||||
this.initializerBindings =
|
||||
int32Analysis?.initializerBindings ?? findInitializerBindings(mod, this.nodeIndex);
|
||||
// One walk finds the dynamic field stores every analysis below consults.
|
||||
const dynamicStores =
|
||||
int32Analysis?.dynamicStores ?? dynamicFieldStores(mod, undefined, undefined, this.nodeIndex);
|
||||
this.int32Slots =
|
||||
int32Analysis?.slots ??
|
||||
analyzeInt32Slots(mod, {
|
||||
dynamicStores,
|
||||
initializerBindings: this.initializerBindings,
|
||||
nodes: this.nodeIndex,
|
||||
});
|
||||
this.untypedIntegrity = mod.untypedIntegrity === true;
|
||||
this.liveViewsReadOnly =
|
||||
mod.lib === undefined &&
|
||||
@@ -2216,6 +2369,46 @@ export class LlEmitter {
|
||||
);
|
||||
// After the vtable slots: borrowing slots exclude their implementations.
|
||||
if (this.debug === null) this.analyzeBorrowedReturns();
|
||||
this.int32SlotsValue ??= this.joinInt32Slots();
|
||||
}
|
||||
|
||||
/** Computes the int32 slot analysis, or starts it on a forked process.
|
||||
* Dev builds read its result only near the end of the constructor
|
||||
* (int32Abis is empty with debug info), so for a large module the
|
||||
* process computes it while the constructor runs the other analyses. */
|
||||
private startInt32Slots(dynamicStores: DynamicFieldStores): void {
|
||||
const mod = this.mod;
|
||||
const nodes = this.nodeIndex;
|
||||
const analyze = (): Int32Slots =>
|
||||
analyzeInt32Slots(mod, {
|
||||
dynamicStores,
|
||||
initializerBindings: this.initializerBindings,
|
||||
nodes,
|
||||
});
|
||||
const fork = this.targetOptions.forkPrimitives;
|
||||
if (
|
||||
fork === undefined ||
|
||||
this.targetOptions.debugSources === undefined ||
|
||||
(this.targetOptions.functionWorkers ?? 1) < 2 ||
|
||||
mod.functions.length < workerMinFunctions()
|
||||
) {
|
||||
this.int32SlotsValue = analyze();
|
||||
return;
|
||||
}
|
||||
this.int32SlotsJoin = forkedAnalysis(
|
||||
fork,
|
||||
analyze,
|
||||
(slots) => slots.encode(nodes),
|
||||
(text) => Int32Slots.decode(text, mod, nodes),
|
||||
);
|
||||
}
|
||||
|
||||
private joinInt32Slots(): Int32Slots {
|
||||
const join = this.int32SlotsJoin;
|
||||
if (join === undefined)
|
||||
throw new InternalCompilerError("llvm emitter bug: int32 slots read before analysis");
|
||||
this.int32SlotsJoin = undefined;
|
||||
return join();
|
||||
}
|
||||
|
||||
/** The borrowed parameters of a hierarchy's vtable slot (empty: owned). */
|
||||
@@ -5073,6 +5266,7 @@ export class LlEmitter {
|
||||
chunkResult(): ChunkResult {
|
||||
return {
|
||||
entries: this.journal!.entries,
|
||||
debugRequests: this.debug?.recorded() ?? [],
|
||||
flags: {
|
||||
usesCoroutines: this.usesCoroutines,
|
||||
usesTimers: this.usesTimers,
|
||||
@@ -5108,6 +5302,8 @@ export class LlEmitter {
|
||||
// emission left them (a helper set guards the declarations its first
|
||||
// request adds).
|
||||
replayJournal(this, chunk.entries, names, tags);
|
||||
const debugIds = new Map<string, string>();
|
||||
this.debug?.replay(chunk.debugRequests, (name) => this.functionNamed(name), debugIds);
|
||||
const flags = chunk.flags;
|
||||
this.usesCoroutines ||= flags.usesCoroutines;
|
||||
this.usesTimers ||= flags.usesTimers;
|
||||
@@ -5128,7 +5324,13 @@ export class LlEmitter {
|
||||
for (const name of chunk.adoptCallees) this.adoptCallees.add(name);
|
||||
for (const name of chunk.directAwaitCallees) this.directAwaitCallees.add(name);
|
||||
for (const name of chunk.inlineAwaitCallees) this.inlineAwaitCallees.add(name);
|
||||
return renameChunk(names, tags);
|
||||
return renameChunk(names, tags, debugIds);
|
||||
}
|
||||
|
||||
private functionNamed(name: string): IrFunction {
|
||||
const fn = this.fnByName.get(name);
|
||||
if (fn === undefined) throw new InternalCompilerError(`llvm emitter bug: no function ${name}`);
|
||||
return fn;
|
||||
}
|
||||
|
||||
/** Adds a content-named constant a chunk interned. Its symbol is this
|
||||
@@ -5256,7 +5458,13 @@ export class LlEmitter {
|
||||
/** An emitter for a range of this module's functions: the same facts,
|
||||
* empty module-level tables, and a journal of what the range adds. */
|
||||
chunkEmitter(): LlEmitter {
|
||||
return new LlEmitter(this.mod, { ...this.targetOptions, functionChunks: 1 }, new EmitJournal());
|
||||
const chunk = new LlEmitter(
|
||||
this.mod,
|
||||
{ ...this.targetOptions, functionChunks: 1 },
|
||||
new EmitJournal(),
|
||||
);
|
||||
chunk.debug?.record();
|
||||
return chunk;
|
||||
}
|
||||
|
||||
/** The double-ABI entry of a function with an i32 entry: every caller
|
||||
@@ -5858,6 +6066,8 @@ export class LlEmitter {
|
||||
if (this.B.isTerminated()) return;
|
||||
if (cursor) cursor.index = i;
|
||||
this.stmtCursor = cursor;
|
||||
const reserve = reservedArrayLength(stmts, i);
|
||||
if (reserve !== null) this.reservedArrayLengths.set(reserve.literal, reserve.hint);
|
||||
const deferred = this.deferredPromiseAllLocal(stmts, i);
|
||||
if (deferred !== null) {
|
||||
this.deferredPromiseAllEntries.set((stmts[i] as { localId: string }).localId, deferred);
|
||||
@@ -7161,7 +7371,10 @@ export class LlEmitter {
|
||||
return childDataDynThunkFor(this, chunk);
|
||||
}
|
||||
|
||||
emitterFixedAdapter(cbT: IrType & { kind: "func" }): { fn: string; shim: string } {
|
||||
emitterFixedAdapter(cbT: IrType & { kind: "func" }): {
|
||||
fn: string;
|
||||
shim: string;
|
||||
} {
|
||||
return emitterFixedAdapter(this, cbT);
|
||||
}
|
||||
|
||||
|
||||
@@ -1167,6 +1167,65 @@ export function emitArrIntrinsic(
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether `e` is a Map.get that emitBorrowedMapGet can emit without a
|
||||
* retain: a typed map with number or inline string keys, a reference value,
|
||||
* and a nullable result (the value's pointer, or the absent arm's). */
|
||||
export function borrowableMapGet(host: LlvmEmitterContext, e: IrExpr): boolean {
|
||||
if (e.kind !== "mapIntrinsic" || e.method !== "get" || e.args.length !== 1) return false;
|
||||
if (!borrowsMapReadInputs(e) || e.receiver.type.kind !== "map") return false;
|
||||
const { key, value } = e.receiver.type;
|
||||
if (!isRefCounted(value) || value.kind === "union" || key.kind === "union") return false;
|
||||
if (e.type.kind !== "union" || !host.nullableUnions.has(e.type.unionId)) return false;
|
||||
const arg = e.args[0]!;
|
||||
const access = mapKeyAccess(key);
|
||||
return (
|
||||
(access === "f64" && arg.type.kind === "f64") ||
|
||||
(access === "str" && arg.type.kind === "string")
|
||||
);
|
||||
}
|
||||
|
||||
/** A borrowableMapGet read as the entry's own reference: no retain, and
|
||||
* no owner. The caller proves that nothing can change the map while the
|
||||
* value is used (findMapReadBorrows). Null, before emitting anything, for
|
||||
* a key held as a split span, which takes the runtime's owned lookup. */
|
||||
export function emitBorrowedMapGet(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr & { kind: "mapIntrinsic" },
|
||||
): LlValue | null {
|
||||
const arg = e.args[0]!;
|
||||
if (arg.kind === "varRef" && host.splitSpans.get(arg.localId) !== undefined) return null;
|
||||
const B = host.B;
|
||||
const mapType = e.receiver.type as IrType & { kind: "map" };
|
||||
const r = host.emitStableReceiver(e.receiver, e.args);
|
||||
const k = emitMapLookupKey(host, e.args[0]!, true);
|
||||
const numberKey = inlineNumberKey(k);
|
||||
const stringKey = entryStringKey(k);
|
||||
const value = mapType.value;
|
||||
let raw: string;
|
||||
if (numberKey !== null) raw = emitNumberMapGetRef(host, r.name, numberKey, value, true);
|
||||
else if (stringKey !== null)
|
||||
raw = emitStringMapGetRef(
|
||||
host,
|
||||
r.name,
|
||||
stringKey,
|
||||
value,
|
||||
compactMapValues(host, mapType.key, value),
|
||||
true,
|
||||
);
|
||||
else throw new InternalCompilerError("llvm emitter bug: borrowed Map.get without an inline key");
|
||||
const resultType = e.type as IrType & { kind: "union" };
|
||||
const absent = host.unitInstanceRef(
|
||||
resultType.unionId,
|
||||
undefinedArmTag(resultType, host.unionsById),
|
||||
);
|
||||
if (absent === "null") return { name: raw, type: e.type };
|
||||
const isnull = B.tmp(),
|
||||
t = B.tmp();
|
||||
B.line(`${isnull} = icmp eq ptr ${raw}, null`);
|
||||
B.line(`${t} = select i1 ${isnull}, ptr ${absent}, ptr ${raw}`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
|
||||
export function wrapNullable(
|
||||
host: LlvmEmitterContext,
|
||||
raw: string,
|
||||
|
||||
@@ -31,7 +31,8 @@ import { elemAccess, vAdapters } from "./shapes.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
import { emitUnionWiden } from "./expr-records.js";
|
||||
import { emitJsIcMethod, jsIcMethods } from "./js-ic.js";
|
||||
import { emitJsIcLiteralMember, emitJsIcMethod, jsIcMethods } from "./js-ic.js";
|
||||
import { jsIcLiteralKey } from "../../ir/js-ic.js";
|
||||
import { DYN_BUFFER_METHODS } from "../../ir/runtime-features.js";
|
||||
|
||||
export function emitDynamicExpr(
|
||||
@@ -316,9 +317,14 @@ export function emitDynamicExpr(
|
||||
const obj = B.tmp();
|
||||
B.line(`${obj} = call ptr @scr_dyn_new_obj()`);
|
||||
const out = host.own({ name: obj, type: e.type });
|
||||
const cached = jsIcMethods(host);
|
||||
for (const f of e.fields ?? []) {
|
||||
const k = emitBorrowedInput(host, f.key);
|
||||
const v = emitBorrowedInput(host, f.value);
|
||||
if (cached && jsIcLiteralKey(f.key) !== null) {
|
||||
emitJsIcLiteralMember(host, obj, k.name, v.name);
|
||||
continue;
|
||||
}
|
||||
B.line(
|
||||
`call void @${f.key.type.kind === "dyn" ? "scr_dyn_key_set_computed" : "scr_dyn_key_set"}(ptr ${obj}, ptr ${k.name}, ptr ${v.name})`,
|
||||
);
|
||||
|
||||
@@ -946,6 +946,34 @@ export function emitStringExpr(
|
||||
}
|
||||
}
|
||||
|
||||
/** scr_arr_reserve_len reserves dense storage only up to this many slots
|
||||
* (SCR_ARR_DENSE_LIMIT, scr_array.c). */
|
||||
const RESERVE_LIMIT = 2 ** 20;
|
||||
|
||||
/** Elements the inline slots of a small array construction
|
||||
* (scr_arr_new_small, scr_arr_new_ops_small) hold at least
|
||||
* (SCR_ARR_SMALL_SLOTS, scr_array.c). */
|
||||
const SMALL_ARRAY_SLOTS = 3;
|
||||
|
||||
/** The capacity scr_arr_reserve_len would reserve for `length` on an empty
|
||||
* array: the length truncated when it is at least 1 and at most
|
||||
* RESERVE_LIMIT, else 0. */
|
||||
function reservedCapacity(host: LlvmEmitterContext, length: IrExpr): string {
|
||||
const B = host.B;
|
||||
const n = host.emitExpr(length);
|
||||
const atLeastOne = B.tmp(),
|
||||
withinLimit = B.tmp(),
|
||||
reserves = B.tmp(),
|
||||
slots = B.tmp(),
|
||||
capacity = B.tmp();
|
||||
B.line(`${atLeastOne} = fcmp oge double ${n.name}, ${f64Lit(1)}`);
|
||||
B.line(`${withinLimit} = fcmp ole double ${n.name}, ${f64Lit(RESERVE_LIMIT)}`);
|
||||
B.line(`${reserves} = and i1 ${atLeastOne}, ${withinLimit}`);
|
||||
B.line(`${slots} = select i1 ${reserves}, double ${n.name}, double ${f64Lit(0)}`);
|
||||
B.line(`${capacity} = fptoui double ${slots} to ${host.sizeType}`);
|
||||
return capacity;
|
||||
}
|
||||
|
||||
export function emitContainerExpr(
|
||||
host: LlvmEmitterContext,
|
||||
e: ExprOf<
|
||||
@@ -1017,7 +1045,34 @@ export function emitContainerExpr(
|
||||
? `${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)}`);
|
||||
} else {
|
||||
const hint = host.reservedArrayLengths.get(e);
|
||||
if (hint !== undefined) {
|
||||
host.reservedArrayLengths.delete(e);
|
||||
host.consumedReserves.add(hint);
|
||||
const capacity = reservedCapacity(host, hint.length);
|
||||
// The small construction's inline slots hold at least
|
||||
// SMALL_ARRAY_SLOTS elements; larger capacities are sized.
|
||||
const fits = B.tmp(),
|
||||
smallArr = B.tmp(),
|
||||
sizedArr = B.tmp();
|
||||
const smallLabel = B.newLabel("arr.cap.small"),
|
||||
sizedLabel = B.newLabel("arr.cap.sized"),
|
||||
joined = B.newLabel("arr.cap.j");
|
||||
B.line(`${fits} = icmp ule ${host.sizeType} ${capacity}, ${SMALL_ARRAY_SLOTS}`);
|
||||
B.condBr(fits, smallLabel, sizedLabel);
|
||||
B.startBlock(smallLabel);
|
||||
B.line(`${smallArr} = ${arrNewCall(host.shapeHost, elem, "0", true)}`);
|
||||
B.br(joined);
|
||||
B.startBlock(sizedLabel);
|
||||
B.line(`${sizedArr} = ${arrNewCall(host.shapeHost, elem, capacity)}`);
|
||||
B.br(joined);
|
||||
B.startBlock(joined);
|
||||
B.line(
|
||||
`${arr} = phi ptr [ ${smallArr}, %${smallLabel} ], [ ${sizedArr}, %${sizedLabel} ]`,
|
||||
);
|
||||
} else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
|
||||
}
|
||||
const out = host.own({ name: arr, type: e.type });
|
||||
const acc: ArrayAccess = int32 ? "i32" : elemAccess(elem);
|
||||
let pending: LlValue[] = [];
|
||||
|
||||
@@ -796,13 +796,16 @@ export function streamTypedRefMaterializeAdapter(
|
||||
t: IrType,
|
||||
): LlStreamTypedRefAdapter {
|
||||
const key = typeKey(t);
|
||||
const existing = host.liveDynRefAdapters.get(key);
|
||||
// A `(T | undefined)[]` stored as T elements shares T[]'s capsule type,
|
||||
// but its adapters also store undefined elements.
|
||||
const adapterKey = t.kind === "array" && t.optionalElements ? `${key}?` : key;
|
||||
const existing = host.liveDynRefAdapters.get(adapterKey);
|
||||
if (existing) return existing;
|
||||
// The converter depends only on the static type. Intern the complete
|
||||
// graph across roots and union arms so recursive types emit once.
|
||||
const snapshot = `sc_ldr_${host.liveDynRefAdapters.size}_materialize`;
|
||||
const adapter: LlStreamTypedRefAdapter = { snapshot, commit: "null" };
|
||||
host.liveDynRefAdapters.set(key, adapter);
|
||||
host.liveDynRefAdapters.set(adapterKey, adapter);
|
||||
host.journal?.allocated(
|
||||
snapshot.replace(/_materialize$/, ""),
|
||||
"streamTypedRefMaterializeAdapter",
|
||||
@@ -1331,6 +1334,18 @@ function liveArrayOps(
|
||||
G.terminate(`ret ptr ${boxed}`);
|
||||
|
||||
const S = new BlockBuilder();
|
||||
if (t.optionalElements) {
|
||||
// A `(T | undefined)[]` stores undefined as a present undefined.
|
||||
const undef = host.dyn.isUndefined(S, "%v");
|
||||
const storeUndefined = S.newLabel("live.array.undefined");
|
||||
const other = S.newLabel("live.array.value");
|
||||
S.condBr(undef, storeUndefined, other);
|
||||
S.startBlock(storeUndefined);
|
||||
host.declare(`declare void @scr_arr_set_undefined(ptr, double)`);
|
||||
S.line(`call void @scr_arr_set_undefined(ptr %p, double %i)`);
|
||||
S.terminate(`ret i1 true`);
|
||||
S.startBlock(other);
|
||||
}
|
||||
const fits = S.tmp();
|
||||
S.line(`${fits} = call zeroext i1 @${host.dyn.dynMatchHelper(elem)}(ptr %v)`);
|
||||
const store = S.newLabel("live.array.store");
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { ForkPrimitives } from "./emitter.js";
|
||||
|
||||
/** Starts `compute` on a forked child process, which inherits this process
|
||||
* as it is at the fork, and returns the join that answers its result. The
|
||||
* child writes `encode`'s text to a file and exits; the join waits for it
|
||||
* and decodes the text. When the fork fails, the child fails, or `encode`
|
||||
* answers null, the join runs `compute` here instead, so the result does
|
||||
* not depend on the child. Join once. */
|
||||
export function forkedAnalysis<T>(
|
||||
fork: ForkPrimitives,
|
||||
compute: () => T,
|
||||
encode: (value: T) => string | null,
|
||||
decode: (text: string) => T,
|
||||
): () => T {
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-analysis-"));
|
||||
const file = join(dir, "result.json");
|
||||
const pid = fork.fork();
|
||||
if (pid === 0) {
|
||||
let code = 1;
|
||||
try {
|
||||
const text = encode(compute());
|
||||
if (text !== null) {
|
||||
writeFileSync(file, text);
|
||||
code = 0;
|
||||
}
|
||||
} catch {
|
||||
// The parent computes the result itself.
|
||||
}
|
||||
fork.exit(code);
|
||||
}
|
||||
return () => {
|
||||
let text: string | null = null;
|
||||
try {
|
||||
if (pid > 0 && fork.wait(pid) === 0) text = readFileSync(file, "utf8");
|
||||
} catch {
|
||||
// The result is computed here.
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
return text === null ? compute() : decode(text);
|
||||
};
|
||||
}
|
||||
@@ -6,13 +6,16 @@
|
||||
*
|
||||
* load ordinary object whose shape is entry 0's own-property shape:
|
||||
* read entry `slot` inline unless it is an accessor; otherwise
|
||||
* scr_js_load_probe (polymorphic, prototype hits, misses)
|
||||
* scr_js_get_probe (polymorphic, prototype hits, absent keys,
|
||||
* misses)
|
||||
* store ordinary object whose shape is entry 0's replace shape, outside
|
||||
* an intact parse region, writable data entry: replace inline;
|
||||
* otherwise scr_js_store_probe (adds, polymorphic, misses)
|
||||
* keyed dense array element at an integral number key: inline;
|
||||
* otherwise scr_js_keyed_probe (string keys of ordinary objects)
|
||||
* method dynInvoke's method read: scr_js_load_probe
|
||||
* literal an object literal's constant-key member: scr_js_store_probe,
|
||||
* else scr_dyn_key_set
|
||||
*
|
||||
* A probe that cannot answer from plain data leaves everything untouched,
|
||||
* and the site calls its helper (or the generic runtime entry), which
|
||||
@@ -402,9 +405,10 @@ function emitGet(host: Host, e: ExprOf<"call">, callee: IrFunction, key: string)
|
||||
B.startBlock(object);
|
||||
inlineLoad(host, receiver.name, ic, INLINE_WAYS, INLINE_PROTO, (cell) => cell, hits, hit, probe);
|
||||
B.startBlock(probe);
|
||||
host.declare(`declare ptr @scr_js_load_probe(ptr, ptr, ptr)`);
|
||||
// The read's probe also answers keys the receiver's plain chain lacks.
|
||||
host.declare(`declare ptr @scr_js_get_probe(ptr, ptr, ptr)`);
|
||||
const probed = B.tmp();
|
||||
B.line(`${probed} = call ptr @scr_js_load_probe(ptr ${receiver.name}, ptr ${name}, ptr ${ic})`);
|
||||
B.line(`${probed} = call ptr @scr_js_get_probe(ptr ${receiver.name}, ptr ${name}, ptr ${ic})`);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = icmp ne ptr ${probed}, null`);
|
||||
const probeEnd = B.currentLabel();
|
||||
@@ -836,6 +840,27 @@ function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue {
|
||||
return host.own({ name: out, type: e.type });
|
||||
}
|
||||
|
||||
/** One member of an object literal (ir/js-ic.ts jsIcLiteralKey) added to
|
||||
* the fresh object `obj` through the site's store cache, else with
|
||||
* scr_dyn_key_set. The key and value are borrowed. */
|
||||
export function emitJsIcLiteralMember(host: Host, obj: string, key: string, value: string): void {
|
||||
const B = host.B;
|
||||
const ic = host.jsIcSite();
|
||||
host.declare(`declare zeroext i1 @scr_js_store_probe(ptr, ptr, ptr, ptr)`);
|
||||
const done = B.tmp();
|
||||
B.line(
|
||||
`${done} = call zeroext i1 @scr_js_store_probe(ptr ${obj}, ptr ${key}, ptr ${value}, ptr ${ic})`,
|
||||
);
|
||||
const slow = B.newLabel("icl.slow");
|
||||
const join = B.newLabel("icl.j");
|
||||
B.condBr(done, join, slow);
|
||||
B.startBlock(slow);
|
||||
B.line(`call void @scr_dyn_key_set(ptr ${obj}, ptr ${key}, ptr ${value})`);
|
||||
host.emitPendingCheck();
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
}
|
||||
|
||||
/** 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_id answers with the full dispatch). */
|
||||
|
||||
@@ -73,6 +73,26 @@ export function rawBytes(host: LlvmEmitterContext, value: LlValue): AbiArgument[
|
||||
return [ptr(data), { type: host.sizeType, name: length }];
|
||||
}
|
||||
|
||||
/** A TLS client's `ca` as rawBytes, with a null pointer for none: an empty
|
||||
* string, which Node reads as no ca (and which an omitted ca lowers to).
|
||||
* A Buffer's pointer is never null, so an empty one is an empty trust
|
||||
* store. */
|
||||
export function caBytes(host: LlvmEmitterContext, value: LlValue): AbiArgument[] {
|
||||
const B = host.B;
|
||||
const [data, length] = rawBytes(host, value);
|
||||
const test = B.tmp(),
|
||||
pointer = B.tmp();
|
||||
if (value.type.kind === "string") {
|
||||
B.line(`${test} = icmp eq ${host.sizeType} ${length!.name}, 0`);
|
||||
B.line(`${pointer} = select i1 ${test}, ptr null, ptr ${data!.name}`);
|
||||
} else {
|
||||
// An empty Buffer's data pointer may be null; its object stands in.
|
||||
B.line(`${test} = icmp eq ptr ${data!.name}, null`);
|
||||
B.line(`${pointer} = select i1 ${test}, ptr ${value.name}, ptr ${data!.name}`);
|
||||
}
|
||||
return [ptr(pointer), length!];
|
||||
}
|
||||
|
||||
export function callbackType(value: LlValue): IrType & { kind: "func" } {
|
||||
if (value.type.kind !== "func")
|
||||
throw new InternalCompilerError("runtime callback must be a function");
|
||||
|
||||
@@ -22,9 +22,10 @@ import {
|
||||
} from "./lib-shared.js";
|
||||
import { isDynUndefined } from "../../ir/runtime-features.js";
|
||||
import { DYN_KIND } from "./dyn.js";
|
||||
import { rawBytes } from "./lib-abi.js";
|
||||
import { caBytes } from "./lib-abi.js";
|
||||
import { emitInlineAtomicsOp } from "./expr-bytes.js";
|
||||
import { regexLiteralCaptures } from "./regex-matchers.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
|
||||
/** ScrDyn.v.typed_ref.type_key_len: the low 27 bits of its bit-field word. */
|
||||
const TYPE_KEY_LEN_MASK = (1 << 27) - 1;
|
||||
@@ -595,7 +596,8 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
// The https URL row is the http one with the TLS entry point — same
|
||||
// three arguments, same response-callback adapter. The https options
|
||||
// row is wider: rejectUnauthorized stays an i1, while its ScrStr or
|
||||
// ScrBytes CA value expands to the runtime's raw pointer + length.
|
||||
// ScrBytes CA value expands to the runtime's raw pointer + length
|
||||
// (caBytes: a null pointer for none).
|
||||
const isTls = e.fn.startsWith("https.");
|
||||
const isUrl = e.fn.includes("requestUrl");
|
||||
const isTlsOptions = isTls && !isUrl;
|
||||
@@ -629,7 +631,7 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
if (isTlsOptions) {
|
||||
callArgs = [
|
||||
...callArgs.slice(0, 8),
|
||||
...rawBytes(host, args[8]!).map((arg) => `${arg.type} ${arg.name}`),
|
||||
...caBytes(host, args[8]!).map((arg) => `${arg.type} ${arg.name}`),
|
||||
];
|
||||
host.declare(
|
||||
`declare ptr @scr_https_request(ptr, double, ptr, ptr, double, ptr, i1 zeroext, i1 zeroext, ptr, ${host.sizeType}, ptr, ptr)`,
|
||||
@@ -841,35 +843,68 @@ function emitAssertFailing(
|
||||
* `a.name.localeCompare(b.name)` reads both fields without retaining). */
|
||||
const READ_ONLY_STRING_LIB_FNS: ReadonlySet<string> = new Set(["str.localeCompare"]);
|
||||
|
||||
/** The parameter count of a replace callback the runtime can call through
|
||||
* the closure ABI (scr_regex_replace_fn), or null when it needs the dyn
|
||||
* box. Each declared parameter must receive a value of its declared kind
|
||||
* in every match: the match string at 0; with more than one parameter the
|
||||
/** The most parameters scr_regex_replace_fn calls a replacer with
|
||||
* (SCR_REPLACER_MAX_ARITY), the first bit of its string-or-undefined
|
||||
* parameters (SCR_REPLACER_OPTIONAL), and its flag for a checked-dynamic
|
||||
* result (SCR_REPLACER_BOXED_RESULT). */
|
||||
const REPLACER_MAX_ARITY = 6;
|
||||
const REPLACER_OPTIONAL = 2 ** 8;
|
||||
const REPLACER_BOXED_RESULT = 2 ** 31;
|
||||
|
||||
/** `string | undefined` held as the string pointer, NULL for undefined. */
|
||||
function optionalStringType(host: LlvmEmitterContext, type: IrType): boolean {
|
||||
if (type.kind !== "union") return false;
|
||||
const nullable = host.nullableUnions.get(type.unionId);
|
||||
if (nullable?.repr !== "single" || nullable.arm.kind !== "string" || nullable.nullTag >= 0)
|
||||
return false;
|
||||
return host.unionsById.get(type.unionId)?.arms[nullable.unitTag]?.kind === "undefinedT";
|
||||
}
|
||||
|
||||
/** The parameter count and kinds (scr_regex_replace_fn's `kinds`) of a
|
||||
* replace callback the runtime can call through the closure ABI, or null
|
||||
* when it needs the dyn box. A checked-dynamic parameter receives whatever
|
||||
* Node passes at its position. Each typed parameter must receive a value of
|
||||
* its declared kind in every match: the match string at 0; past 0 the
|
||||
* pattern must be a literal whose captures are known, so position i is a
|
||||
* capture that every match sets (a string), the offset (a number) right
|
||||
* after the captures, or the subject (a string) after that. */
|
||||
function typedReplacerArity(callback: IrType, pattern: IrExpr): number | null {
|
||||
* capture (a string when every match sets it, otherwise `string |
|
||||
* undefined`), the offset (a number) right after the captures, or the
|
||||
* subject (a string) after that. The result is a string or a
|
||||
* checked-dynamic value. */
|
||||
function typedReplacerShape(
|
||||
host: LlvmEmitterContext,
|
||||
callback: IrType,
|
||||
pattern: IrExpr,
|
||||
): { arity: number; kinds: number } | null {
|
||||
if (callback.kind !== "func" || callback.rest || callback.argumentsAll) return null;
|
||||
if (callback.ret.kind !== "string") return null;
|
||||
if (callback.ret.kind !== "string" && callback.ret.kind !== "dyn") return null;
|
||||
const params = callback.params;
|
||||
if (params.length > 4) return null;
|
||||
if (params.length === 0) return 0;
|
||||
if (params[0]!.kind !== "string") return null;
|
||||
if (params.length === 1) return 1;
|
||||
if (pattern.kind !== "regexLit") return null;
|
||||
const captures = regexLiteralCaptures(pattern.pattern, pattern.flags);
|
||||
if (!captures) return null;
|
||||
for (let i = 1; i < params.length; i++) {
|
||||
if (params.length > REPLACER_MAX_ARITY) return null;
|
||||
let kinds = callback.ret.kind === "dyn" ? REPLACER_BOXED_RESULT : 0;
|
||||
let captures: { count: number; always: boolean[] } | null | undefined;
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
const kind = params[i]!.kind;
|
||||
if (kind === "dyn") {
|
||||
kinds += 2 ** i;
|
||||
continue;
|
||||
}
|
||||
const optional = optionalStringType(host, params[i]!);
|
||||
if (i === 0) {
|
||||
if (kind !== "string") return null;
|
||||
continue;
|
||||
}
|
||||
if (pattern.kind !== "regexLit") return null;
|
||||
captures ??= regexLiteralCaptures(pattern.pattern, pattern.flags);
|
||||
if (!captures) return null;
|
||||
const ok =
|
||||
i < captures.count
|
||||
? kind === "string" && captures.always[i] === true
|
||||
? (kind === "string" && captures.always[i] === true) || optional
|
||||
: i === captures.count
|
||||
? kind === "f64"
|
||||
: i === captures.count + 1 && kind === "string";
|
||||
if (!ok) return null;
|
||||
if (optional) kinds += REPLACER_OPTIONAL * 2 ** i;
|
||||
}
|
||||
return params.length;
|
||||
return { arity: params.length, kinds };
|
||||
}
|
||||
|
||||
/** `s.replace(re, fn)` / `replaceAll` with a typed callback the runtime can
|
||||
@@ -879,16 +914,16 @@ function typedReplacerArity(callback: IrType, pattern: IrExpr): number | null {
|
||||
function emitTypedRegexReplace(host: LlvmEmitterContext, e: LibCallExpr): LlValue | null {
|
||||
const [subject, pattern, callback, all] = e.args;
|
||||
if (!subject || !pattern || callback?.kind !== "dynFrom" || all?.kind !== "boolLit") return null;
|
||||
const arity = typedReplacerArity(callback.value.type, pattern);
|
||||
if (arity === null) return null;
|
||||
const shape = typedReplacerShape(host, callback.value.type, pattern);
|
||||
if (shape === null) return null;
|
||||
const B = host.B;
|
||||
const s = host.emitExpr(subject);
|
||||
const re = host.emitExpr(pattern);
|
||||
const fn = host.emitExpr(callback.value);
|
||||
host.declare(`declare ptr @scr_regex_replace_fn(ptr, ptr, ptr, i32, i1 zeroext)`);
|
||||
host.declare(`declare ptr @scr_regex_replace_fn(ptr, ptr, ptr, i32, i32, i1 zeroext)`);
|
||||
const result = B.tmp();
|
||||
B.line(
|
||||
`${result} = call ptr @scr_regex_replace_fn(ptr ${s.name}, ptr ${re.name}, ptr ${fn.name}, i32 ${arity}, i1 zeroext ${all.value})`,
|
||||
`${result} = call ptr @scr_regex_replace_fn(ptr ${s.name}, ptr ${re.name}, ptr ${fn.name}, i32 ${shape.arity}, i32 ${shape.kinds | 0}, i1 zeroext ${all.value})`,
|
||||
);
|
||||
const output = host.own({ name: result, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
@@ -1154,6 +1189,36 @@ function emitArrayForeignThrow(host: LlvmEmitterContext, e: LibCallExpr): LlValu
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The settlement adapter of Promise.all's tuple form for the builder
|
||||
* `fnName`, which fulfills the result with the builder's tuple. */
|
||||
function allTupleAdapterFor(host: LlvmEmitterContext, fnName: string, tuple: IrType): string {
|
||||
const key = `allTuple:${fnName}`;
|
||||
let adapter = host.resolveThunks.get(key);
|
||||
if (adapter !== undefined) return adapter;
|
||||
adapter = `sc_all_tuple_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, adapter);
|
||||
host.journal?.allocated(adapter, "allTupleAdapterFor", [fnName, tuple]);
|
||||
const rc = vAdapters(host.shapeHost, tuple);
|
||||
host.declare(`declare ptr @scr_promise_payload_ref(ptr)`);
|
||||
host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`);
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal void @${adapter}(ptr %dst, ptr %src) ${FN_ATTRS} { ; Promise.all tuple`,
|
||||
`entry:`,
|
||||
` %entries = call ptr @scr_promise_payload_ref(ptr %src) ; +1, moves into the builder`,
|
||||
` %tuple = call ptr @${host.callTarget(fnName)}(ptr %entries)`,
|
||||
` call void @scr_promise_fulfill_ref(ptr %dst, ptr %tuple, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, tuple)})`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
return adapter;
|
||||
}
|
||||
|
||||
registerReplay("allTupleAdapterFor", (host, args) =>
|
||||
allTupleAdapterFor(host, args[0] as string, args[1] as IrType),
|
||||
);
|
||||
|
||||
/** Promise.all's tuple form (promise.allTuple): the combinator over the
|
||||
* entries stores each fulfilled entry promise, and a settlement adapter on
|
||||
* the combinator calls the builder with them and fulfills the result with
|
||||
@@ -1166,26 +1231,7 @@ function emitPromiseAllTuple(host: LlvmEmitterContext, e: LibCallExpr): LlValue
|
||||
throw new InternalCompilerError("llvm emitter bug: promise.allTuple shape");
|
||||
const entriesT = e.args[0]!.type;
|
||||
const entries = host.emitExpr(e.args[0]!);
|
||||
const tuple = e.type.inner;
|
||||
const key = `allTuple:${build.fnName}`;
|
||||
let adapter = host.resolveThunks.get(key);
|
||||
if (adapter === undefined) {
|
||||
adapter = `sc_all_tuple_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, adapter);
|
||||
const rc = vAdapters(host.shapeHost, tuple);
|
||||
host.declare(`declare ptr @scr_promise_payload_ref(ptr)`);
|
||||
host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`);
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal void @${adapter}(ptr %dst, ptr %src) ${FN_ATTRS} { ; Promise.all tuple`,
|
||||
`entry:`,
|
||||
` %entries = call ptr @scr_promise_payload_ref(ptr %src) ; +1, moves into the builder`,
|
||||
` %tuple = call ptr @${host.callTarget(build.fnName)}(ptr %entries)`,
|
||||
` call void @scr_promise_fulfill_ref(ptr %dst, ptr %tuple, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, tuple)})`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
const adapter = allTupleAdapterFor(host, build.fnName, e.type.inner);
|
||||
host.declare(`declare ptr @scr_promise_all(ptr, ptr, ptr)`);
|
||||
host.declare(`declare void @scr_promise_all_store_promise(ptr, double, ptr)`);
|
||||
host.declare(`declare ptr @scr_promise_new()`);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
callbackType,
|
||||
finishRuntimeCall,
|
||||
ptr,
|
||||
caBytes,
|
||||
rawBytes,
|
||||
requestHandler,
|
||||
} from "./lib-abi.js";
|
||||
@@ -204,7 +205,7 @@ export function emitHttp2LibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVa
|
||||
);
|
||||
return finishRuntimeCall(host, e, "scr_http2_connect", [
|
||||
...a.slice(0, 2),
|
||||
...rawBytes(host, args[2]!),
|
||||
...caBytes(host, args[2]!),
|
||||
...handler,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js
|
||||
import { f64Lit } from "./common.js";
|
||||
import { mockTimerSym } from "./lib-shared.js";
|
||||
import { FN_ATTRS } from "./shapes.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
|
||||
export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
@@ -498,6 +499,7 @@ function boundCallbackThunk(host: LlvmEmitterContext, cbT: IrType & { kind: "fun
|
||||
if (sym) return sym;
|
||||
sym = `sc_alsbound_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "boundCallbackThunk", [cbT]);
|
||||
host.declare("declare ptr @scr_als_bound_target(ptr)");
|
||||
host.declare("declare ptr @scr_als_bound_enter(ptr)");
|
||||
host.declare("declare void @scr_als_bound_leave(ptr, ptr)");
|
||||
@@ -517,9 +519,14 @@ function boundCallbackThunk(host: LlvmEmitterContext, cbT: IrType & { kind: "fun
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
registerReplay("boundCallbackThunk", (host, args) =>
|
||||
boundCallbackThunk(host, args[0] as IrType & { kind: "func" }),
|
||||
);
|
||||
|
||||
export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
if (e.fn === "process.forkTarget") {
|
||||
|
||||
@@ -532,6 +532,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"fs.openSync": "scr_fs_open",
|
||||
"fs.openNumericSync": "scr_fs_open_numeric",
|
||||
"fs.readSync": "scr_fs_read_sync",
|
||||
"fs.readSyncDyn": "scr_fs_read_sync_dyn",
|
||||
"fs.writeSync": "scr_fs_write_sync",
|
||||
"fs.writeStrSync": "scr_fs_write_str_sync",
|
||||
"fs.closeSync": "scr_fs_close",
|
||||
@@ -1104,6 +1105,28 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"crypto.verifyOneshot": "scr_crypto_verify_oneshot",
|
||||
"crypto.createSign": "scr_crypto_create_sign",
|
||||
"crypto.createVerify": "scr_crypto_create_verify",
|
||||
"crypto.x509Raw": "scr_crypto_x509_raw",
|
||||
"crypto.x509RawDer": "scr_crypto_x509_raw_der",
|
||||
"crypto.x509RawStr": "scr_crypto_x509_raw_str",
|
||||
"crypto.x509Subject": "scr_crypto_x509_subject",
|
||||
"crypto.x509Issuer": "scr_crypto_x509_issuer",
|
||||
"crypto.x509SubjectAltName": "scr_crypto_x509_subject_alt_name",
|
||||
"crypto.x509InfoAccess": "scr_crypto_x509_info_access",
|
||||
"crypto.x509SerialNumber": "scr_crypto_x509_serial_number",
|
||||
"crypto.x509Fingerprint256": "scr_crypto_x509_fingerprint256",
|
||||
"crypto.x509Fingerprint512": "scr_crypto_x509_fingerprint512",
|
||||
"crypto.x509Ca": "scr_crypto_x509_ca",
|
||||
"crypto.x509SignatureAlgorithm": "scr_crypto_x509_signature_algorithm",
|
||||
"crypto.x509SignatureAlgorithmOid": "scr_crypto_x509_signature_algorithm_oid",
|
||||
"crypto.x509CheckHost": "scr_crypto_x509_check_host",
|
||||
"crypto.x509CheckEmail": "scr_crypto_x509_check_email",
|
||||
"crypto.x509CheckIP": "scr_crypto_x509_check_ip",
|
||||
"crypto.x509CheckIssued": "scr_crypto_x509_check_issued",
|
||||
"crypto.x509LegacyObject": "scr_crypto_x509_legacy_object",
|
||||
"crypto.x509Pem": "scr_crypto_x509_pem",
|
||||
"crypto.x509PublicKey": "scr_crypto_x509_public_key",
|
||||
"crypto.x509Verify": "scr_crypto_x509_verify",
|
||||
"crypto.x509CheckPrivateKey": "scr_crypto_x509_check_private_key",
|
||||
"fs.writeFileModeSync": "scr_fs_write_file_mode",
|
||||
"fs.chownSync": "scr_fs_chown",
|
||||
"fs.lchownSync": "scr_fs_lchown",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
finishRuntimeCall,
|
||||
nullableReference,
|
||||
ptr,
|
||||
caBytes,
|
||||
rawBytes,
|
||||
requestHandler,
|
||||
} from "./lib-abi.js";
|
||||
@@ -110,6 +111,31 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu
|
||||
return finishRuntimeCall(host, e, "scr_tls_sock_get_cipher", a);
|
||||
case "tls.sockAlpnProtocol":
|
||||
return finishRuntimeCall(host, e, "scr_tls_sock_alpn_protocol", a);
|
||||
case "tls.serverAddContext":
|
||||
return finishRuntimeCall(host, e, "scr_tls_server_add_context", [...a, ptr()]);
|
||||
case "tls.serverAddContextCtx":
|
||||
return finishRuntimeCall(host, e, "scr_tls_server_add_context", [a[0]!, a[1]!, ptr(), a[2]!]);
|
||||
case "tls.serverSetSecureContext":
|
||||
return finishRuntimeCall(host, e, "scr_tls_server_set_secure_context", a);
|
||||
case "tls.serverOnClientError": {
|
||||
// (err, tlsSocket) listeners by arity: the adapter retains what the
|
||||
// listener takes.
|
||||
const cb = args[1]!;
|
||||
const thunk = `scr_net_client_error_thunk${callbackType(cb).params.length}`;
|
||||
return finishRuntimeCall(host, e, "scr_net_server_on_client_error", [
|
||||
a[0]!,
|
||||
...callbackAdapter(host, cb, thunk, ["ptr", "ptr", "ptr"]),
|
||||
a[2]!,
|
||||
]);
|
||||
}
|
||||
case "tls.sockGetPeerCertificate":
|
||||
return finishRuntimeCall(host, e, "scr_tls_sock_get_peer_certificate", a);
|
||||
case "tls.sockGetCertificate":
|
||||
return finishRuntimeCall(host, e, "scr_tls_sock_get_certificate", a);
|
||||
case "tls.sockPeerCertDer":
|
||||
return finishRuntimeCall(host, e, "scr_tls_sock_peer_cert_der", a);
|
||||
case "tls.sockOwnCertDer":
|
||||
return finishRuntimeCall(host, e, "scr_tls_sock_own_cert_der", a);
|
||||
case "tls.sockAuthError": {
|
||||
const result = callRuntime(host, "scr_tls_sock_auth_error", "ptr", a);
|
||||
return nullableReference(host, e.type, result, "string", "nullT");
|
||||
@@ -159,7 +185,7 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu
|
||||
);
|
||||
return finishRuntimeCall(host, e, "scr_https_request_agent", [
|
||||
...a.slice(0, 8),
|
||||
...rawBytes(host, args[8]!),
|
||||
...caBytes(host, args[8]!),
|
||||
a[9]!,
|
||||
...handler,
|
||||
]);
|
||||
@@ -179,7 +205,7 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu
|
||||
["ptr", "ptr"],
|
||||
);
|
||||
// Evaluate arguments once before selecting which transport consumes the callback.
|
||||
const pem = rawBytes(host, args[9]!);
|
||||
const pem = caBytes(host, args[9]!);
|
||||
const B = host.B,
|
||||
tls = B.newLabel("request.tls"),
|
||||
plain = B.newLabel("request.plain"),
|
||||
|
||||
@@ -150,14 +150,16 @@ export function emitStringMapGetScalar(
|
||||
}
|
||||
|
||||
/** Map.get of a reference value: a +1 value, or null when absent — the
|
||||
* contract of scr_map_get_f64_ref. */
|
||||
* contract of scr_map_get_f64_ref. `borrow`: the entry's own reference,
|
||||
* without a retain, for a reader that finishes before the map can change. */
|
||||
export function emitNumberMapGetRef(
|
||||
host: LlvmEmitterContext,
|
||||
map: string,
|
||||
key: string,
|
||||
value: IrType,
|
||||
borrow = false,
|
||||
): string {
|
||||
return emitMapEntryGetRef(host, emitNumberMapEntry(host, map, key), value);
|
||||
return emitMapEntryGetRef(host, emitNumberMapEntry(host, map, key), value, false, borrow);
|
||||
}
|
||||
|
||||
/** Map.get of a reference value under a string key, with scr_map_get_str_ref's
|
||||
@@ -169,16 +171,25 @@ export function emitStringMapGetRef(
|
||||
key: string,
|
||||
value: IrType,
|
||||
compact = false,
|
||||
borrow = false,
|
||||
): string {
|
||||
return emitMapEntryGetRef(host, emitStringMapEntry(host, map, key, compact), value, compact);
|
||||
return emitMapEntryGetRef(
|
||||
host,
|
||||
emitStringMapEntry(host, map, key, compact),
|
||||
value,
|
||||
compact,
|
||||
borrow,
|
||||
);
|
||||
}
|
||||
|
||||
/** The +1 value of a found entry, or null for a missing (null) entry. */
|
||||
/** The +1 value of a found entry (its own reference when `borrow`), or
|
||||
* null for a missing (null) entry. */
|
||||
function emitMapEntryGetRef(
|
||||
host: LlvmEmitterContext,
|
||||
entry: string,
|
||||
value: IrType,
|
||||
compact = false,
|
||||
borrow = false,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const found = B.tmp();
|
||||
@@ -188,10 +199,8 @@ function emitMapEntryGetRef(
|
||||
join = B.newLabel("mapn.join");
|
||||
B.condBr(found, hit, miss);
|
||||
B.startBlock(hit);
|
||||
const retained = host.retainFrame(
|
||||
compact ? emitCompactMapSlot(host, entry) : emitNumberMapSlot(host, entry, "ptr"),
|
||||
value,
|
||||
);
|
||||
const loaded = compact ? emitCompactMapSlot(host, entry) : emitNumberMapSlot(host, entry, "ptr");
|
||||
const retained = borrow ? loaded : host.retainFrame(loaded, value);
|
||||
B.br(join);
|
||||
B.startBlock(miss);
|
||||
B.br(join);
|
||||
|
||||
@@ -120,7 +120,12 @@ test("local map results keep scalar and owned reference payloads without heap wr
|
||||
expect(ir).toContain("call ptr @sc_map_entry_str(");
|
||||
expect(ir).not.toContain("@scr_map_get_");
|
||||
}
|
||||
if (value === STRING) expect(ir).toContain("@scr_str_release");
|
||||
// Nothing can change the map before the tag test, so a reference
|
||||
// result is the entry's own (findMapReadBorrows): no retain or release.
|
||||
if (value === STRING) {
|
||||
expect(ir).not.toContain("@scr_str_retain");
|
||||
expect(ir).not.toContain("@scr_str_release");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -132,7 +137,8 @@ test("nullable record results are the looked-up pointer itself on both ABIs", ()
|
||||
expect(ir).not.toContain("%ScrUnion");
|
||||
expect(ir).not.toContain("@scr_union_");
|
||||
expect(ir).toMatch(/icmp (?:eq|ne) ptr %\w+, null/);
|
||||
expect(ir).toContain("@sc_rrelease_");
|
||||
// The pointer is the entry's own reference until the tag test.
|
||||
expect(ir).not.toContain("@sc_rrelease_");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "../../ir/ir.js";
|
||||
import { everyExpr, everyStmt, everyStmtList } from "../../ir/traverse.js";
|
||||
import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { ReferenceEffects } from "./reference-effects.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import {
|
||||
emitNumberMapGetRef,
|
||||
@@ -69,6 +70,10 @@ export function borrowsMapMutationReceiver(expr: IrExpr): boolean {
|
||||
export interface LocalMapRead {
|
||||
/** The Map.get expression itself. */
|
||||
expr: IrExpr;
|
||||
/** A local's reference result may stay the entry's own reference: every
|
||||
* use of the local runs before anything that can change a reference
|
||||
* edge (findMapReadBorrows), so the entry still holds the value. */
|
||||
borrowed?: boolean;
|
||||
type: IrType;
|
||||
receiver: IrExpr;
|
||||
key: IrExpr;
|
||||
@@ -127,6 +132,7 @@ export function findMapReadLifetimes(
|
||||
fn: IrFunction,
|
||||
unions: ReadonlyMap<string, IrUnionDef>,
|
||||
lifetimes: CallLifetimes,
|
||||
effects?: ReferenceEffects,
|
||||
): MapReadLifetimes {
|
||||
const result: MapReadLifetimes = { locals: new Map(), arguments: new Map() };
|
||||
if (fn.async || fn.generator) return result;
|
||||
@@ -151,6 +157,125 @@ export function findMapReadLifetimes(
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (effects) {
|
||||
const borrowed = findMapReadBorrows(fn, effects, (decl) => {
|
||||
const read = result.locals.get(decl.localId);
|
||||
return read !== undefined && isRefCounted(read.value);
|
||||
});
|
||||
for (const id of borrowed) result.locals.get(id)!.borrowed = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Whether a map read's receiver keeps the map past its own statement: a
|
||||
* local or a field of one. Changing either is an edge change the borrow
|
||||
* proof sees. A temporary map (a call's result, `new Map()`) is released at
|
||||
* the end of the declaration, and its entries with it. */
|
||||
function heldReceiver(expr: IrExpr): boolean {
|
||||
if (expr.kind === "varRef") return true;
|
||||
if (expr.kind === "fieldGet" || expr.kind === "recordGet") return heldReceiver(expr.obj);
|
||||
if (expr.kind === "unionNarrow" || expr.kind === "downcast") return heldReceiver(expr.value);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The locals, declared by statements `candidate` accepts, that can hold a
|
||||
* map entry's own reference instead of a +1 copy. The statements after the
|
||||
* declaration run in order, and every one that names the local must
|
||||
* preserve reference edges (ReferenceEffects) and run before any statement
|
||||
* that may not, so no write, deletion or unknown call can release the value
|
||||
* while the local is read. An `if` passes its condition's state to both
|
||||
* branches and joins them; any other statement that names the local is
|
||||
* judged whole. A later pass of an enclosing loop declares the local again,
|
||||
* so only the rest of its own statement list matters. The map must be held
|
||||
* by a local or a field (heldReceiver), the read itself must preserve
|
||||
* reference edges (a key whose evaluation replaces the receiver's field
|
||||
* leaves the read on a map the statement releases), and locals declared
|
||||
* from the local count as its uses. An update such as
|
||||
* `const c = m.get(k); if (c === undefined) m.set(k, v); else c.n++` keeps
|
||||
* the borrow: the write runs only on the branch that does not read `c`. */
|
||||
export function findMapReadBorrows(
|
||||
fn: IrFunction,
|
||||
effects: ReferenceEffects,
|
||||
candidate: (decl: IrStmt & { kind: "varDecl" }) => boolean,
|
||||
): Set<string> {
|
||||
const result = new Set<string>();
|
||||
if (fn.async || fn.generator) return result;
|
||||
// `ids` holds the local and the locals declared from it: a stable alias
|
||||
// (`const d = c`) reads the same entry without a reference of its own.
|
||||
const names = (node: IrStmt | IrExpr, ids: ReadonlySet<string>, isStmt: boolean): boolean => {
|
||||
let found = false;
|
||||
const visitor = {
|
||||
stmt: () => !found,
|
||||
expr: (expr: IrExpr) => {
|
||||
if (expr.kind === "varRef" && ids.has(expr.localId)) found = true;
|
||||
return !found;
|
||||
},
|
||||
};
|
||||
if (isStmt) everyStmt(node as IrStmt, visitor);
|
||||
else everyExpr(node as IrExpr, visitor);
|
||||
return found;
|
||||
};
|
||||
// Adds the reference locals that `stmt` declares from one in `ids`, at any
|
||||
// depth: a `var` inside a loop or a try stays in scope after it.
|
||||
const alias = (stmt: IrStmt, ids: Set<string>): void => {
|
||||
everyStmt(stmt, {
|
||||
stmt: (node) => {
|
||||
if (
|
||||
node.kind === "varDecl" &&
|
||||
node.init !== null &&
|
||||
isRefCounted(node.init.type) &&
|
||||
names(node.init, ids, false)
|
||||
)
|
||||
ids.add(node.localId);
|
||||
return true;
|
||||
},
|
||||
expr: () => true,
|
||||
});
|
||||
};
|
||||
// Whether an edge may have changed after `list`, or null when a statement
|
||||
// names the local after such a change (or changes edges itself).
|
||||
const walk = (list: readonly IrStmt[], ids: Set<string>, changed: boolean): boolean | null => {
|
||||
for (const stmt of list) {
|
||||
if (stmt.kind === "if") {
|
||||
const preserving = effects.preserves(stmt.cond);
|
||||
if (names(stmt.cond, ids, false) && (changed || !preserving)) return null;
|
||||
const condition = changed || !preserving;
|
||||
const then = walk(stmt.then, ids, condition);
|
||||
const otherwise = stmt.else_ === null ? condition : walk(stmt.else_, ids, condition);
|
||||
if (then === null || otherwise === null) return null;
|
||||
changed = then || otherwise;
|
||||
continue;
|
||||
}
|
||||
if (stmt.kind === "block") {
|
||||
const inner = walk(stmt.body, ids, changed);
|
||||
if (inner === null) return null;
|
||||
changed = inner;
|
||||
continue;
|
||||
}
|
||||
const preserving = effects.preservesScope([stmt]);
|
||||
if (names(stmt, ids, true)) {
|
||||
if (changed || !preserving) return null;
|
||||
alias(stmt, ids);
|
||||
}
|
||||
changed ||= !preserving;
|
||||
}
|
||||
return changed;
|
||||
};
|
||||
const visit = (list: readonly IrStmt[]): void => {
|
||||
list.forEach((stmt, index) => {
|
||||
if (
|
||||
stmt.kind === "varDecl" &&
|
||||
stmt.init?.kind === "mapIntrinsic" &&
|
||||
heldReceiver(stmt.init.receiver) &&
|
||||
effects.preserves(stmt.init) &&
|
||||
candidate(stmt) &&
|
||||
walk(list.slice(index + 1), new Set([stmt.localId]), false) !== null
|
||||
)
|
||||
result.add(stmt.localId);
|
||||
for (const nested of nestedStmtLists(stmt)) visit(nested);
|
||||
});
|
||||
};
|
||||
visit(fn.body);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -305,13 +430,14 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead):
|
||||
let owner: StackMapRead["owner"] = null;
|
||||
const number = inlineNumberKey(key);
|
||||
const string = entryStringKey(key);
|
||||
const borrow = read.borrowed === true;
|
||||
if (number !== null) {
|
||||
if (isRefCounted(read.value)) {
|
||||
const raw = emitNumberMapGetRef(host, receiver.name, number, read.value);
|
||||
const raw = emitNumberMapGetRef(host, receiver.name, number, read.value, borrow);
|
||||
B.line(`${found} = icmp ne ptr ${raw}, null`);
|
||||
B.line(`store i64 0, ptr ${payload}`);
|
||||
B.line(`store ptr ${raw}, ptr ${payload}`);
|
||||
owner = { slot: payload, type: read.value };
|
||||
if (!borrow) owner = { slot: payload, type: read.value };
|
||||
} else {
|
||||
// Booleans are stored as 0/1, already the widened union payload.
|
||||
const scalar = emitNumberMapGetScalar(host, receiver.name, number);
|
||||
@@ -329,8 +455,10 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead):
|
||||
let raw: string;
|
||||
const mapType = read.receiver.type;
|
||||
const compact = mapType.kind === "map" && compactMapValues(host, mapType.key, mapType.value);
|
||||
// The runtime's other key kinds return +1, which the frame then owns.
|
||||
const borrowed = borrow && stringKey !== null;
|
||||
if (stringKey !== null)
|
||||
raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value, compact);
|
||||
raw = emitStringMapGetRef(host, receiver.name, stringKey, read.value, compact, borrowed);
|
||||
else {
|
||||
raw = B.tmp();
|
||||
host.declare(`declare ptr @scr_map_get_${access}_ref(ptr, ${keyType})`);
|
||||
@@ -339,7 +467,7 @@ export function emitStackMapRead(host: LlvmEmitterContext, read: LocalMapRead):
|
||||
B.line(`${found} = icmp ne ptr ${raw}, null`);
|
||||
B.line(`store i64 0, ptr ${payload}`);
|
||||
B.line(`store ptr ${raw}, ptr ${payload}`);
|
||||
owner = { slot: payload, type: read.value };
|
||||
if (!borrowed) owner = { slot: payload, type: read.value };
|
||||
} else {
|
||||
const scalar = read.value.kind === "f64" ? "f64" : "bool";
|
||||
B.line(`store i64 0, ptr ${payload}`);
|
||||
|
||||
@@ -187,6 +187,37 @@ function exactPowerOfTwo(value: number): number | null {
|
||||
return k >= 0 && k <= 52 && 2 ** k === value ? k : null;
|
||||
}
|
||||
|
||||
/** The same speculated operand written twice: a local, a literal, or a
|
||||
* power of two of the same exponent. */
|
||||
function sameOperand(a: IrExpr, b: IrExpr): boolean {
|
||||
if (a.kind === "varRef") return b.kind === "varRef" && a.localId === b.localId;
|
||||
if (a.kind === "numLit") return b.kind === "numLit" && Object.is(a.value, b.value);
|
||||
return (
|
||||
a.kind === "libCall" &&
|
||||
b.kind === "libCall" &&
|
||||
a.fn === "math.pow" &&
|
||||
b.fn === "math.pow" &&
|
||||
powerOfTwo(a) &&
|
||||
powerOfTwo(b) &&
|
||||
sameOperand(a.args[1]!, b.args[1]!)
|
||||
);
|
||||
}
|
||||
|
||||
/** `a - Math.floor(a / p) * p` (either factor order) with p a power of two
|
||||
* and `a` a local or literal: the floor-mod of `a` by p. */
|
||||
function floorModPowerOfTwo(e: IrExpr): { value: IrExpr; power: IrExpr } | null {
|
||||
if (e.kind !== "bin" || e.op !== "-" || e.right.kind !== "bin" || e.right.op !== "*") return null;
|
||||
const { left, right } = e.right;
|
||||
const [floor, power] =
|
||||
left.kind === "libCall" && left.fn === "math.floor" ? [left, right] : [right, left];
|
||||
if (floor.kind !== "libCall" || floor.fn !== "math.floor" || floor.args.length !== 1) return null;
|
||||
const quotient = floor.args[0]!;
|
||||
if (quotient.kind !== "bin" || quotient.op !== "/") return null;
|
||||
if (!powerOfTwo(power) || !sameOperand(quotient.right, power)) return null;
|
||||
if (e.left.kind !== "varRef" && e.left.kind !== "numLit") return null;
|
||||
return sameOperand(quotient.left, e.left) ? { value: e.left, power } : null;
|
||||
}
|
||||
|
||||
/** Whether `e` is a power of two speculation can shift by:
|
||||
* `Math.pow(2, n)` or a literal 2^k with k in [0, 52]. */
|
||||
function powerOfTwo(e: IrExpr): boolean {
|
||||
@@ -950,6 +981,37 @@ export function emitSpeculativeInteger(
|
||||
}
|
||||
case "bin": {
|
||||
if (e.op !== "+" && e.op !== "-" && e.op !== "*" && e.op !== "%") break;
|
||||
// The floor-mod of a safe integer by 2^n (n in [0, 52]) is its low n
|
||||
// bits, also for a negative value: in [0, 2^n), and never -0.
|
||||
const mod = floorModPowerOfTwo(e);
|
||||
if (mod) {
|
||||
const value = emitSpeculativeInteger(host, mod.value, bail, reads, integral);
|
||||
const n = exponentOf(host, mod.power, bail, reads, integral);
|
||||
const mask = B.tmp(),
|
||||
low = B.tmp();
|
||||
B.line(`${mask} = sub i64 ${shiftOne(host, n)}, 1`);
|
||||
B.line(`${low} = and i64 ${value}, ${mask}`);
|
||||
return low;
|
||||
}
|
||||
// A product with 2^n shifts: it is safe iff the other factor's
|
||||
// magnitude is at most (2^53 - 1) >> n, and a zero product of a safe
|
||||
// integer and a positive power is +0.
|
||||
if (e.op === "*" && (powerOfTwo(e.right) || powerOfTwo(e.left))) {
|
||||
const [factor, power] = powerOfTwo(e.right) ? [e.left, e.right] : [e.right, e.left];
|
||||
const value = emitSpeculativeInteger(host, factor, bail, reads, integral);
|
||||
const n = exponentOf(host, power, bail, reads, integral);
|
||||
const limit = B.tmp(),
|
||||
magnitude = B.tmp(),
|
||||
outside = B.tmp(),
|
||||
product = B.tmp();
|
||||
B.line(`${limit} = ashr i64 ${SAFE}, ${n}`);
|
||||
host.declare("declare i64 @llvm.abs.i64(i64, i1)");
|
||||
B.line(`${magnitude} = call i64 @llvm.abs.i64(i64 ${value}, i1 true)`);
|
||||
B.line(`${outside} = icmp ugt i64 ${magnitude}, ${limit}`);
|
||||
bail(outside);
|
||||
B.line(`${product} = shl nsw i64 ${value}, ${n}`);
|
||||
return product;
|
||||
}
|
||||
const a = emitSpeculativeInteger(host, e.left, bail, reads, integral);
|
||||
const b = emitSpeculativeInteger(host, e.right, bail, reads, integral);
|
||||
const result = B.tmp();
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import { splitLlvmLibraryProgram, splitLlvmProgram } from "./split.js";
|
||||
import { splitLlvmLibraryProgram, splitLlvmProgram, splitLlvmProgramFile } from "./split.js";
|
||||
import { LlvmDebugInfo } from "./debug-info.js";
|
||||
import { VOID } from "../../ir/ir.js";
|
||||
|
||||
@@ -61,17 +61,17 @@ test("splits generated LLVM functions and promotes only private cross-shard defi
|
||||
expect(actual!.promotedSymbols).not.toContain("public_entry");
|
||||
expect(actual!.publicSymbols).toEqual(["public_value", "public_entry"]);
|
||||
expect(actual!.shards[0]!.name).toBe("program-globals.ll");
|
||||
expect(actual!.shards[0]!.source).toContain("@hidden_value = hidden global i64 7");
|
||||
expect(actual!.shards[0]!.source()).toContain("@hidden_value = hidden global i64 7");
|
||||
expect(
|
||||
actual!.shards
|
||||
.slice(1)
|
||||
.every((shard) => shard.source.includes("@hidden_value = external hidden global i64")),
|
||||
.every((shard) => shard.source().includes("@hidden_value = external hidden global i64")),
|
||||
).toBe(true);
|
||||
expect(
|
||||
actual!.shards.some(
|
||||
(shard) =>
|
||||
!shard.source.includes("define hidden i64 @right() #0") &&
|
||||
shard.source.includes("declare hidden i64 @right() #0"),
|
||||
!shard.source().includes("define hidden i64 @right() #0") &&
|
||||
shard.source().includes("declare hidden i64 @right() #0"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
@@ -82,9 +82,115 @@ test("thread-local definitions and declarations preserve their storage model acr
|
||||
"@hidden_value = internal thread_local global i64 7",
|
||||
);
|
||||
const split = splitLlvmProgram(tls, { minimumBytes: 0, targetBytes: 64 * 1024 })!;
|
||||
expect(split.shards[0]!.source).toContain("@hidden_value = hidden thread_local global i64 7");
|
||||
expect(split.shards[0]!.source()).toContain("@hidden_value = hidden thread_local global i64 7");
|
||||
for (const shard of split.shards.slice(1)) {
|
||||
expect(shard.source).toContain("@hidden_value = external hidden thread_local global i64");
|
||||
expect(shard.source()).toContain("@hidden_value = external hidden thread_local global i64");
|
||||
}
|
||||
});
|
||||
|
||||
const PADDING = Array.from(
|
||||
{ length: 80 },
|
||||
(_, i) =>
|
||||
`define internal i64 @pad_${i}() #0 {\nentry:\n ; ${"x".repeat(2048)}\n ret i64 ${i}\n}\n`,
|
||||
).join("\n");
|
||||
|
||||
/** SAMPLE with more globals after @public_value and enough functions to
|
||||
* split into several shards. */
|
||||
function withGlobals(globals: string, tail = ""): string {
|
||||
return `${SAMPLE.replace(
|
||||
"@public_value = constant i64 11",
|
||||
`@public_value = constant i64 11\n${globals}`,
|
||||
).replace("define i64 @public_entry", `${PADDING}\ndefine i64 @public_entry`)}${tail}`;
|
||||
}
|
||||
|
||||
/** Writes each shard and compiles it with clang. */
|
||||
async function compileShards(
|
||||
shards: readonly { name: string; source: () => string }[],
|
||||
): Promise<void> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-llvm-split-shards-"));
|
||||
scratch.push(dir);
|
||||
for (const shard of shards) {
|
||||
const source = join(dir, shard.name);
|
||||
await writeFile(source, shard.source());
|
||||
execFileSync("clang", ["-Wno-override-module", "-c", source, "-o", `${source}.o`]);
|
||||
}
|
||||
}
|
||||
|
||||
test("private unnamed_addr lookup tables are promoted like internal globals", async () => {
|
||||
const split = splitLlvmProgram(
|
||||
withGlobals('@table = private unnamed_addr constant [2 x i8] c"\\01\\02"'),
|
||||
{ minimumBytes: 0, targetBytes: 64 * 1024 },
|
||||
)!;
|
||||
expect(split).not.toBeNull();
|
||||
const [globals, ...functions] = split.shards;
|
||||
expect(globals!.source()).toContain('@table = hidden unnamed_addr constant [2 x i8] c"\\01\\02"');
|
||||
for (const shard of functions)
|
||||
expect(shard.source()).toContain("@table = external hidden constant [2 x i8]");
|
||||
expect(split.promotedSymbols).toContain("table");
|
||||
await compileShards(split.shards);
|
||||
});
|
||||
|
||||
test("aggregate initializers over several lines stay whole in the globals shard", async () => {
|
||||
// The regex global's comment holds unbalanced brackets.
|
||||
const split = splitLlvmProgram(
|
||||
withGlobals(`@regex = internal global { ptr, i64 } { ptr @left, i64 0 } ; /(a|[b/
|
||||
@vt = internal constant [2 x { i64, ptr }] [
|
||||
{ i64, ptr } { i64 1, ptr @left },
|
||||
{ i64, ptr } { i64 2, ptr @right }
|
||||
]`),
|
||||
{ minimumBytes: 0, targetBytes: 64 * 1024 },
|
||||
)!;
|
||||
expect(split).not.toBeNull();
|
||||
const [globals, ...functions] = split.shards;
|
||||
expect(globals!.source()).toContain(
|
||||
"@vt = hidden constant [2 x { i64, ptr }] [\n { i64, ptr } { i64 1, ptr @left },\n { i64, ptr } { i64 2, ptr @right }\n]",
|
||||
);
|
||||
for (const shard of functions) {
|
||||
expect(shard.source()).toContain("@vt = external hidden constant [2 x { i64, ptr }]");
|
||||
expect(shard.source()).not.toContain("{ i64, ptr } { i64 1");
|
||||
}
|
||||
expect(split.promotedSymbols).toEqual(expect.arrayContaining(["regex", "vt"]));
|
||||
await compileShards(split.shards);
|
||||
});
|
||||
|
||||
test("globals after the attributes and metadata join the globals shard", async () => {
|
||||
const split = splitLlvmProgram(withGlobals("", "@flag = internal constant i1 true\n"), {
|
||||
minimumBytes: 0,
|
||||
targetBytes: 64 * 1024,
|
||||
})!;
|
||||
expect(split).not.toBeNull();
|
||||
const [globals, ...functions] = split.shards;
|
||||
expect(globals!.source()).toContain("@flag = hidden constant i1 true");
|
||||
for (const shard of functions)
|
||||
expect(shard.source()).toContain("@flag = external hidden constant i1");
|
||||
await compileShards(split.shards);
|
||||
});
|
||||
|
||||
test("a module split from its file has the shards of the same module split in memory", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-llvm-split-file-"));
|
||||
scratch.push(dir);
|
||||
const options = { minimumBytes: 0, targetBytes: 64 * 1024 };
|
||||
const storage = withGlobals(
|
||||
`@table = private unnamed_addr constant [2 x i8] c"\\01\\02"
|
||||
@vt = internal constant [2 x { i64, ptr }] [
|
||||
{ i64, ptr } { i64 1, ptr @left },
|
||||
{ i64, ptr } { i64 2, ptr @right }
|
||||
]`,
|
||||
"@flag = internal constant i1 true\n",
|
||||
);
|
||||
for (const [name, source] of [
|
||||
["sample.ll", withGlobals("")],
|
||||
["storage.ll", storage],
|
||||
] as const) {
|
||||
const path = join(dir, name);
|
||||
await writeFile(path, source);
|
||||
const fromFile = splitLlvmProgramFile(path, options)!;
|
||||
expect(fromFile).not.toBeNull();
|
||||
const materialize = (split: NonNullable<typeof fromFile>) => ({
|
||||
...split,
|
||||
shards: split.shards.map((shard) => ({ name: shard.name, source: shard.source() })),
|
||||
});
|
||||
expect(materialize(fromFile)).toEqual(materialize(splitLlvmProgram(source, options)!));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -127,7 +233,7 @@ entry:
|
||||
for (const shard of split.shards) {
|
||||
const path = join(dir, shard.name);
|
||||
const object = `${path}.o`;
|
||||
await writeFile(path, shard.source);
|
||||
await writeFile(path, shard.source());
|
||||
execFileSync("clang", ["-Wno-override-module", "-O2", "-c", path, "-o", object]);
|
||||
objects.push(object);
|
||||
}
|
||||
@@ -169,10 +275,10 @@ test("shared metadata bounds shard growth while preserving every definition", ()
|
||||
expect(split).not.toBeNull();
|
||||
expect(split!.shards.length).toBeGreaterThan(1);
|
||||
expect(
|
||||
split!.shards.reduce((bytes, shard) => bytes + Buffer.byteLength(shard.source), 0),
|
||||
split!.shards.reduce((bytes, shard) => bytes + Buffer.byteLength(shard.source()), 0),
|
||||
).toBeLessThanOrEqual(2 * Buffer.byteLength(source));
|
||||
const definitions = split!.shards.flatMap((shard) =>
|
||||
[...shard.source.matchAll(/^define (?:hidden )?i64 @(\w+)\(/gm)].map((match) => match[1]),
|
||||
[...shard.source().matchAll(/^define (?:hidden )?i64 @(\w+)\(/gm)].map((match) => match[1]),
|
||||
);
|
||||
expect(definitions.sort()).toEqual([...names, "left", "right", "public_entry"].sort());
|
||||
expect(split!.publicSymbols).toEqual(["public_value", "public_entry"]);
|
||||
@@ -207,9 +313,9 @@ test("debug metadata stays on definitions when splitting LLVM modules", async ()
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-debug-split-"));
|
||||
scratch.push(dir);
|
||||
for (const shard of split!.shards) {
|
||||
expect(shard.source).not.toMatch(/^declare.*!dbg/m);
|
||||
expect(shard.source()).not.toMatch(/^declare.*!dbg/m);
|
||||
const path = join(dir, shard.name);
|
||||
await writeFile(path, shard.source);
|
||||
await writeFile(path, shard.source());
|
||||
const result = spawnSync("clang", ["-Wno-override-module", "-c", path, "-o", `${path}.o`], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
@@ -250,7 +356,7 @@ test("every shard compiles and its merged object exposes only canonical public d
|
||||
for (const shard of split.shards) {
|
||||
const source = join(dir, shard.name);
|
||||
const object = `${source}.o`;
|
||||
await writeFile(source, shard.source);
|
||||
await writeFile(source, shard.source());
|
||||
execFileSync("clang", ["-Wno-override-module", "-c", source, "-o", object]);
|
||||
objects.push(object);
|
||||
}
|
||||
|
||||
@@ -19,10 +19,15 @@
|
||||
* canonical TU normally — cache optimization must never become correctness.
|
||||
*/
|
||||
|
||||
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
||||
|
||||
export interface LlvmProgramShard {
|
||||
/** Stable source identity used in object cache keys and diagnostics. */
|
||||
name: string;
|
||||
source: string;
|
||||
/** Assembles the shard's LLVM text. A module split from a file reads its
|
||||
* function bodies back from there on each call, so only the shards being
|
||||
* written are in memory. */
|
||||
source: () => string;
|
||||
}
|
||||
|
||||
export interface LlvmProgramSplit {
|
||||
@@ -54,12 +59,20 @@ const LIBRARY_TARGET_BYTES = 768 * 1024;
|
||||
const LIBRARY_MINIMUM_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
interface FunctionDef {
|
||||
source: string;
|
||||
/** UTF-8 length of the definition's text. */
|
||||
bytes: number;
|
||||
/** The definition's text: its header (promoted to hidden linkage when
|
||||
* internal), body and closing brace. A definition left in a file reads
|
||||
* its body through `read`. */
|
||||
text: (read: RangeReader) => string;
|
||||
declaration: string;
|
||||
symbol: string;
|
||||
promoted: boolean;
|
||||
}
|
||||
|
||||
/** Reads the text between two byte offsets of the module file. */
|
||||
type RangeReader = (start: number, end: number) => string;
|
||||
|
||||
interface GlobalDef {
|
||||
line: string;
|
||||
declaration: string | null;
|
||||
@@ -72,50 +85,68 @@ function symbolOf(spelling: string): string | null {
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function functionDefAt(
|
||||
lines: readonly string[],
|
||||
start: number,
|
||||
): { def: FunctionDef; end: number } | null {
|
||||
const header = lines[start]!;
|
||||
/** A definition's header line: the line its shard compiles, the
|
||||
* declaration every other shard carries, and its linkage. */
|
||||
function functionHeader(
|
||||
header: string,
|
||||
): { line: string; declaration: string; symbol: string; promoted: boolean } | null {
|
||||
const match =
|
||||
/^define\s+(internal\s+)?(.+?)\s+(@[-$._A-Za-z][-$.\w]*)\((.*)\)(.*)\{(?:\s*;.*)?$/.exec(
|
||||
header,
|
||||
);
|
||||
if (!match) return null;
|
||||
let end = start + 1;
|
||||
while (end < lines.length && lines[end] !== "}") end++;
|
||||
if (end >= lines.length) return null;
|
||||
const symbol = symbolOf(match[3]!);
|
||||
if (symbol === null) return null;
|
||||
const promoted = match[1] !== undefined;
|
||||
const linkage = promoted ? "hidden " : "";
|
||||
const definitionHeader = promoted
|
||||
? header.replace(/^define internal /, "define hidden ")
|
||||
: header;
|
||||
const source = [definitionHeader, ...lines.slice(start + 1, end + 1)].join("\n");
|
||||
const line = promoted ? header.replace(/^define internal /, "define hidden ") : header;
|
||||
// Parameter names are legal on declarations. Preserve the emitted text so
|
||||
// attributes, zeroext, and varargs stay byte-exact.
|
||||
// A definition's DISubprogram cannot be attached to a declaration in a
|
||||
// different shard. Its owning definition retains the original metadata.
|
||||
const attrs = match[5]!.replace(/\s+!dbg !\d+/g, "");
|
||||
const declaration = `declare ${linkage}${match[2]} ${match[3]}(${match[4]})${attrs}`.trimEnd();
|
||||
return { def: { source, declaration, symbol, promoted }, end };
|
||||
return { line, declaration, symbol, promoted };
|
||||
}
|
||||
|
||||
function functionDefAt(
|
||||
lines: readonly string[],
|
||||
start: number,
|
||||
): { def: FunctionDef; end: number } | null {
|
||||
const header = functionHeader(lines[start]!);
|
||||
if (header === null) return null;
|
||||
let end = start + 1;
|
||||
while (end < lines.length && lines[end] !== "}") end++;
|
||||
if (end >= lines.length) return null;
|
||||
const source = [header.line, ...lines.slice(start + 1, end + 1)].join("\n");
|
||||
return {
|
||||
def: {
|
||||
bytes: Buffer.byteLength(source),
|
||||
text: () => source,
|
||||
declaration: header.declaration,
|
||||
symbol: header.symbol,
|
||||
promoted: header.promoted,
|
||||
},
|
||||
end,
|
||||
};
|
||||
}
|
||||
|
||||
function globalDef(line: string): GlobalDef | null {
|
||||
const match =
|
||||
/^(@[-$._A-Za-z][-$.\w]*)\s+=\s+(internal\s+)?(thread_local(?:\([^)]*\))?\s+)?(global|constant)\s+(.+)$/.exec(
|
||||
/^(@[-$._A-Za-z][-$.\w]*)\s+=\s+((?:internal|private)\s+)?(thread_local(?:\([^)]*\))?\s+)?((?:local_)?unnamed_addr\s+)?(global|constant)\s+([\s\S]+)$/.exec(
|
||||
line,
|
||||
);
|
||||
if (!match) return null;
|
||||
const symbol = symbolOf(match[1]!);
|
||||
if (symbol === null) return null;
|
||||
// Internal and private definitions (such as private unnamed_addr lookup
|
||||
// tables) are local to their module: shards reach them as hidden ones.
|
||||
const promoted = match[2] !== undefined;
|
||||
const threadLocal = match[3] === undefined ? "" : `${match[3].trim()} `;
|
||||
// The declaration needs only the complete type. Emitted initializers begin
|
||||
// after it; the generated LLVM subset uses first-class scalars/pointers or
|
||||
// balanced aggregate types, so one small scanner is sufficient.
|
||||
const rest = match[5]!;
|
||||
const rest = match[6]!;
|
||||
let depth = 0;
|
||||
let quote = false;
|
||||
let typeEnd = -1;
|
||||
@@ -133,13 +164,30 @@ function globalDef(line: string): GlobalDef | null {
|
||||
if (typeEnd < 0) return null;
|
||||
const type = rest.slice(0, typeEnd);
|
||||
return {
|
||||
line: promoted ? line.replace(`${match[1]} = internal `, `${match[1]} = hidden `) : line,
|
||||
declaration: `${match[1]} = external ${promoted ? "hidden " : ""}${threadLocal}${match[4]} ${type}`,
|
||||
line: promoted ? line.replace(`${match[1]} = ${match[2]}`, `${match[1]} = hidden `) : line,
|
||||
declaration: `${match[1]} = external ${promoted ? "hidden " : ""}${threadLocal}${match[5]} ${type}`,
|
||||
symbol,
|
||||
promoted,
|
||||
};
|
||||
}
|
||||
|
||||
/** The change in aggregate nesting depth over one line of LLVM text,
|
||||
* outside string constants and comments (a regex global's comment spells
|
||||
* its source). */
|
||||
function bracketDepth(line: string): number {
|
||||
let depth = 0;
|
||||
let quote = false;
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const ch = line[i]!;
|
||||
if (ch === '"') quote = !quote;
|
||||
if (quote) continue;
|
||||
if (ch === ";") break;
|
||||
if (ch === "{" || ch === "[" || ch === "<" || ch === "(") depth++;
|
||||
else if (ch === "}" || ch === "]" || ch === ">" || ch === ")") depth--;
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
function stableBucket(symbol: string, count: number): number {
|
||||
let hash = 0x811c9dc5;
|
||||
for (let i = 0; i < symbol.length; i++) {
|
||||
@@ -154,7 +202,7 @@ function chunksOf(
|
||||
targetBytes: number,
|
||||
maximumBuckets: number,
|
||||
): { bucket: number; functions: FunctionDef[] }[] {
|
||||
const totalBytes = functions.reduce((sum, fn) => sum + Buffer.byteLength(fn.source) + 2, 0);
|
||||
const totalBytes = functions.reduce((sum, fn) => sum + fn.bytes + 2, 0);
|
||||
// A power-of-two bucket count changes only when the program crosses a wide
|
||||
// size band. Ordinary edits therefore keep every function in the same
|
||||
// symbol-hash bucket; ceil(total/target) would reshuffle the whole program
|
||||
@@ -168,6 +216,74 @@ function chunksOf(
|
||||
);
|
||||
}
|
||||
|
||||
/** A module's top-level forms, as the splitter classifies them. */
|
||||
interface ModuleParts {
|
||||
preamble: string[];
|
||||
globals: GlobalDef[];
|
||||
functions: FunctionDef[];
|
||||
trailer: string[];
|
||||
}
|
||||
|
||||
/** Classifies a module one line at a time. `define` lines go to
|
||||
* `onFunction`, which takes the following lines through the definition's
|
||||
* closing brace; every other line is offered to `line`. */
|
||||
class ModuleReader {
|
||||
readonly parts: ModuleParts = { preamble: [], globals: [], functions: [], trailer: [] };
|
||||
private sawDefinition = false;
|
||||
private inTrailer = false;
|
||||
/** A global definition whose aggregate initializer continues over the
|
||||
* following lines (vtable arrays list one element per line). */
|
||||
private pendingGlobal: { text: string; depth: number } | null = null;
|
||||
failed = false;
|
||||
|
||||
/** Whether a definition may start here (none may follow the trailer). */
|
||||
startFunction(): boolean {
|
||||
if (this.inTrailer || this.pendingGlobal !== null) this.failed = true;
|
||||
this.sawDefinition = true;
|
||||
return !this.failed;
|
||||
}
|
||||
|
||||
line(line: string): void {
|
||||
if (this.pendingGlobal !== null) {
|
||||
this.pendingGlobal.text += "\n" + line;
|
||||
this.pendingGlobal.depth += bracketDepth(line);
|
||||
if (this.pendingGlobal.depth <= 0) {
|
||||
const { text } = this.pendingGlobal;
|
||||
this.pendingGlobal = null;
|
||||
this.global(text);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (line.startsWith("@") && line.includes(" = ")) {
|
||||
const depth = bracketDepth(line);
|
||||
if (depth > 0) this.pendingGlobal = { text: line, depth };
|
||||
else this.global(line);
|
||||
return;
|
||||
}
|
||||
if (line.startsWith("attributes ") || line.startsWith("!")) this.inTrailer = true;
|
||||
if (this.inTrailer) this.parts.trailer.push(line);
|
||||
else if (!this.sawDefinition || line.trim() !== "") this.parts.preamble.push(line);
|
||||
}
|
||||
|
||||
/** Globals may follow the attributes and metadata (the feature flags the
|
||||
* link-features step appends); they are module storage like any other. */
|
||||
private global(definition: string): void {
|
||||
const parsed = globalDef(definition);
|
||||
if (parsed === null) {
|
||||
// External declarations are already valid in every shard.
|
||||
if (/^@.+\s+=\s+external\s+/.test(definition)) this.parts.preamble.push(definition);
|
||||
else this.failed = true;
|
||||
} else {
|
||||
this.parts.globals.push(parsed);
|
||||
this.sawDefinition = true;
|
||||
}
|
||||
}
|
||||
|
||||
finish(): ModuleParts | null {
|
||||
return this.failed || this.pendingGlobal !== null ? null : this.parts;
|
||||
}
|
||||
}
|
||||
|
||||
export function splitLlvmProgram(
|
||||
source: string,
|
||||
options: LlvmProgramSplitOptions = {},
|
||||
@@ -175,42 +291,129 @@ export function splitLlvmProgram(
|
||||
const minimumBytes = options.minimumBytes ?? DEFAULT_MINIMUM_BYTES;
|
||||
const sourceBytes = Buffer.byteLength(source);
|
||||
if (sourceBytes < minimumBytes) return null;
|
||||
const targetBytes = Math.max(64 * 1024, options.targetBytes ?? DEFAULT_TARGET_BYTES);
|
||||
const lines = source.split("\n");
|
||||
const preamble: string[] = [];
|
||||
const globals: GlobalDef[] = [];
|
||||
const functions: FunctionDef[] = [];
|
||||
const trailer: string[] = [];
|
||||
let sawDefinition = false;
|
||||
let inTrailer = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const reader = new ModuleReader();
|
||||
for (let i = 0; i < lines.length && !reader.failed; i++) {
|
||||
const line = lines[i]!;
|
||||
if (line.startsWith("define ")) {
|
||||
if (inTrailer) return null;
|
||||
if (!reader.startFunction()) break;
|
||||
const parsed = functionDefAt(lines, i);
|
||||
if (parsed === null) return null;
|
||||
functions.push(parsed.def);
|
||||
sawDefinition = true;
|
||||
reader.parts.functions.push(parsed.def);
|
||||
i = parsed.end;
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("@") && line.includes(" = ")) {
|
||||
if (inTrailer) return null;
|
||||
const parsed = globalDef(line);
|
||||
if (parsed === null) {
|
||||
// External declarations are already valid in every shard.
|
||||
if (/^@.+\s+=\s+external\s+/.test(line)) preamble.push(line);
|
||||
else return null;
|
||||
} else {
|
||||
globals.push(parsed);
|
||||
sawDefinition = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("attributes ") || line.startsWith("!")) inTrailer = true;
|
||||
if (inTrailer) trailer.push(line);
|
||||
else if (!sawDefinition || line.trim() !== "") preamble.push(line);
|
||||
} else reader.line(line);
|
||||
}
|
||||
const parts = reader.finish();
|
||||
return parts === null ? null : shardModule(parts, sourceBytes, options, null);
|
||||
}
|
||||
|
||||
/** The read size of splitLlvmProgramFile's scan. */
|
||||
const FILE_CHUNK_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
/** splitLlvmProgram over LLVM text already written to `path`, for modules
|
||||
* too large to hold in memory more than once (or past the longest string the
|
||||
* engine allows). Function bodies stay in the file: each shard's source
|
||||
* reads its bodies when it is read, so only the shards being written at a
|
||||
* time are in memory. */
|
||||
export function splitLlvmProgramFile(
|
||||
path: string,
|
||||
options: LlvmProgramSplitOptions = {},
|
||||
): LlvmProgramSplit | null {
|
||||
const minimumBytes = options.minimumBytes ?? DEFAULT_MINIMUM_BYTES;
|
||||
const sourceBytes = statSync(path).size;
|
||||
if (sourceBytes < minimumBytes) return null;
|
||||
const reader = new ModuleReader();
|
||||
// The definition being read: its header and where its body starts.
|
||||
let current: { header: NonNullable<ReturnType<typeof functionHeader>>; body: number } | null =
|
||||
null;
|
||||
const onLine = (bytes: Buffer, start: number, end: number, offset: number): void => {
|
||||
if (current !== null) {
|
||||
// Only the closing brace ends a definition; its body is not decoded.
|
||||
if (end - start === 1 && bytes[start] === 0x7d) {
|
||||
const { header, body } = current;
|
||||
const bodyEnd = offset + 1;
|
||||
reader.parts.functions.push({
|
||||
bytes: Buffer.byteLength(header.line) + 1 + (bodyEnd - body),
|
||||
text: (read) => `${header.line}\n${read(body, bodyEnd)}`,
|
||||
declaration: header.declaration,
|
||||
symbol: header.symbol,
|
||||
promoted: header.promoted,
|
||||
});
|
||||
current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const line = bytes.toString("utf8", start, end);
|
||||
if (line.startsWith("define ")) {
|
||||
const header = functionHeader(line);
|
||||
if (header === null || !reader.startFunction()) reader.failed = true;
|
||||
else current = { header, body: offset + (end - start) + 1 };
|
||||
} else reader.line(line);
|
||||
};
|
||||
const fd = openSync(path, "r");
|
||||
try {
|
||||
let carry: Buffer = Buffer.alloc(0);
|
||||
let carryOffset = 0;
|
||||
for (let position = 0; position < sourceBytes && !reader.failed;) {
|
||||
const chunk = Buffer.allocUnsafe(Math.min(FILE_CHUNK_BYTES, sourceBytes - position));
|
||||
const read = readSync(fd, chunk, 0, chunk.length, position);
|
||||
if (read === 0) break;
|
||||
const data =
|
||||
carry.length === 0
|
||||
? chunk.subarray(0, read)
|
||||
: Buffer.concat([carry, chunk.subarray(0, read)]);
|
||||
const dataOffset = carry.length === 0 ? position : carryOffset;
|
||||
position += read;
|
||||
let lineStart = 0;
|
||||
for (let newline = data.indexOf(10); newline >= 0; newline = data.indexOf(10, lineStart)) {
|
||||
onLine(data, lineStart, newline, dataOffset + lineStart);
|
||||
lineStart = newline + 1;
|
||||
}
|
||||
carry = Buffer.from(data.subarray(lineStart));
|
||||
carryOffset = dataOffset + lineStart;
|
||||
}
|
||||
// The text after the last newline is a line too (empty when the file
|
||||
// ends with one), as String.prototype.split answers it.
|
||||
if (!reader.failed) onLine(carry, 0, carry.length, carryOffset);
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
if (current !== null) return null;
|
||||
const parts = reader.finish();
|
||||
return parts === null ? null : shardModule(parts, sourceBytes, options, path);
|
||||
}
|
||||
|
||||
/** Runs `use` with a reader of `path`'s byte ranges (or of nothing, for a
|
||||
* module held in memory). */
|
||||
function withRangeReader<T>(path: string | null, use: (read: RangeReader) => T): T {
|
||||
if (path === null)
|
||||
return use(() => {
|
||||
throw new Error("an in-memory LLVM module has no file to read");
|
||||
});
|
||||
const fd = openSync(path, "r");
|
||||
try {
|
||||
return use((start, end) => {
|
||||
const bytes = Buffer.allocUnsafe(end - start);
|
||||
for (let done = 0; done < bytes.length;) {
|
||||
const read = readSync(fd, bytes, done, bytes.length - done, start + done);
|
||||
if (read === 0) throw new Error(`${path} ended before byte ${end}`);
|
||||
done += read;
|
||||
}
|
||||
return bytes.toString("utf8");
|
||||
});
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
}
|
||||
|
||||
function shardModule(
|
||||
parts: ModuleParts,
|
||||
sourceBytes: number,
|
||||
options: LlvmProgramSplitOptions,
|
||||
path: string | null,
|
||||
): LlvmProgramSplit | null {
|
||||
const { preamble, globals, functions, trailer } = parts;
|
||||
const targetBytes = Math.max(64 * 1024, options.targetBytes ?? DEFAULT_TARGET_BYTES);
|
||||
if (functions.length < 2) return null;
|
||||
const globalDecls = globals.flatMap((global) =>
|
||||
global.declaration === null ? [] : [global.declaration],
|
||||
@@ -230,16 +433,22 @@ export function splitLlvmProgram(
|
||||
const chunks = chunksOf(functions, targetBytes, 2 ** Math.floor(Math.log2(bucketLimit)));
|
||||
if (chunks.length < 2) return null;
|
||||
const tail = trailer.length === 0 ? "" : `\n${trailer.join("\n")}`;
|
||||
const functionShards = chunks.map(({ bucket, functions: chunk }) => {
|
||||
// A shard's source is assembled when it is read; a module split from a
|
||||
// file reads its function bodies from there each time.
|
||||
const functionShards = chunks.map(({ bucket, functions: chunk }): LlvmProgramShard => {
|
||||
const owned = new Set(chunk);
|
||||
const declarations = functions.filter((fn) => !owned.has(fn)).map((fn) => fn.declaration);
|
||||
return {
|
||||
name: `program-f${bucket.toString().padStart(3, "0")}.ll`,
|
||||
source: [
|
||||
[...preamble, ...globalDecls, ...declarations, ""].join("\n"),
|
||||
...chunk.map((fn) => fn.source),
|
||||
tail,
|
||||
].join("\n"),
|
||||
source: () => {
|
||||
const declarations = functions.filter((fn) => !owned.has(fn)).map((fn) => fn.declaration);
|
||||
return withRangeReader(path, (read) =>
|
||||
[
|
||||
[...preamble, ...globalDecls, ...declarations, ""].join("\n"),
|
||||
...chunk.map((fn) => fn.text(read)),
|
||||
tail,
|
||||
].join("\n"),
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
// Global initializer changes are common (literal text, generated tables)
|
||||
@@ -247,15 +456,15 @@ export function splitLlvmProgram(
|
||||
// all module storage in one dedicated shard that declares every function.
|
||||
const globalShard: LlvmProgramShard = {
|
||||
name: "program-globals.ll",
|
||||
source: [
|
||||
[...preamble, ...functions.map((fn) => fn.declaration), ""].join("\n"),
|
||||
...globals.map((global) => global.line),
|
||||
tail,
|
||||
].join("\n"),
|
||||
source: () =>
|
||||
[
|
||||
[...preamble, ...functions.map((fn) => fn.declaration), ""].join("\n"),
|
||||
...globals.map((global) => global.line),
|
||||
tail,
|
||||
].join("\n"),
|
||||
};
|
||||
const shards = [globalShard, ...functionShards];
|
||||
return {
|
||||
shards,
|
||||
shards: [globalShard, ...functionShards],
|
||||
promotedSymbols: [
|
||||
...globals.filter((global) => global.promoted).map((global) => global.symbol),
|
||||
...functions.filter((fn) => fn.promoted).map((fn) => fn.symbol),
|
||||
|
||||
@@ -478,7 +478,9 @@ test("string comparisons decide identity, lengths and leading bytes before the r
|
||||
expect(units).toContain("%below = icmp ult i8 %bx, %by");
|
||||
expect(units).toContain("%tailable = and i1 %samelen, %short");
|
||||
expect(units).toContain("%anyascii = icmp slt i32 %cws, 0");
|
||||
expect(units).toContain("call i32 @scr_str_cmp(ptr %a, ptr %b)");
|
||||
expect(units).toContain(
|
||||
"call i32 @scr_str_cmp(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)",
|
||||
);
|
||||
expect(units).toContain(
|
||||
"call i32 @scr_str_cmp_u16_bytes(ptr %a, ptr %b) nounwind willreturn memory(argmem: read)",
|
||||
);
|
||||
|
||||
@@ -25,9 +25,11 @@ import { literalCapWord, literalCapWord32 } from "./string-key-hash.js";
|
||||
export const STRING_UTF16_LEN_DECL = "declare double @scr_str_utf16_len(ptr)";
|
||||
export const STRING_CHAR_CODE_AT_DECL = "declare double @scr_str_char_code_at(ptr, double)";
|
||||
/** Converts a string to UTF-16 units and publishes it as the scan receiver
|
||||
* (bytes-loop-versions.ts). Like the fallbacks below, it can evict an
|
||||
* index entry, so it carries no memory restriction. */
|
||||
export const STRING_SCAN_UNITS_DECL = "declare void @scr_str_scan_units(ptr)";
|
||||
* when the loop about to read it can read enough of it (the other two
|
||||
* operands are the range of indexes it reads; bytes-loop-versions.ts).
|
||||
* Like the fallbacks below, it can evict an index entry, so it carries no
|
||||
* memory restriction. */
|
||||
export const STRING_SCAN_UNITS_DECL = "declare void @scr_str_scan_units_for(ptr, double, double)";
|
||||
/** The runtime fallbacks are cold: both common cases read inline. They
|
||||
* carry no
|
||||
* memory restriction: a fallback can evict an index entry, which resets
|
||||
|
||||
@@ -16,7 +16,7 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
* jsonStringify emission site runs the pending check (join still walks
|
||||
* only f64/string/bool/unit arms — the frontend's fence). */
|
||||
import type { IrRecordShape, IrType, IrUnionDef } from "../../ir/ir.js";
|
||||
import { DYN, isRefCounted, typeKey } from "../../ir/ir.js";
|
||||
import { isRefCounted, typeKey } from "../../ir/ir.js";
|
||||
import { jsonObjectKeyLabel } from "../json-literal.js";
|
||||
import { llvmBytes, wtf8Bytes } from "../literals.js";
|
||||
import { mangleRecordStruct } from "../mangle.js";
|
||||
@@ -534,14 +534,15 @@ export class LlWalkers {
|
||||
|
||||
/** Whether the walker for `t` can leave an exception pending: cyclic
|
||||
* values, records serialized through a kept checked source, and
|
||||
* checked-dynamic values (toJSON methods, getters, BigInts). */
|
||||
* checked-dynamic values (toJSON methods, getters, BigInts), including
|
||||
* the foreign elements of arrays that can hold them. */
|
||||
jsonWriteMayThrow(t: IrType, seen = new Set<string>()): boolean {
|
||||
if (traceAdapter(this.host, t) !== null) return true;
|
||||
switch (t.kind) {
|
||||
case "dyn":
|
||||
return true;
|
||||
case "array":
|
||||
return this.jsonWriteMayThrow(t.elem, seen);
|
||||
return this.host.mayHoldForeign(t.elem) || this.jsonWriteMayThrow(t.elem, seen);
|
||||
case "union":
|
||||
return (this.host.unionsById.get(t.unionId)?.arms ?? []).some((arm) =>
|
||||
this.jsonWriteMayThrow(arm, seen),
|
||||
@@ -752,7 +753,8 @@ export class LlWalkers {
|
||||
B.condBr(present, valueBlock, missingBlock);
|
||||
B.startBlock(missingBlock);
|
||||
if (host.mayHoldForeign(elem)) {
|
||||
// A foreign element (SCR_ARR_FOREIGN) serializes its own value.
|
||||
// A foreign element (SCR_ARR_FOREIGN) serializes its own value,
|
||||
// after its toJSON runs with the index as its key.
|
||||
const foreign = B.tmp();
|
||||
const foreignBlock = B.newLabel("jwa.foreign");
|
||||
const nullBlock = B.newLabel("jwa.null");
|
||||
@@ -763,7 +765,8 @@ export class LlWalkers {
|
||||
host.declare(`declare void @scr_dyn_release(ptr)`);
|
||||
const payload = B.tmp();
|
||||
B.line(`${payload} = call ptr @scr_arr_get_foreign(ptr %v, double ${i}) ; +1`);
|
||||
B.line(`call void @${this.jsonWriteHelper(DYN)}(ptr %b, ptr ${payload})`);
|
||||
host.declare(`declare void @scr_jb_put_dyn_element(ptr, ptr, double)`);
|
||||
B.line(`call void @scr_jb_put_dyn_element(ptr %b, ptr ${payload}, double ${i})`);
|
||||
B.line(`call void @scr_dyn_release(ptr ${payload})`);
|
||||
B.br(doneBlock);
|
||||
B.startBlock(nullBlock);
|
||||
|
||||
@@ -109,6 +109,9 @@ export interface NativeLinkFeatures {
|
||||
/** Ed25519 keys and signatures with pki: scr_ed25519.c (the vendored
|
||||
* libsodium Ed25519). */
|
||||
ed25519: boolean;
|
||||
/** X509Certificate fields beyond the validity window and fingerprint:
|
||||
* scr_x509.c. */
|
||||
x509: boolean;
|
||||
/** perf_hooks event-loop metrics: scr_perf_loop.c. */
|
||||
perfLoop: boolean;
|
||||
tlsCa: boolean;
|
||||
|
||||
@@ -3983,12 +3983,12 @@ test.skipIf(
|
||||
programShards: [
|
||||
{
|
||||
name: "program-f000.ll",
|
||||
source:
|
||||
source: () =>
|
||||
"define i32 @main() {\nentry:\n ret i32 7\n}\ndeclare hidden i64 @other_value()\n",
|
||||
},
|
||||
{
|
||||
name: "program-f001.ll",
|
||||
source:
|
||||
source: () =>
|
||||
"declare i32 @main()\ndefine hidden i64 @other_value() {\nentry:\n ret i64 9\n}\n",
|
||||
},
|
||||
],
|
||||
@@ -4038,11 +4038,11 @@ test.skipIf(!nativeShardMergeAvailable)(
|
||||
const programShards = [
|
||||
{
|
||||
name: "program-f000.ll",
|
||||
source: "define i64 @foo() {\nentry:\n ret i64 1\n}\ndeclare i64 @bar()\n",
|
||||
source: () => "define i64 @foo() {\nentry:\n ret i64 1\n}\ndeclare i64 @bar()\n",
|
||||
},
|
||||
{
|
||||
name: "program-f001.ll",
|
||||
source: "declare i64 @foo()\ndefine i64 @bar() {\nentry:\n ret i64 2\n}\n",
|
||||
source: () => "declare i64 @foo()\ndefine i64 @bar() {\nentry:\n ret i64 2\n}\n",
|
||||
},
|
||||
];
|
||||
try {
|
||||
@@ -4082,7 +4082,7 @@ test.skipIf(!nativeShardMergeAvailable)(
|
||||
// The canonical TU and ABI stay fixed, but the exact shard projection
|
||||
// changes. The merged-object tier must not reuse the previous layout.
|
||||
await build("bar", join(dir, "bar-commented.lib.a"), [
|
||||
{ ...programShards[0]!, source: `; projection edit\n${programShards[0]!.source}` },
|
||||
{ ...programShards[0]!, source: () => `; projection edit\n${programShards[0]!.source()}` },
|
||||
programShards[1]!,
|
||||
]);
|
||||
expect(await completeArtifacts(cacheRoot, "lib")).toHaveLength(3);
|
||||
@@ -4131,11 +4131,11 @@ test.skipIf(
|
||||
const programShards = [
|
||||
{
|
||||
name: "program-f000.ll",
|
||||
source: "define i64 @foo() {\nentry:\n ret i64 1\n}\ndeclare i64 @bar()\n",
|
||||
source: () => "define i64 @foo() {\nentry:\n ret i64 1\n}\ndeclare i64 @bar()\n",
|
||||
},
|
||||
{
|
||||
name: "program-f001.ll",
|
||||
source: "declare i64 @foo()\ndefine i64 @bar() {\nentry:\n ret i64 2\n}\n",
|
||||
source: () => "declare i64 @foo()\ndefine i64 @bar() {\nentry:\n ret i64 2\n}\n",
|
||||
},
|
||||
];
|
||||
const ldWrapper = (generation: string): string => `#!/bin/sh
|
||||
@@ -4227,8 +4227,8 @@ test.skipIf(
|
||||
// If compilation reaches either shard, this build fails. Missing ld
|
||||
// (and objcopy on Linux) must select the valid canonical TU instead.
|
||||
programShards: [
|
||||
{ name: "program-f000.ll", source: "not valid LLVM IR\n" },
|
||||
{ name: "program-f001.ll", source: "also not valid LLVM IR\n" },
|
||||
{ name: "program-f000.ll", source: () => "not valid LLVM IR\n" },
|
||||
{ name: "program-f001.ll", source: () => "also not valid LLVM IR\n" },
|
||||
],
|
||||
programPublicSymbols: ["public_value"],
|
||||
outPath,
|
||||
@@ -4293,12 +4293,12 @@ test.skipIf(
|
||||
programShards: [
|
||||
{
|
||||
name: "program-f000.ll",
|
||||
source:
|
||||
source: () =>
|
||||
"define i64 @public_value() {\nentry:\n ret i64 7\n}\ndeclare i64 @other_value()\n",
|
||||
},
|
||||
{
|
||||
name: "program-f001.ll",
|
||||
source:
|
||||
source: () =>
|
||||
"declare i64 @public_value()\ndefine i64 @other_value() {\nentry:\n ret i64 9\n}\n",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface CcOptions {
|
||||
windowsSubsystem?: WindowsSubsystem;
|
||||
/** Optional equivalent LLVM modules for dev compilation. Unsupported
|
||||
* targets or merge failures fall back to the canonical cPath TU. */
|
||||
programShards?: readonly { name: string; source: string }[];
|
||||
programShards?: readonly { name: string; source: () => string }[];
|
||||
/** Canonical externally visible definitions retained while shard merging
|
||||
* demotes generated cross-shard linkage back to local symbols. */
|
||||
programPublicSymbols?: readonly string[];
|
||||
@@ -275,6 +275,8 @@ export interface CcOptions {
|
||||
/** Ed25519 with pki: compiles scr_ed25519.c (the vendored libsodium
|
||||
* Ed25519). */
|
||||
ed25519?: boolean;
|
||||
/** X509Certificate fields: compiles scr_x509.c. */
|
||||
x509?: boolean;
|
||||
/** crypto.subtle importKey/sign/verify/digest: compiles scr_webcrypto.c. */
|
||||
webcrypto?: boolean;
|
||||
/** perf_hooks event-loop metrics: compiles scr_perf_loop.c. */
|
||||
@@ -304,7 +306,7 @@ export interface LibArchiveOptions {
|
||||
* `programSource` remains the canonical TU/cache identity; these stable
|
||||
* shards compile independently and are relocatably merged into one program
|
||||
* object before archive assembly. */
|
||||
programShards?: readonly { name: string; source: string }[];
|
||||
programShards?: readonly { name: string; source: () => string }[];
|
||||
/** Canonical externally visible definitions retained while the shard merge
|
||||
* demotes generated cross-shard linkage back to local symbols. */
|
||||
programPublicSymbols?: readonly string[];
|
||||
@@ -360,6 +362,8 @@ export interface LibArchiveOptions {
|
||||
searchParams?: boolean;
|
||||
emitter?: boolean;
|
||||
zlib?: boolean;
|
||||
/** X509Certificate fields: scr_x509.c. */
|
||||
x509?: boolean;
|
||||
copying?: boolean;
|
||||
textDecoderLegacy?: boolean;
|
||||
}
|
||||
|
||||
@@ -328,6 +328,9 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unit((u) => u.opts.webcrypto, "scr_webcrypto.c"),
|
||||
unit((u) => u.opts.webcrypto === true && u.opts.pki === true, "scr_webcrypto_pki.c"),
|
||||
unit((u) => u.opts.pki === true && u.opts.ed25519 === true, "scr_ed25519.c"),
|
||||
unit((u) => u.opts.x509 === true, "scr_x509.c"),
|
||||
// The TLS socket certificate accessors read scr_tls.c and build with scr_x509.c.
|
||||
unit((u) => u.tls && u.opts.x509 === true, "scr_tls_x509.c"),
|
||||
// The crypto global's object extends scr_async_dyn.c's.
|
||||
unit((u) => u.opts.webcrypto && u.opts.dynAsync, "scr_webcrypto_object.c"),
|
||||
// The CA-store unit rides its own gate OR the tls one: scr_tls.c
|
||||
@@ -433,7 +436,7 @@ export async function compileCInternal(
|
||||
? ["-gline-tables-only", ...(opts.cPath.endsWith(".ll") ? [] : ["-gno-column-info"])]
|
||||
: [];
|
||||
const { programShards, programPublicSymbols, programShardMergeIdentity } =
|
||||
await selectProgramShards(driver, opts, optimization === "dev" && !sanitize);
|
||||
await selectProgramShards(driver, opts, optimization === "dev");
|
||||
// Mobile triples produce library archives, never standalone executables:
|
||||
// the executable-lane runtime (event loop, sockets, child processes) is
|
||||
// not verified on those device classes. compile() reports the SC3002
|
||||
@@ -887,9 +890,15 @@ export async function compileCInternal(
|
||||
};
|
||||
const buildExecutable = async (
|
||||
runtimeInput: (path: string) => string,
|
||||
build: { programPath?: string; outPath?: string; compilerVisibleSource?: string } = {},
|
||||
build: {
|
||||
programPath?: string;
|
||||
outPath?: string;
|
||||
compilerVisibleSource?: string;
|
||||
/** programPath is an object already (the merged program shards). */
|
||||
programObject?: boolean;
|
||||
} = {},
|
||||
): Promise<void> => {
|
||||
if (!separateSanitizedProgram) {
|
||||
if (!separateSanitizedProgram || build.programObject === true) {
|
||||
await runClang(buildArgs(runtimeInput, build));
|
||||
return;
|
||||
}
|
||||
@@ -1468,7 +1477,7 @@ export async function compileCInternal(
|
||||
.update("\0")
|
||||
.update(shard.name)
|
||||
.update("\0")
|
||||
.update(shard.source)
|
||||
.update(shard.source())
|
||||
.digest("hex");
|
||||
return {
|
||||
...shard,
|
||||
@@ -1482,7 +1491,7 @@ export async function compileCInternal(
|
||||
for (let i = 0; i < entries.length; i += shardWidth) {
|
||||
await Promise.all(
|
||||
entries.slice(i, i + shardWidth).map(async (entry) => {
|
||||
await writeFile(entry.sourcePath, entry.source);
|
||||
await writeFile(entry.sourcePath, entry.source());
|
||||
if (await copyValidCachedFile(entry.cachePath, entry.staged)) return;
|
||||
entry.missed = true;
|
||||
await runClang([
|
||||
@@ -1569,7 +1578,7 @@ export async function compileCInternal(
|
||||
(p) => objects?.get(p) ?? p,
|
||||
shardedProgramObject === null
|
||||
? { programPath, outPath: privateOut, compilerVisibleSource: opts.cPath }
|
||||
: { programPath: shardedProgramObject, outPath: privateOut },
|
||||
: { programPath: shardedProgramObject, outPath: privateOut, programObject: true },
|
||||
);
|
||||
if (darwinDebugSymbols) {
|
||||
// clang invokes dsymutil when compiling a source TU while linking.
|
||||
|
||||
@@ -73,7 +73,7 @@ import { CacheInputsChangedError } from "./session.js";
|
||||
* async_free refusal already guarantees nothing references them — and
|
||||
* scr_library.c (sink, arena, reset registry, library funnel) joins. The gated
|
||||
* units a library may reach are the pure-data ones (regex + the vendored matcher, assert,
|
||||
* inspect, symbol, searchParams, emitter+dyn_handle, zlib); every
|
||||
* inspect, symbol, searchParams, emitter+dyn_handle, zlib, x509); every
|
||||
* loop-hooked or ambient unit was refused at SC4005 before emission.
|
||||
* External-symbol contract: undefined references only to the target's C/math
|
||||
* runtime and system APIs. Windows embedders additionally link advapi32,
|
||||
@@ -115,6 +115,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise<void>
|
||||
...(opts.dynInvoke && opts.dynBuffer ? ["scr_bytes_dyn.c"] : []),
|
||||
...(opts.dynTier === true ? ["scr_js_value.c", "scr_js_object.c"] : []),
|
||||
...(opts.zlib ? ["scr_zlib.c"] : []),
|
||||
...(opts.x509 ? ["scr_x509.c"] : []),
|
||||
...(opts.copying ? ["scr_copying.c"] : []),
|
||||
];
|
||||
const cflags = [
|
||||
@@ -484,7 +485,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise<void>
|
||||
.update("\0")
|
||||
.update(shard.name)
|
||||
.update("\0")
|
||||
.update(shard.source)
|
||||
.update(shard.source())
|
||||
.digest("hex");
|
||||
cachePath = join(persistentCache.root, "program-shard", key);
|
||||
}
|
||||
@@ -494,7 +495,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise<void>
|
||||
for (let i = 0; i < shardEntries.length; i += shardWidth) {
|
||||
await Promise.all(
|
||||
shardEntries.slice(i, i + shardWidth).map(async (entry) => {
|
||||
await writeFile(entry.sourcePath, entry.source);
|
||||
await writeFile(entry.sourcePath, entry.source());
|
||||
if (
|
||||
entry.cachePath !== null &&
|
||||
(await copyValidCachedFile(entry.cachePath, entry.staged))
|
||||
|
||||
@@ -7,7 +7,7 @@ import { rememberFingerprintDependencies } from "./dependency-files.js";
|
||||
|
||||
export function updateProgramShardCacheIdentity(
|
||||
hash: ReturnType<typeof createHash>,
|
||||
shards: readonly { name: string; source: string }[] | undefined,
|
||||
shards: readonly { name: string; source: () => string }[] | undefined,
|
||||
publicSymbols: readonly string[] | undefined,
|
||||
mergeIdentity: string | undefined,
|
||||
): void {
|
||||
@@ -17,7 +17,7 @@ export function updateProgramShardCacheIdentity(
|
||||
} else {
|
||||
hash.update("<present>\0");
|
||||
for (const shard of shards) {
|
||||
hash.update(shard.name).update("\0").update(shard.source).update("\0");
|
||||
hash.update(shard.name).update("\0").update(shard.source()).update("\0");
|
||||
}
|
||||
}
|
||||
hash.update("\0program-public-symbols\0");
|
||||
@@ -78,7 +78,7 @@ export async function resolveProgramShardMergeIdentity(driver: CcDriver): Promis
|
||||
}
|
||||
|
||||
/** Library builds admit shards in both optimization modes; executable builds
|
||||
* only admit unsanitized dev LLVM. Both use the same validated names, required
|
||||
* admit dev LLVM, sanitized or not. Both use the same validated names, required
|
||||
* public-symbol projection, target support, and canonical-TU fallback. */
|
||||
export async function selectProgramShards(
|
||||
driver: CcDriver,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* of the link contract: a program object keeps this symbol undefined and the
|
||||
* matching runtime defines it, so incompatible manual links fail before the
|
||||
* program can start. */
|
||||
export const RUNTIME_ABI_VERSION = 43 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v43" as const;
|
||||
export const RUNTIME_ABI_VERSION = 44 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v44" as const;
|
||||
|
||||
/** Object consumption is useful today, but the complete scr_* surface may
|
||||
* still change before 1.0. The versioned marker prevents accidental mixing;
|
||||
|
||||
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" },
|
||||
runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
|
||||
flavors: {
|
||||
@@ -129,6 +129,7 @@ const features = {
|
||||
dh: false,
|
||||
pki: false,
|
||||
ed25519: false,
|
||||
x509: false,
|
||||
webcrypto: false,
|
||||
perfLoop: false,
|
||||
};
|
||||
|
||||
@@ -65,6 +65,7 @@ const BASE: NativeLinkFeatures = {
|
||||
dh: false,
|
||||
pki: false,
|
||||
ed25519: false,
|
||||
x509: false,
|
||||
webcrypto: false,
|
||||
perfLoop: false,
|
||||
};
|
||||
@@ -128,7 +129,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 43, marker: "scr_runtime_abi_v43" },
|
||||
runtime_abi: { version: 44, marker: "scr_runtime_abi_v44" },
|
||||
compiler: {
|
||||
command: "clang",
|
||||
identity: "fixture clang",
|
||||
|
||||
@@ -71,6 +71,7 @@ export async function warmNativeCaches(
|
||||
dh: false,
|
||||
pki: false,
|
||||
ed25519: false,
|
||||
x509: false,
|
||||
webcrypto: false,
|
||||
perfLoop: false,
|
||||
tlsCa: false,
|
||||
|
||||
@@ -4,15 +4,22 @@ import { parseArgs } from "node:util";
|
||||
import { isExactExternalTypeSpecifier } from "../frontend/program.js";
|
||||
import { renderDiagnostics } from "../diagnostics/render.js";
|
||||
import {
|
||||
COVERAGE_FAIL_ON,
|
||||
coverageEnvelope,
|
||||
coveragePasses,
|
||||
renderCoverage,
|
||||
renderExplainDynamic,
|
||||
renderExplainIsland,
|
||||
type CoverageFailOn,
|
||||
} from "../coverage/report.js";
|
||||
import {
|
||||
NODE_ISLANDS_MODES,
|
||||
type NodeIslandOptions,
|
||||
type NodeIslandsMode,
|
||||
} from "../node-islands/policy.js";
|
||||
import { TIER_MODES, type TierMode } from "../frontend/tier-analysis.js";
|
||||
import { buildEnvelope } from "../diagnostics/envelope.js";
|
||||
import { renderWarnings } from "../diagnostics/render.js";
|
||||
import { renderConfigWarnings, renderWarnings } from "../diagnostics/render.js";
|
||||
import { isPerformanceCode, renderPerformanceWarnings } from "../diagnostics/dynamic-tier.js";
|
||||
import { isNativeOptimization } from "../backend/optimization.js";
|
||||
import {
|
||||
@@ -114,6 +121,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
values["emit-ir"] ||
|
||||
values.tier !== undefined ||
|
||||
values["explain-dynamic"] !== undefined ||
|
||||
hasNodeIslandFlags(values) ||
|
||||
!values["keep-llvm"]
|
||||
) {
|
||||
fail(
|
||||
@@ -172,6 +180,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
values["emit-ir"] ||
|
||||
values.tier !== undefined ||
|
||||
values["explain-dynamic"] !== undefined ||
|
||||
hasNodeIslandFlags(values) ||
|
||||
!values["keep-llvm"];
|
||||
if (values.out === undefined || inputs.length === 0 || others) {
|
||||
fail(
|
||||
@@ -225,7 +234,8 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
(values["npm-static"] ?? []).length > 0 ||
|
||||
externalTypeArgs.length > 0 ||
|
||||
values.tier !== undefined ||
|
||||
values["explain-dynamic"] !== undefined
|
||||
values["explain-dynamic"] !== undefined ||
|
||||
hasNodeIslandFlags(values)
|
||||
) {
|
||||
fail(
|
||||
"scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--target-cpu/--profile-generate/--profile-use/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only",
|
||||
@@ -298,14 +308,9 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
fail(`--print=diagnostics is a build/coverage option\n\n${USAGE}`);
|
||||
}
|
||||
const failOnRaw = values["fail-on"];
|
||||
if (
|
||||
failOnRaw !== undefined &&
|
||||
failOnRaw !== "blockers" &&
|
||||
failOnRaw !== "divergences" &&
|
||||
failOnRaw !== "dynamic"
|
||||
) {
|
||||
if (failOnRaw !== undefined && !(COVERAGE_FAIL_ON as readonly string[]).includes(failOnRaw)) {
|
||||
fail(
|
||||
`unknown --fail-on value "${failOnRaw}" (supported: blockers, divergences, dynamic)\n\n${USAGE}`,
|
||||
`unknown --fail-on value "${failOnRaw}" (supported: ${COVERAGE_FAIL_ON.join(", ")})\n\n${USAGE}`,
|
||||
);
|
||||
}
|
||||
// --no-dynamic-tier-warning leaves out SC6101 (diagnostics/dynamic-tier.ts).
|
||||
@@ -326,6 +331,48 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
if (explainDynamic !== undefined && command !== "coverage") {
|
||||
fail(`--explain-dynamic is a coverage option\n\n${USAGE}`);
|
||||
}
|
||||
const explainIsland = values["explain-island"];
|
||||
if (explainIsland !== undefined && command !== "coverage") {
|
||||
fail(`--explain-island is a coverage option\n\n${USAGE}`);
|
||||
}
|
||||
if (explainIsland === "") fail(`--explain-island needs a package name\n\n${USAGE}`);
|
||||
if (values["strict-user-js"] && command !== "coverage") {
|
||||
fail(`--strict-user-js is a coverage option\n\n${USAGE}`);
|
||||
}
|
||||
// The Node island policy's command-line half. Report-only: build and run
|
||||
// validate it, coverage reports the islands it would choose.
|
||||
const islandMode = values["node-islands"];
|
||||
if (islandMode !== undefined && !(NODE_ISLANDS_MODES as readonly string[]).includes(islandMode)) {
|
||||
fail(
|
||||
`unknown --node-islands value "${islandMode}" (supported: ${NODE_ISLANDS_MODES.join(", ")})\n\n${USAGE}`,
|
||||
);
|
||||
}
|
||||
const listValues = (raw: string[] | undefined): string[] =>
|
||||
(raw ?? [])
|
||||
.flatMap((v) => v.split(","))
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => v !== "");
|
||||
const islandArgs = listValues(values["node-island"]);
|
||||
const nativeArgs = listValues(values.native);
|
||||
for (const name of nativeArgs) {
|
||||
if (islandArgs.includes(name))
|
||||
fail(`--native=${name} and --node-island=${name} contradict each other\n\n${USAGE}`);
|
||||
}
|
||||
if (values.node === "") fail(`--node needs the path of a node executable\n\n${USAGE}`);
|
||||
const nodeIslands: NodeIslandOptions | undefined =
|
||||
islandMode === undefined &&
|
||||
islandArgs.length === 0 &&
|
||||
nativeArgs.length === 0 &&
|
||||
values.node === undefined &&
|
||||
!values["strict-user-js"]
|
||||
? undefined
|
||||
: {
|
||||
...(islandMode !== undefined ? { mode: islandMode as NodeIslandsMode } : {}),
|
||||
...(islandArgs.length > 0 ? { islands: islandArgs } : {}),
|
||||
...(nativeArgs.length > 0 ? { native: nativeArgs } : {}),
|
||||
...(values.node !== undefined ? { node: resolve(values.node) } : {}),
|
||||
...(values["strict-user-js"] ? { strictUserJs: true } : {}),
|
||||
};
|
||||
if (printNativeLinkInfo && command !== "build") {
|
||||
fail(`--print=native-link-info is a build option\n\n${USAGE}`);
|
||||
}
|
||||
@@ -468,6 +515,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
...(tier !== undefined ? { tier } : {}),
|
||||
...(explainDynamic !== undefined ? { explainDynamic: true } : {}),
|
||||
...(wasmFiles.length > 0 ? { wasm: wasmFiles } : {}),
|
||||
...(nodeIslands !== undefined ? { nodeIslands } : {}),
|
||||
});
|
||||
if (!dynamicTierWarning) delete coverage.performance;
|
||||
if (printDiagnostics) {
|
||||
@@ -483,6 +531,17 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
process.stdout.write(
|
||||
renderCoverage(coverage, { color, sourceTexts, root: process.cwd() }) + "\n",
|
||||
);
|
||||
if (explainIsland !== undefined) {
|
||||
process.stdout.write(
|
||||
"\n" +
|
||||
renderExplainIsland(coverage.nodeIslands, explainIsland, {
|
||||
color,
|
||||
sourceTexts,
|
||||
root: process.cwd(),
|
||||
}) +
|
||||
"\n",
|
||||
);
|
||||
}
|
||||
if (explainDynamic !== undefined && coverage.tiers !== undefined) {
|
||||
process.stdout.write(
|
||||
"\n" +
|
||||
@@ -577,14 +636,21 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
// warning names likely-hot functions that use untyped values. `run`
|
||||
// keeps stderr for the program's own output.
|
||||
const color = process.stderr.isTTY ?? false;
|
||||
const divergences = warnings.filter((d) => !isPerformanceCode(d.code));
|
||||
const config = warnings.filter((d) => d.code === "SC8004");
|
||||
const divergences = warnings.filter((d) => !isPerformanceCode(d.code) && d.code !== "SC8004");
|
||||
const performance = warnings.filter((d) => isPerformanceCode(d.code));
|
||||
if (divergences.length > 0)
|
||||
if (config.length > 0)
|
||||
process.stderr.write(
|
||||
renderConfigWarnings(config, result.sourceTexts ?? new Map(), { color }) + "\n",
|
||||
);
|
||||
if (divergences.length > 0) {
|
||||
if (config.length > 0) process.stderr.write("\n");
|
||||
process.stderr.write(
|
||||
renderWarnings(divergences, result.sourceTexts ?? new Map(), { color }) + "\n",
|
||||
);
|
||||
}
|
||||
if (performance.length > 0) {
|
||||
if (divergences.length > 0) process.stderr.write("\n");
|
||||
if (divergences.length > 0 || config.length > 0) process.stderr.write("\n");
|
||||
process.stderr.write(renderPerformanceWarnings(performance) + "\n");
|
||||
}
|
||||
}
|
||||
@@ -615,6 +681,19 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Whether any Node island flag is present (they belong to build, run and
|
||||
* coverage). */
|
||||
function hasNodeIslandFlags(values: ReturnType<typeof parseCli>["values"]): boolean {
|
||||
return (
|
||||
values["node-islands"] !== undefined ||
|
||||
(values["node-island"] ?? []).length > 0 ||
|
||||
(values.native ?? []).length > 0 ||
|
||||
values.node !== undefined ||
|
||||
values["explain-island"] !== undefined ||
|
||||
values["strict-user-js"]
|
||||
);
|
||||
}
|
||||
|
||||
/** Both installed and seed commands use this argument and diagnostic contract. */
|
||||
export async function runCli(args: string[], host: CliHost): Promise<number> {
|
||||
try {
|
||||
|
||||
@@ -31,11 +31,13 @@ Options:
|
||||
(native-link-info implies --emit=obj and never links;
|
||||
diagnostics prints the versioned JSON diagnostics
|
||||
envelope for build and coverage)
|
||||
--fail-on <blockers|divergences|dynamic>
|
||||
--fail-on <blockers|divergences|dynamic|node-islands|eager-node-islands|island-gaps>
|
||||
coverage only: exit 1 when the entry path has blockers
|
||||
(a build would fail), or also when it has Node
|
||||
divergences, or also when it has likely-hot functions
|
||||
that use untyped values (SC6101)
|
||||
divergences, likely-hot functions that use untyped
|
||||
values (SC6101), any Node island (eager-node-islands is
|
||||
the same until deferred evaluation lands), or a gap
|
||||
island (a package scriptc should compile but cannot yet)
|
||||
--backend <b> code generator (llvm)
|
||||
--optimization <release|dev|speed>
|
||||
native optimization posture (default: release/-O2). dev
|
||||
@@ -110,6 +112,28 @@ Options:
|
||||
--no-dynamic-tier-warning
|
||||
build and coverage: leave out SC6101, the warning that
|
||||
names likely-hot functions that use untyped values
|
||||
--node-islands <auto|explicit|off>
|
||||
Node island policy (default: auto): npm packages outside
|
||||
the native subset run in Node automatically; explicit
|
||||
allows only marked islands; off allows none. REPORT-ONLY
|
||||
in this version: coverage reports the islands a build
|
||||
would choose; builds compile as before
|
||||
--node-island <pkg-or-path>
|
||||
mark an npm package or one of your modules (a path or
|
||||
glob) to run in a Node island (repeatable, comma lists;
|
||||
report-only)
|
||||
--native <pkg> require an npm package to compile natively; outside the
|
||||
native subset it is SC8003 (repeatable; report-only)
|
||||
--node <path> the node executable Node islands use (report-only)
|
||||
--explain-island <pkg>
|
||||
coverage only: why a package would run in a Node island,
|
||||
with the first reason per module, the native import
|
||||
sites and the packages inside its closure
|
||||
--strict-user-js
|
||||
coverage only: report constructor functions, prototype
|
||||
and shape mutation, sloppy-mode semantics and builtin
|
||||
patching in your own .js/.cjs/.mjs modules as
|
||||
SC8101–SC8105 blockers
|
||||
--wasm <file.wasm>
|
||||
coverage only: report a WebAssembly module the program
|
||||
loads whose path its sources do not spell (repeatable;
|
||||
@@ -144,6 +168,12 @@ export const CLI_OPTIONS = {
|
||||
"explain-dynamic": { type: "string" },
|
||||
"dynamic-tier-warning": { type: "boolean", default: true },
|
||||
wasm: { type: "string", multiple: true },
|
||||
"node-islands": { type: "string" },
|
||||
"node-island": { type: "string", multiple: true },
|
||||
native: { type: "string", multiple: true },
|
||||
node: { type: "string" },
|
||||
"explain-island": { type: "string" },
|
||||
"strict-user-js": { type: "boolean", default: false },
|
||||
lib: { type: "boolean", default: false },
|
||||
profile: { type: "string" },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ import type { NativeProfileMatch } from "./backend/native-profile.js";
|
||||
import type { ScrDiagnostic } from "./diagnostics/diagnostic.js";
|
||||
import type { CoverageInput } from "./coverage/report.js";
|
||||
import type { TierMode } from "./frontend/tier-analysis.js";
|
||||
import type { NodeIslandOptions } from "./node-islands/policy.js";
|
||||
|
||||
export type CompileOutputKind = "ir" | "llvm" | "asm" | "obj" | "exe";
|
||||
|
||||
@@ -193,6 +194,9 @@ export interface AnalyzeOptions {
|
||||
/** `--wasm <file>`: WebAssembly module files the program loads that its
|
||||
* sources do not name (absolute paths); reported with the discovered ones. */
|
||||
wasm?: readonly string[];
|
||||
/** The Node island policy's command-line half (report-only: coverage
|
||||
* reports which packages and modules would run in a Node island). */
|
||||
nodeIslands?: NodeIslandOptions;
|
||||
}
|
||||
|
||||
export interface AnalyzeResult {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type DiagnosticsEnvelope,
|
||||
type LocatedDiagnostic,
|
||||
type SourcePosition,
|
||||
type NodeIslandsEnvelope,
|
||||
type TierUnitPosition,
|
||||
type TiersEnvelope,
|
||||
} from "../diagnostics/envelope.js";
|
||||
@@ -25,6 +26,14 @@ import type { LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { ProvenanceSources } from "../frontend/provenance-registry.js";
|
||||
import type { DemotionUnit, TierReport } from "../frontend/tier-analysis.js";
|
||||
import type { WasmModuleReport } from "../wasm/discover.js";
|
||||
import {
|
||||
describe as describeReason,
|
||||
hasNodeIslandDecisions,
|
||||
type NodeIslandEntry,
|
||||
type NodeIslandReport,
|
||||
} from "../node-islands/summary.js";
|
||||
import { classDescription } from "../node-islands/rules.js";
|
||||
import type { SrcLoc } from "../ir/ir.js";
|
||||
|
||||
export interface CoverageInput {
|
||||
file: string;
|
||||
@@ -65,6 +74,10 @@ export interface CoverageInput {
|
||||
/** The WebAssembly modules the build can see (wasm/discover.ts): their
|
||||
* size, validity and the proposals they use. */
|
||||
wasmModules?: WasmModuleReport[];
|
||||
/** Node islands, report-only: the packages and user modules that would
|
||||
* run in a Node island, and why. Present when there is something to
|
||||
* report. */
|
||||
nodeIslands?: NodeIslandReport;
|
||||
/** True when tsc preflight failed — no coverage can be computed. */
|
||||
preflightFailed: boolean;
|
||||
}
|
||||
@@ -148,6 +161,19 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions
|
||||
out.push(` ${c(RED, "not analyzable")}: ${config[0]!.message}`);
|
||||
return out.join("\n");
|
||||
}
|
||||
const projectConfig = input.diagnostics.filter((d) => d.code === "SC8004");
|
||||
if (projectConfig.length > 0) {
|
||||
out.push(
|
||||
` ${c(RED, "not analyzable")}: the project's scriptc configuration has ${projectConfig.length} unknown setting${projectConfig.length === 1 ? "" : "s"}`,
|
||||
);
|
||||
for (const d of projectConfig) {
|
||||
const p = locator.position(d.loc.file, d.loc.start);
|
||||
out.push(` ${c(DIM, "SC8004")} ${d.message}`);
|
||||
out.push(` ${c(DIM, `at ${where({ file: d.loc.file, ...p })}`)}`);
|
||||
if (d.hint !== undefined) out.push(` ${c(DIM, `hint: ${d.hint}`)}`);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
const tsc = input.diagnostics.filter((d) => d.code === "SC0001");
|
||||
if (tsc.length > 0) {
|
||||
const world = tsc.filter((d) => d.typeWorld === true).length;
|
||||
@@ -225,6 +251,22 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions
|
||||
out.push("");
|
||||
}
|
||||
|
||||
// Node islands (report-only): what would run in Node, gap islands
|
||||
// first (DESIGN.md §1.3: a gap island is compiler work an island hides).
|
||||
if (input.nodeIslands !== undefined && hasNodeIslandDecisions(input.nodeIslands)) {
|
||||
renderNodeIslands(
|
||||
input.nodeIslands,
|
||||
out,
|
||||
c,
|
||||
(loc) => {
|
||||
const p = locator.position(loc.file, loc.start);
|
||||
return where({ file: loc.file, ...p });
|
||||
},
|
||||
opts.root,
|
||||
);
|
||||
out.push("");
|
||||
}
|
||||
|
||||
// The WebAssembly modules the build can see (wasm/discover.ts). A build
|
||||
// translates the valid ones to native code (wasm/translate.ts) when the
|
||||
// program uses the WebAssembly API; the others fail to load at run time.
|
||||
@@ -477,9 +519,24 @@ function displayPath(file: string, root: string | undefined): string {
|
||||
}
|
||||
|
||||
/** What `coverage --fail-on` checks: reached blockers (they fail a build)
|
||||
* and, optionally, divergences or the performance warning. Unreached
|
||||
* blockers never fail. */
|
||||
export type CoverageFailOn = "blockers" | "divergences" | "dynamic";
|
||||
* and, optionally, divergences, the performance warning or Node islands.
|
||||
* Unreached blockers never fail. */
|
||||
export type CoverageFailOn =
|
||||
| "blockers"
|
||||
| "divergences"
|
||||
| "dynamic"
|
||||
| "node-islands"
|
||||
| "eager-node-islands"
|
||||
| "island-gaps";
|
||||
|
||||
export const COVERAGE_FAIL_ON: readonly CoverageFailOn[] = [
|
||||
"blockers",
|
||||
"divergences",
|
||||
"dynamic",
|
||||
"node-islands",
|
||||
"eager-node-islands",
|
||||
"island-gaps",
|
||||
];
|
||||
|
||||
/** Whether the analysis passes the selected gate. Preflight failures
|
||||
* always fail; `failOn` undefined means only preflight fails (the report's
|
||||
@@ -492,6 +549,16 @@ export function coveragePasses(input: CoverageInput, failOn?: CoverageFailOn): b
|
||||
if (input.diagnostics.length > 0) return false;
|
||||
if (failOn === "divergences" && (input.divergences?.length ?? 0) > 0) return false;
|
||||
if (failOn === "dynamic" && (input.performance?.length ?? 0) > 0) return false;
|
||||
// The Node island gates (report-only islands count: they are what a
|
||||
// build would choose). Every island evaluates eagerly until deferred
|
||||
// evaluation lands, so eager-node-islands equals node-islands for now.
|
||||
// Would-be policy errors (SC8002, SC8003) fail every island gate.
|
||||
const islands = input.nodeIslands;
|
||||
if (islands === undefined) return true;
|
||||
const anyIsland = failOn === "node-islands" || failOn === "eager-node-islands";
|
||||
if ((anyIsland || failOn === "island-gaps") && islands.violations.length > 0) return false;
|
||||
if (anyIsland && (islands.islands.length > 0 || islands.userModules.length > 0)) return false;
|
||||
if (failOn === "island-gaps" && islands.islands.some((i) => i.class === "gap")) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -555,6 +622,9 @@ export function coverageEnvelope(
|
||||
}),
|
||||
...(input.tiers !== undefined ? { tiers: tiersEnvelope(input.tiers, locator) } : {}),
|
||||
...(input.wasmModules !== undefined ? { wasmModules: input.wasmModules } : {}),
|
||||
...(input.nodeIslands !== undefined
|
||||
? { nodeIslands: nodeIslandsEnvelope(input.nodeIslands, locator) }
|
||||
: {}),
|
||||
groups,
|
||||
diagnostics: located,
|
||||
};
|
||||
@@ -692,3 +762,211 @@ export function renderExplainDynamic(
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
/* ── Node islands (report-only) ───────────────────────────────────────── */
|
||||
|
||||
const ISLAND_REASONS_SHOWN = 1;
|
||||
|
||||
function islandName(i: { package: string; version: string | null }): string {
|
||||
return i.version === null ? i.package : `${i.package}@${i.version}`;
|
||||
}
|
||||
|
||||
function renderNodeIslands(
|
||||
report: NodeIslandReport,
|
||||
out: string[],
|
||||
c: (code: string, s: string) => string,
|
||||
at: (loc: SrcLoc) => string,
|
||||
root: string | undefined,
|
||||
): void {
|
||||
const n = report.islands.length + report.userModules.length;
|
||||
out.push(
|
||||
` ${c(BOLD, "Node islands")} ${c(DIM, `(would-be, report-only: the build is unchanged; --node-islands=${report.policy})`)}`,
|
||||
);
|
||||
if (n === 0) out.push(` ${c(DIM, "none")}`);
|
||||
const widest = Math.max(0, ...report.islands.map((i) => i.class.length));
|
||||
for (const island of report.islands) {
|
||||
const tone = island.class === "gap" ? RED : YELLOW;
|
||||
const decided =
|
||||
island.decision === "auto"
|
||||
? ""
|
||||
: c(
|
||||
DIM,
|
||||
` (marked by ${island.decision === "cli" ? "--node-island" : "scriptc.packages"})`,
|
||||
);
|
||||
out.push(` ${c(tone, island.class.padEnd(widest))} ${islandName(island)}${decided}`);
|
||||
for (const r of island.reasons.slice(0, ISLAND_REASONS_SHOWN)) {
|
||||
const where = r.loc === null ? "" : `${at(r.loc)}: `;
|
||||
out.push(` ${c(DIM, capMessage(`${where}${describeReason(r)}`))}`);
|
||||
}
|
||||
const more = island.reasons.length - ISLAND_REASONS_SHOWN;
|
||||
const details = [
|
||||
more > 0
|
||||
? `+${more} more reason${more === 1 ? "" : "s"} in ${counted(island.modules, "module")}`
|
||||
: "",
|
||||
island.carries.length > 0 ? `carries ${island.carries.join(", ")}` : "",
|
||||
].filter((t) => t !== "");
|
||||
if (details.length > 0) out.push(` ${c(DIM, details.join("; "))}`);
|
||||
}
|
||||
for (const m of report.userModules) {
|
||||
out.push(
|
||||
` ${c(YELLOW, "marked".padEnd(widest))} ${displayPath(m.file, root)} ${c(DIM, `(user module, marked by ${m.source === "cli" ? "--node-island" : "nodeIslands.modules"})`)}`,
|
||||
);
|
||||
}
|
||||
for (const v of report.violations) {
|
||||
out.push(` ${c(RED, v.code)} ${capMessage(v.message)}`);
|
||||
}
|
||||
const shared = report.packages.filter((p) => p.status === "shared");
|
||||
if (shared.length > 0) {
|
||||
out.push(
|
||||
` ${c(DIM, `reached from native code and from an island: ${shared.map((p) => p.package).join(", ")}`)}`,
|
||||
);
|
||||
}
|
||||
const native = report.packages.filter((p) => p.status === "native");
|
||||
if (native.length > 0) {
|
||||
out.push(
|
||||
` ${c(DIM, `${counted(native.length, "package")} stay${native.length === 1 ? "s" : ""} native: ${native.map((p) => p.package).join(", ")}`)}`,
|
||||
);
|
||||
}
|
||||
if (report.unknownPackages.length > 0) {
|
||||
out.push(
|
||||
` ${c(YELLOW, "note")} not imported by the program: ${report.unknownPackages.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (report.islands.length > 0) {
|
||||
out.push(
|
||||
` ${c(DIM, "hint: scriptc coverage --explain-island=<package> lists every reason")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** `scriptc coverage --explain-island=<package>`: why a package would run
|
||||
* in a Node island — every module's first reason, the native import sites
|
||||
* (the boundary), the packages inside its closure, and what later
|
||||
* milestones decide. */
|
||||
export function renderExplainIsland(
|
||||
report: NodeIslandReport | undefined,
|
||||
query: string,
|
||||
opts: CoverageRenderOptions = {},
|
||||
): string {
|
||||
const c = (code: string, s: string) => (opts.color ? code + s + RESET : s);
|
||||
const locator = new SourceLocator(opts.sourceTexts);
|
||||
const at = (loc: SrcLoc) => {
|
||||
const p = locator.position(loc.file, loc.start);
|
||||
return `${displayPath(loc.file, opts.root)}:${p.line}:${p.column}`;
|
||||
};
|
||||
const out: string[] = [];
|
||||
const matches = (report?.islands ?? []).filter(
|
||||
(i) => i.package === query || islandName(i) === query,
|
||||
);
|
||||
if (matches.length === 0) {
|
||||
const pkg = (report?.packages ?? []).find((p) => p.package === query);
|
||||
if (pkg === undefined) {
|
||||
out.push(` '${query}' is not a Node island: no npm package of that name is in the program`);
|
||||
} else if (pkg.status === "inside") {
|
||||
out.push(
|
||||
` '${query}' is not a Node island of its own: only ${pkg.islands.join(", ")} import${pkg.islands.length === 1 ? "s" : ""} it, so it would run inside that island`,
|
||||
);
|
||||
} else if (pkg.status === "shared") {
|
||||
out.push(
|
||||
` '${query}' compiles natively and is also reached from the Node island${pkg.islands.length === 1 ? "" : "s"} ${pkg.islands.join(", ")}`,
|
||||
);
|
||||
} else {
|
||||
out.push(` '${query}' is not a Node island: it compiles natively`);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
for (const island of matches) {
|
||||
out.push(
|
||||
` ${c(BOLD, "Node island")} ${islandName(island)} ${c(DIM, "(would-be, report-only)")}`,
|
||||
);
|
||||
if (island.path !== null) out.push(` path ${displayPath(island.path, opts.root)}`);
|
||||
out.push(` class ${island.class}: ${classDescription(island.class)}`);
|
||||
out.push(
|
||||
` decision ${island.decision === "auto" ? "automatic (npm code outside the native subset)" : island.decision === "cli" ? "marked with --node-island" : 'marked "node" in scriptc.packages'}`,
|
||||
);
|
||||
out.push(
|
||||
` evaluation eager at its module position ${c(DIM, "(deferred evaluation is decided by a later milestone)")}`,
|
||||
);
|
||||
if (island.reasons.length > 0) {
|
||||
out.push(` reasons ${c(DIM, "(first per module; decisive class first)")}`);
|
||||
const shown = new Set<string>();
|
||||
for (const r of island.reasons) {
|
||||
const key = r.loc === null ? "" : r.loc.file;
|
||||
if (shown.has(key)) continue;
|
||||
shown.add(key);
|
||||
const where = r.loc === null ? "package" : at(r.loc);
|
||||
out.push(` ${r.class.padEnd(12)} ${where}`);
|
||||
out.push(` ${c(DIM, capMessage(describeReason(r)))}`);
|
||||
}
|
||||
const counts = new Map<string, number>();
|
||||
for (const r of island.reasons) counts.set(r.class, (counts.get(r.class) ?? 0) + 1);
|
||||
const summary = [...counts].map(([cls, k]) => `${k} ${cls}`).join(", ");
|
||||
out.push(` all reasons ${island.reasons.length} (${summary})`);
|
||||
}
|
||||
if (island.importedFrom.length > 0) {
|
||||
out.push(` boundary ${counted(island.importedFrom.length, "native import site")}:`);
|
||||
for (const loc of island.importedFrom.slice(0, SITES_SHOWN)) out.push(` ${at(loc)}`);
|
||||
if (island.importedFrom.length > SITES_SHOWN)
|
||||
out.push(` ${c(DIM, `+${island.importedFrom.length - SITES_SHOWN} more`)}`);
|
||||
out.push(
|
||||
` ${c(DIM, "copy and handle decisions per boundary site come with compiler-managed islands")}`,
|
||||
);
|
||||
} else {
|
||||
out.push(
|
||||
` boundary ${c(DIM, "no native import site (reached only from other islands)")}`,
|
||||
);
|
||||
}
|
||||
if (island.carries.length > 0) out.push(` carries ${island.carries.join(", ")}`);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
function nodeIslandsEnvelope(
|
||||
report: NodeIslandReport,
|
||||
locator: SourceLocator,
|
||||
): NodeIslandsEnvelope {
|
||||
const pos = (loc: SrcLoc): SourcePosition => ({
|
||||
file: loc.file,
|
||||
...locator.position(loc.file, loc.start),
|
||||
});
|
||||
const entry = (i: NodeIslandEntry) => {
|
||||
const first = i.reasons[0];
|
||||
return {
|
||||
package: i.package,
|
||||
version: i.version,
|
||||
path: i.path,
|
||||
class: i.class,
|
||||
reason: i.reason,
|
||||
diagnostic: i.diagnostic,
|
||||
decision: i.decision,
|
||||
evaluation: i.evaluation,
|
||||
...(first !== undefined && first.loc !== null ? { at: pos(first.loc) } : {}),
|
||||
reasons: i.reasons.map((r) => ({
|
||||
class: r.class,
|
||||
rule: r.rule,
|
||||
reason: r.reason,
|
||||
code: r.code,
|
||||
message: r.message,
|
||||
source: r.source,
|
||||
...(r.loc !== null ? { at: pos(r.loc) } : {}),
|
||||
})),
|
||||
carries: i.carries,
|
||||
importedFrom: i.importedFrom.map(pos),
|
||||
};
|
||||
};
|
||||
return {
|
||||
mode: report.mode,
|
||||
policy: report.policy,
|
||||
islands: report.islands.map(entry),
|
||||
userModules: report.userModules.map((m) => ({ file: m.file, decision: m.source })),
|
||||
npmPackages: report.packages.map((p) => ({
|
||||
package: p.package,
|
||||
version: p.version,
|
||||
path: p.path,
|
||||
status: p.status,
|
||||
islands: p.islands,
|
||||
})),
|
||||
violations: report.violations.map((v) => ({ code: v.code, message: v.message, ...pos(v.loc) })),
|
||||
unknownPackages: report.unknownPackages,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,14 +39,14 @@ import { InternalCompilerError } from "../errors.js";
|
||||
* byte-deterministic: entries sort by id, keys are emitted in one fixed
|
||||
* order, and the output carries no timestamps or absolute paths. */
|
||||
import { FENCE_CODES, UNSUPPORTED, webAssemblyDiag } from "../diagnostics/diagnostic.js";
|
||||
import { NODE24_FETCH_COMPAT_PROFILE } from "../compat/fetch-profile.js";
|
||||
import { node24FetchCompatProfile } from "../compat/fetch-profile.js";
|
||||
import { SUPPORTED_BUILTIN_MODULES, SUPPORTED_NODE_MODULES } from "../frontend/builtin-modules.js";
|
||||
import {
|
||||
AMBIENT_SURFACE_FNS,
|
||||
ambientSurfaceFns,
|
||||
ARRAY_METHODS,
|
||||
BUILTIN_MODULE_CONSTS,
|
||||
BUILTIN_MODULE_FENCE_HINTS,
|
||||
BUILTIN_MODULE_FNS,
|
||||
builtinModuleFns,
|
||||
COLLECTION_ITERATOR_METHODS,
|
||||
COMPOUND_ASSIGN_OPS,
|
||||
STDLIB_SURFACE,
|
||||
@@ -88,18 +88,21 @@ export const MANIFEST_SCHEMA_VERSION = 2;
|
||||
/** The manifest's own scope statement: what the projection covers and —
|
||||
* just as load-bearing — what it deliberately leaves out, so absence is
|
||||
* never read as a support claim in either direction. */
|
||||
const COVERAGE_NOTES: string[] = [
|
||||
"Entries are projected mechanically from the compiler's own decision tables: the diagnostics registry, the unsupported-syntax dispatch tables, the stdlib and node-builtin lowering tables, and the supported-builtin-module list. Nothing is hand-maintained; the manifest regenerates byte-identically from the source tree at this version.",
|
||||
"Absence from this manifest means 'not projected', never 'unsupported'. Surfaces lowered through dedicated code paths rather than tables are not yet projected: console, JSON, Promise/async and the timer surface, the net/http/tls/https/dgram/dns/assert/test/stream/readline module member surfaces, template literals, the regex slice, global functions (parseInt, parseFloat, isNaN, isFinite), and the process surface outside its ambient slice.",
|
||||
"The ambient-nondeterminism and ambient-authority surfaces the library sidecar's determinism attestation scans ARE projected even where they lower through dedicated code paths — the Date compositions (stdlib.date.*), perf_hooks' performance.now, and the process global's ambient reads and authority calls (node-builtin.process.*) — so a determinism fence can name every surface the attestation demotes on.",
|
||||
"stdlib and node-builtin member entries name surface whose LOWERED call forms are constrained (arity, argument shapes); declared call forms outside the lowered set are refused per site, with code SC2020 for standard-library and node-builtin surface.",
|
||||
"Entries with status 'unsupported' describe where the named code is raised: forms of the construct outside the supported subset are refused with that code — not that every form of the named feature is refused. Supported forms appear as their own static entries where a table projects them.",
|
||||
`The engine-free fetch projection targets Node ${NODE24_FETCH_COMPAT_PROFILE.target.node} with bundled Undici ${NODE24_FETCH_COMPAT_PROFILE.target.undici}. Each projected row names the differential evidence that guards it; changing the pinned Node or Undici version is an explicit profile update.`,
|
||||
"The fetch profile also contains a runtime-reflected census of the selected fetch, abort, Headers, and readable-stream interfaces plus RequestInit/ResponseInit dictionary reads. Static and unsupported census rows are projected here; its explicitly out-of-scope metadata rows and adjacent-interface exclusions remain in the profile so absence is deliberate rather than ambiguous.",
|
||||
"Process-level diagnostic codes are not surface entries: SC0001-SC0004 are preflight gates, SC1110 is a comptime evaluation failure, SC3001/SC3002 are backend/target tier refusals, SC9001/SC9002 are internal errors.",
|
||||
"Entry statuses are projected for the desktop targets. The mobile targets (aarch64-apple-ios, aarch64-apple-ios-simulator, aarch64-linux-android) compile library-mode archives only: the library-admissible surface (what SC4005's async_free requirement and the library link set admit) is supported there, the executable lane refuses those triples with SC3002, and no entry outside the library-admissible surface carries a mobile support claim. iOS archives build for iOS 15.0 on darwin hosts; Android archives build against NDK API level 26.",
|
||||
"No scheduling metadata is published; entry ids are the stable diff keys across releases.",
|
||||
];
|
||||
function coverageNotes(): string[] {
|
||||
const fetchTarget = node24FetchCompatProfile().target;
|
||||
return [
|
||||
"Entries are projected mechanically from the compiler's own decision tables: the diagnostics registry, the unsupported-syntax dispatch tables, the stdlib and node-builtin lowering tables, and the supported-builtin-module list. Nothing is hand-maintained; the manifest regenerates byte-identically from the source tree at this version.",
|
||||
"Absence from this manifest means 'not projected', never 'unsupported'. Surfaces lowered through dedicated code paths rather than tables are not yet projected: console, JSON, Promise/async and the timer surface, the net/http/tls/https/dgram/dns/assert/test/stream/readline module member surfaces, template literals, the regex slice, global functions (parseInt, parseFloat, isNaN, isFinite), and the process surface outside its ambient slice.",
|
||||
"The ambient-nondeterminism and ambient-authority surfaces the library sidecar's determinism attestation scans ARE projected even where they lower through dedicated code paths — the Date compositions (stdlib.date.*), perf_hooks' performance.now, and the process global's ambient reads and authority calls (node-builtin.process.*) — so a determinism fence can name every surface the attestation demotes on.",
|
||||
"stdlib and node-builtin member entries name surface whose LOWERED call forms are constrained (arity, argument shapes); declared call forms outside the lowered set are refused per site, with code SC2020 for standard-library and node-builtin surface.",
|
||||
"Entries with status 'unsupported' describe where the named code is raised: forms of the construct outside the supported subset are refused with that code — not that every form of the named feature is refused. Supported forms appear as their own static entries where a table projects them.",
|
||||
`The engine-free fetch projection targets Node ${fetchTarget.node} with bundled Undici ${fetchTarget.undici}. Each projected row names the differential evidence that guards it; changing the pinned Node or Undici version is an explicit profile update.`,
|
||||
"The fetch profile also contains a runtime-reflected census of the selected fetch, abort, Headers, and readable-stream interfaces plus RequestInit/ResponseInit dictionary reads. Static and unsupported census rows are projected here; its explicitly out-of-scope metadata rows and adjacent-interface exclusions remain in the profile so absence is deliberate rather than ambiguous.",
|
||||
"Process-level diagnostic codes are not surface entries: SC0001-SC0004 are preflight gates, SC1110 is a comptime evaluation failure, SC3001/SC3002 are backend/target tier refusals, SC9001/SC9002 are internal errors.",
|
||||
"Entry statuses are projected for the desktop targets. The mobile targets (aarch64-apple-ios, aarch64-apple-ios-simulator, aarch64-linux-android) compile library-mode archives only: the library-admissible surface (what SC4005's async_free requirement and the library link set admit) is supported there, the executable lane refuses those triples with SC3002, and no entry outside the library-admissible surface carries a mobile support claim. iOS archives build for iOS 15.0 on darwin hosts; Android archives build against NDK API level 26.",
|
||||
"No scheduling metadata is published; entry ids are the stable diff keys across releases.",
|
||||
];
|
||||
}
|
||||
|
||||
/** Stable kebab id segment from prose: quotes dropped, the parenthetical
|
||||
* tail dropped, non-alphanumeric runs collapsed to one dash. */
|
||||
@@ -410,7 +413,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
: "recognized module (bare and node:-prefixed specifiers)",
|
||||
});
|
||||
}
|
||||
for (const [mod, members] of Object.entries(BUILTIN_MODULE_FNS)) {
|
||||
for (const [mod, members] of Object.entries(builtinModuleFns())) {
|
||||
for (const member of Object.keys(members!)) {
|
||||
add({
|
||||
id: `${moduleId(mod)}.${member}`,
|
||||
@@ -459,10 +462,9 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
}
|
||||
|
||||
// ── fetch/Web platform: one explicit, versioned compatibility profile ─
|
||||
const fetchTarget =
|
||||
`Node ${NODE24_FETCH_COMPAT_PROFILE.target.node} / ` +
|
||||
`Undici ${NODE24_FETCH_COMPAT_PROFILE.target.undici}`;
|
||||
for (const operation of NODE24_FETCH_COMPAT_PROFILE.operations) {
|
||||
const fetchProfile = node24FetchCompatProfile();
|
||||
const fetchTarget = `Node ${fetchProfile.target.node} / Undici ${fetchProfile.target.undici}`;
|
||||
for (const operation of fetchProfile.operations) {
|
||||
const evidence = operation.evidence.map((item) =>
|
||||
item.generated !== undefined ? `generated:${item.generated}` : `fixture:${item.fixture!}`,
|
||||
);
|
||||
@@ -478,10 +480,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
`differential evidence: ${evidence.join(", ")}`,
|
||||
});
|
||||
}
|
||||
for (const options of [
|
||||
NODE24_FETCH_COMPAT_PROFILE.requestInit,
|
||||
NODE24_FETCH_COMPAT_PROFILE.responseInit,
|
||||
]) {
|
||||
for (const options of [fetchProfile.requestInit, fetchProfile.responseInit]) {
|
||||
for (const option of options) {
|
||||
const evidence = option.evidence.map((item) =>
|
||||
item.generated !== undefined ? `generated:${item.generated}` : `fixture:${item.fixture!}`,
|
||||
@@ -497,7 +496,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const row of NODE24_FETCH_COMPAT_PROFILE.inventory.entries) {
|
||||
for (const row of fetchProfile.inventory.entries) {
|
||||
if (row.status !== "unsupported") continue;
|
||||
if (row.code === undefined || row.reason === undefined) {
|
||||
throw new InternalCompilerError(`non-static fetch inventory row '${row.id}' is incomplete`);
|
||||
@@ -524,7 +523,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
// Date/perf_hooks/process rows from the one ambient table — each entry
|
||||
// is fenceable by construction (its row carries the reach-witnessing
|
||||
// libCall spellings the fence detector uses).
|
||||
for (const row of AMBIENT_SURFACE_FNS) {
|
||||
for (const row of ambientSurfaceFns()) {
|
||||
add({
|
||||
id: row.id,
|
||||
kind: row.kind,
|
||||
@@ -578,7 +577,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
return {
|
||||
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
||||
compilerVersion,
|
||||
coverage: COVERAGE_NOTES,
|
||||
coverage: coverageNotes(),
|
||||
entries,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,13 +15,15 @@ import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import type { LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { NpmStaticStatus } from "./report.js";
|
||||
import type { WasmModuleReport } from "../wasm/discover.js";
|
||||
import type { NodeIslandReport } from "../node-islands/summary.js";
|
||||
import { nodeIslandOptionsKey } from "../node-islands/policy.js";
|
||||
import {
|
||||
FrontendInputTracker,
|
||||
frontendInputsStillMatch,
|
||||
type FrontendInputSnapshot,
|
||||
} from "../frontend/input-tracker.js";
|
||||
|
||||
const SCHEMA = "scriptc.coverage-verdict.v2";
|
||||
const SCHEMA = "scriptc.coverage-verdict.v3";
|
||||
|
||||
/** Key-value storage for verdict entries (best effort: failures miss). */
|
||||
export interface VerdictStore {
|
||||
@@ -41,6 +43,7 @@ export interface CachedCoverage {
|
||||
performance?: ScrDiagnostic[];
|
||||
npmStatic?: NpmStaticStatus[];
|
||||
wasmModules?: WasmModuleReport[];
|
||||
nodeIslands?: NodeIslandReport;
|
||||
preflightFailed: boolean;
|
||||
}
|
||||
|
||||
@@ -130,6 +133,10 @@ export function verdictKey(
|
||||
...(opts.tier === "static-only" ? ["tier=static-only"] : []),
|
||||
// The module files' contents are probes; their names are options.
|
||||
...(opts.wasm !== undefined && opts.wasm.length > 0 ? [`wasm=${opts.wasm.join("\n")}`] : []),
|
||||
// The Node island policy's command-line half shapes the report.
|
||||
...(opts.nodeIslands !== undefined
|
||||
? [`node-islands=${nodeIslandOptionsKey(opts.nodeIslands)}`]
|
||||
: []),
|
||||
].join("\0"),
|
||||
);
|
||||
}
|
||||
@@ -153,6 +160,7 @@ function cacheable(coverage: AnalyzeResult["coverage"]): CachedCoverage | null {
|
||||
if (coverage.performance !== undefined) out.performance = coverage.performance;
|
||||
if (coverage.npmStatic !== undefined) out.npmStatic = coverage.npmStatic;
|
||||
if (coverage.wasmModules !== undefined) out.wasmModules = coverage.wasmModules;
|
||||
if (coverage.nodeIslands !== undefined) out.nodeIslands = coverage.nodeIslands;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -171,6 +179,7 @@ function restore(cached: CachedCoverage, sources: CachedSource[]): AnalyzeResult
|
||||
if (cached.performance !== undefined) coverage.performance = cached.performance;
|
||||
if (cached.npmStatic !== undefined) coverage.npmStatic = cached.npmStatic;
|
||||
if (cached.wasmModules !== undefined) coverage.wasmModules = cached.wasmModules;
|
||||
if (cached.nodeIslands !== undefined) coverage.nodeIslands = cached.nodeIslands;
|
||||
return { coverage, sourceTexts };
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,13 @@
|
||||
* notes, also warnings (coverage --fail-on=dynamic opts in):
|
||||
* likely-hot functions that use untyped values (SC6101,
|
||||
* diagnostics/dynamic-tier.ts)
|
||||
* SC8xxx Node island policy: packages and modules outside the native
|
||||
* subset that run (or, in report-only mode, would run) in a
|
||||
* Node island — unknown project configuration (SC8004), a
|
||||
* package required to be native (SC8003) or every island
|
||||
* refused (SC8002) while outside the native subset, and the
|
||||
* R2 object-model constructs in user JavaScript (SC8101–SC8105,
|
||||
* behind --strict-user-js)
|
||||
* SC9xxx internal compiler errors (still source-anchored)
|
||||
*/
|
||||
import type { SrcLoc } from "../ir/ir.js";
|
||||
@@ -92,6 +99,11 @@ export interface ScrDiagnostic {
|
||||
* the error may not reproduce under the project's tsc. Machine-readable
|
||||
* output reports these under the "environment" category. */
|
||||
typeWorld?: boolean;
|
||||
/** Why the construct is outside the native subset, as a Node island
|
||||
* reason id (node-islands/rules.ts), at the refusal sites whose code
|
||||
* alone does not say it (a computed require(), a native addon, `with`).
|
||||
* Report-only: never part of the rendered message. */
|
||||
reason?: string;
|
||||
/** A runtime fence in a module's top-level code, outside every function
|
||||
* and class member body: it throws when the module initializes, which is
|
||||
* program startup for a module the entry imports. */
|
||||
@@ -208,6 +220,75 @@ export function unrelatedClassCastDivergenceDiag(
|
||||
};
|
||||
}
|
||||
|
||||
/* ── SC8xxx: Node island policy ───────────────────────────────────────── */
|
||||
|
||||
/** SC8004 — an unknown key or value in `scriptc.packages` or
|
||||
* `scriptc.nodeIslands`. */
|
||||
export function projectConfigDiag(
|
||||
path: string,
|
||||
detail: string,
|
||||
hint: string,
|
||||
loc: SrcLoc,
|
||||
): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC8004",
|
||||
message: `package.json '${path}' ${detail}`,
|
||||
loc,
|
||||
hint,
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8002 — Node islands are off (or limited to marked islands) and an
|
||||
* npm package is outside the native subset. */
|
||||
export function nodeIslandsOffDiag(pkg: string, why: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC8002",
|
||||
message: `npm package '${pkg}' is outside the native subset (${why}) and Node islands are off`,
|
||||
loc,
|
||||
hint: `allow Node islands (--node-islands=auto), mark it with --node-island=${pkg}, or replace the dependency`,
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8002 — a user module is marked to run in a Node island while
|
||||
* Node islands are off. */
|
||||
export function nodeIslandsOffModuleDiag(
|
||||
file: string,
|
||||
source: "config" | "cli",
|
||||
loc: SrcLoc,
|
||||
): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC8002",
|
||||
message: `user module '${file}' is marked to run in Node (${source === "cli" ? "--node-island" : "scriptc.nodeIslands.modules"}) and Node islands are off`,
|
||||
loc,
|
||||
hint: "allow Node islands (--node-islands=auto or explicit), or remove the marking",
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8003 — a package marked "native" is outside the native subset. */
|
||||
export function nativePackageDiag(pkg: string, why: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC8003",
|
||||
message: `npm package '${pkg}' is marked native but is outside the native subset: ${why}`,
|
||||
loc,
|
||||
hint: `remove the "native" marking to let it run in a Node island, or replace the construct the reason names`,
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8101–SC8105 — an R2 object-model construct in the program's own
|
||||
* JavaScript (R2.1 constructor functions … R2.5 builtin patching). */
|
||||
export function userJsObjectModelDiag(
|
||||
code: "SC8101" | "SC8102" | "SC8103" | "SC8104" | "SC8105",
|
||||
construct: string,
|
||||
loc: SrcLoc,
|
||||
): ScrDiagnostic {
|
||||
return {
|
||||
code,
|
||||
message: `${construct} needs a mutable JavaScript object model, which native code does not have`,
|
||||
loc,
|
||||
hint: "rewrite it as a class or typed object, or mark this module as a Node island in scriptc.nodeIslands.modules",
|
||||
};
|
||||
}
|
||||
|
||||
/* ── SC5xxx: native FFI ───────────────────────────────────────────────── */
|
||||
|
||||
/** SC5001 — the outbound native-FFI manifest is malformed or unreadable. */
|
||||
@@ -773,6 +854,7 @@ export function nativeAddonDiag(specifier: string, loc: SrcLoc): ScrDiagnostic {
|
||||
loc,
|
||||
milestone: "later",
|
||||
hint: "expose the native operation as a plain C ABI function in an object or static archive, then bind a signature-only TypeScript declaration with --ffi; Node-API callbacks themselves require Node (https://scriptc.dev/ffi#replacing-a-node-api-addon)",
|
||||
reason: "native-addon",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -148,10 +148,60 @@ export interface DiagnosticsEnvelope {
|
||||
/** coverage only: the WebAssembly modules the build can see, with their
|
||||
* size, validity, proposals and the sources that found them. */
|
||||
wasmModules?: WasmModuleReport[];
|
||||
/** coverage only, when there is something to report: the packages and
|
||||
* user modules that would run in a Node island (report-only). */
|
||||
nodeIslands?: NodeIslandsEnvelope;
|
||||
groups: DiagnosticGroup[];
|
||||
diagnostics: LocatedDiagnostic[];
|
||||
}
|
||||
|
||||
/** One reason a package is outside the native subset. */
|
||||
export interface NodeIslandReasonEnvelope {
|
||||
class: string;
|
||||
/** R1.1–R2.5, or null for gap and packaging reasons. */
|
||||
rule: string | null;
|
||||
reason: string;
|
||||
code: string | null;
|
||||
message: string;
|
||||
source: string;
|
||||
at?: SourcePosition;
|
||||
}
|
||||
|
||||
/** `scriptc coverage --print=diagnostics`: the Node island decisions. In
|
||||
* this version they are would-be decisions (mode "would-be"): the build
|
||||
* does not change. */
|
||||
export interface NodeIslandsEnvelope {
|
||||
mode: "would-be";
|
||||
policy: "auto" | "explicit" | "off";
|
||||
/** Gap islands first, then by class and name. */
|
||||
islands: {
|
||||
package: string;
|
||||
version: string | null;
|
||||
path: string | null;
|
||||
class: string;
|
||||
reason: string;
|
||||
diagnostic: string | null;
|
||||
decision: string;
|
||||
evaluation: string;
|
||||
at?: SourcePosition;
|
||||
reasons: NodeIslandReasonEnvelope[];
|
||||
carries: string[];
|
||||
importedFrom: SourcePosition[];
|
||||
}[];
|
||||
userModules: { file: string; decision: string }[];
|
||||
/** Every npm package that is not an island: native, inside an island's
|
||||
* closure, or shared between native code and an island. */
|
||||
npmPackages: {
|
||||
package: string;
|
||||
version: string | null;
|
||||
path: string | null;
|
||||
status: string;
|
||||
islands: string[];
|
||||
}[];
|
||||
violations: (SourcePosition & { code: string; message: string })[];
|
||||
unknownPackages: string[];
|
||||
}
|
||||
|
||||
/** The divergence band: compiles, but the program can behave differently
|
||||
* from Node. Warnings, never build failures. */
|
||||
export function isDivergenceCode(code: string): boolean {
|
||||
@@ -166,6 +216,7 @@ export function categoryOf(diag: ScrDiagnostic): DiagnosticCategory {
|
||||
if (code === "SC0003") return "invalid-source";
|
||||
if (code === "SC0002" || code === "SC0004") return "environment";
|
||||
if (/^SC[35]\d{3}$/.test(code)) return "environment";
|
||||
if (code === "SC8004" || code === "SC8005" || code === "SC8012") return "environment";
|
||||
if (/^SC9\d{3}$/.test(code)) return "internal";
|
||||
return "unsupported";
|
||||
}
|
||||
@@ -186,6 +237,8 @@ export function hintOf(diag: ScrDiagnostic): string {
|
||||
return "restate the value's type inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')";
|
||||
if (/^SC2\d{3}$/.test(code))
|
||||
return "restate the type inside the compilable set (https://scriptc.dev/limitations describes the boundary)";
|
||||
if (/^SC8\d{3}$/.test(code))
|
||||
return "see the Node island policy: npm code outside the native subset runs in Node, user code must be marked (scriptc coverage --explain-island=<package>)";
|
||||
if (/^SC9\d{3}$/.test(code))
|
||||
return "this is a compiler bug — please report it with the program that triggers it";
|
||||
return "see https://scriptc.dev/limitations";
|
||||
@@ -281,7 +334,10 @@ export function locateDiagnostic(
|
||||
return {
|
||||
code: diag.code,
|
||||
category: categoryOf(diag),
|
||||
severity: isDivergenceCode(diag.code) || isPerformanceCode(diag.code) ? "warning" : "error",
|
||||
severity:
|
||||
isDivergenceCode(diag.code) || isPerformanceCode(diag.code) || diag.code === "SC8004"
|
||||
? "warning"
|
||||
: "error",
|
||||
scope,
|
||||
file: diag.loc.file,
|
||||
line: start.line,
|
||||
|
||||
@@ -102,14 +102,12 @@ export function renderDiagnostic(
|
||||
frame.push(
|
||||
c(DIM, ` ${" ".repeat(gutterWidth)} | `) +
|
||||
" ".repeat(caretCol) +
|
||||
c(/^SC6\d{3}$/.test(diag.code) ? YELLOW : RED, "^" + "~".repeat(spanOnLine - 1)),
|
||||
c(isWarningCode(diag.code) ? YELLOW : RED, "^" + "~".repeat(spanOnLine - 1)),
|
||||
);
|
||||
emitLine(lineNum + 1);
|
||||
}
|
||||
|
||||
// The divergence band (SC6xxx) is the one warning severity: the program
|
||||
// compiles, and the site can behave differently from Node.
|
||||
const warning = /^SC6\d{3}$/.test(diag.code);
|
||||
const warning = isWarningCode(diag.code);
|
||||
out.push(
|
||||
`${c(BOLD, `${diag.loc.file}:${lineNum}:${colNum}`)} - ${warning ? c(YELLOW, "warning") : c(RED, "error")} ${c(BOLD, diag.code)}: ${diag.message}`,
|
||||
);
|
||||
@@ -154,6 +152,27 @@ export function renderDiagnostics(
|
||||
|
||||
/** Divergence warnings after a successful build: the rendered sites plus a
|
||||
* one-line count, on stderr. */
|
||||
/** The warning severities: the divergence band (SC6xxx), where the program
|
||||
* compiles and the site can behave differently from Node, and unknown
|
||||
* settings in the project's scriptc configuration (SC8004), which builds do
|
||||
* not use. */
|
||||
export function isWarningCode(code: string): boolean {
|
||||
return /^SC6\d{3}$/.test(code) || code === "SC8004";
|
||||
}
|
||||
|
||||
/** A build's SC8004 warnings: the unknown scriptc settings it ignored. */
|
||||
export function renderConfigWarnings(
|
||||
diags: readonly ScrDiagnostic[],
|
||||
sourceTextByFile: Map<string, string>,
|
||||
opts: RenderOptions = {},
|
||||
): string {
|
||||
const n = diags.length;
|
||||
return (
|
||||
renderDiagnostics([...diags], sourceTextByFile, opts) +
|
||||
`\n\n${n} warning${n === 1 ? "" : "s"}: the build does not use ${n === 1 ? "this setting" : "these settings"}; scriptc coverage reports Node islands once they are valid.`
|
||||
);
|
||||
}
|
||||
|
||||
export function renderWarnings(
|
||||
diags: readonly ScrDiagnostic[],
|
||||
sourceTextByFile: Map<string, string>,
|
||||
|
||||
@@ -69,6 +69,7 @@ const native: EarlyExecutableNativeFeatures = {
|
||||
dh: false,
|
||||
pki: false,
|
||||
ed25519: false,
|
||||
x509: false,
|
||||
webcrypto: false,
|
||||
perfLoop: false,
|
||||
};
|
||||
|
||||
@@ -105,6 +105,7 @@ export interface EarlyExecutableNativeFeatures {
|
||||
dh: boolean;
|
||||
pki: boolean;
|
||||
ed25519: boolean;
|
||||
x509: boolean;
|
||||
perfLoop: boolean;
|
||||
tlsCa: boolean;
|
||||
webcrypto: boolean;
|
||||
@@ -252,6 +253,7 @@ const BOOLEAN_NATIVE_KEYS = [
|
||||
"dh",
|
||||
"pki",
|
||||
"ed25519",
|
||||
"x509",
|
||||
"webcrypto",
|
||||
"perfLoop",
|
||||
] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[];
|
||||
|
||||
@@ -122,6 +122,10 @@ export function prepareExecutableModule(
|
||||
mod: lowered.module!,
|
||||
sourceTexts,
|
||||
stats: lowered.stats,
|
||||
warnings: [...(lowered.divergences ?? []), ...(lowered.performance ?? [])],
|
||||
warnings: [
|
||||
...(fe.warnings ?? []),
|
||||
...(lowered.divergences ?? []),
|
||||
...(lowered.performance ?? []),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ import type { AnalyzeOptions, AnalyzeResult } from "../compile-types.js";
|
||||
import { loadFfiProfile, type FfiProfile } from "../ffi/ffi-manifest.js";
|
||||
import { provenanceSources } from "./provenance-registry.js";
|
||||
import type { FrontendFactory } from "./pipeline.js";
|
||||
import { readProjectConfig } from "../project-config.js";
|
||||
import { nodeIslandPolicy } from "../node-islands/policy.js";
|
||||
import { buildNodeIslandReport } from "../node-islands/report.js";
|
||||
import { hasNodeIslandFindings } from "../node-islands/summary.js";
|
||||
|
||||
/** Analysis without codegen: how much of the program compiles statically.
|
||||
* Unlike compile(), lowering diagnostics are data here, not failure. */
|
||||
@@ -34,7 +38,9 @@ export function analyzeWithFrontend(
|
||||
}
|
||||
ffi = loaded.profile;
|
||||
}
|
||||
const fe = createFrontend(entryPath, opts.npmStatic, opts.externalTypes);
|
||||
const fe = createFrontend(entryPath, opts.npmStatic, opts.externalTypes, undefined, {
|
||||
nodeIslands: true,
|
||||
});
|
||||
try {
|
||||
const emptyStats = {
|
||||
statementsTotal: 0,
|
||||
@@ -96,6 +102,23 @@ export function analyzeWithFrontend(
|
||||
...(opts.wasm !== undefined && opts.wasm.length > 0 ? { wasmFiles: opts.wasm } : {}),
|
||||
});
|
||||
const provenance = provenanceSources();
|
||||
// Node islands, report-only: which packages and modules would run in
|
||||
// Node, from the detector's scan and the policy. Code generation does
|
||||
// not depend on it.
|
||||
const policy = nodeIslandPolicy(readProjectConfig(entryPath), opts.nodeIslands);
|
||||
const islandReport =
|
||||
fe.nodeIslandScan === undefined
|
||||
? null
|
||||
: buildNodeIslandReport({
|
||||
entry: entryPath,
|
||||
scan: fe.nodeIslandScan({ userJs: policy.strictUserJs }),
|
||||
npm: fe.npmStatic,
|
||||
diagnostics: [...lowered.diagnostics, ...(lowered.unreached?.diagnostics ?? [])],
|
||||
fences: [...lowered.runtimeFences, ...(lowered.unreached?.runtimeFences ?? [])],
|
||||
policy,
|
||||
});
|
||||
const nodeIslands =
|
||||
islandReport !== null && hasNodeIslandFindings(islandReport) ? islandReport : null;
|
||||
return {
|
||||
coverage: {
|
||||
file: entryPath,
|
||||
@@ -103,7 +126,9 @@ export function analyzeWithFrontend(
|
||||
// The import fences report as blockers alongside the statement-level
|
||||
// ones (use sites of the fenced bindings emit matching diagnostics,
|
||||
// which the report groups with these).
|
||||
diagnostics: [...preflight, ...lowered.diagnostics],
|
||||
// --strict-user-js: R2 constructs in user JavaScript are blockers
|
||||
// (SC8101–SC8105), as scope B's builds will make them errors.
|
||||
diagnostics: [...preflight, ...lowered.diagnostics, ...(nodeIslands?.userJs ?? [])],
|
||||
...(lowered.runtimeFences.length > 0 ? { runtimeFences: lowered.runtimeFences } : {}),
|
||||
...(lowered.divergences !== undefined ? { divergences: lowered.divergences } : {}),
|
||||
...(lowered.performance !== undefined ? { performance: lowered.performance } : {}),
|
||||
@@ -116,6 +141,7 @@ export function analyzeWithFrontend(
|
||||
...(lowered.provenanceElided ? { provenanceElided: lowered.provenanceElided } : {}),
|
||||
...(lowered.tiers !== undefined ? { tiers: lowered.tiers } : {}),
|
||||
...(lowered.wasmModules !== undefined ? { wasmModules: lowered.wasmModules } : {}),
|
||||
...(nodeIslands !== null ? { nodeIslands } : {}),
|
||||
preflightFailed: false,
|
||||
},
|
||||
sourceTexts: fe.sourceTexts(),
|
||||
|
||||
@@ -65,3 +65,56 @@ test("fork discovery batches only candidate bindings and preserves aliases and s
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
/** forkTargetPaths over the entry of a program made of `files`, and
|
||||
* whether it asked the checker for any binding. */
|
||||
function discover(files: Record<string, string>): { targets: string[]; queried: boolean } {
|
||||
const directory = mkdtempSync(join(tmpdir(), "scriptc-fork-gate-"));
|
||||
for (const [name, text] of Object.entries(files)) writeFileSync(join(directory, name), text);
|
||||
const entry = join(directory, "main.ts");
|
||||
const host = new Ts7Host((options) => new Ts7Api(options), { cwd: directory });
|
||||
const program = host.createProgram([entry, fallbackDtsPath()], {
|
||||
noLib: true,
|
||||
types: [],
|
||||
module: ModuleKind.ESNext,
|
||||
moduleResolution: ModuleResolutionKind.Bundler,
|
||||
});
|
||||
const batch = vi.spyOn(program.getTypeChecker(), "prefetchSymbolNodesExact");
|
||||
try {
|
||||
const targets = forkTargetPaths(program, [program.getSourceFile(entry)!]).map((path) =>
|
||||
path.slice(directory.length + 1),
|
||||
);
|
||||
return { targets, queried: batch.mock.calls.length > 0 };
|
||||
} finally {
|
||||
batch.mockRestore();
|
||||
program.dispose();
|
||||
host.close();
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test("fork discovery asks the checker nothing when no file names node:child_process", () => {
|
||||
expect(
|
||||
discover({
|
||||
"main.ts":
|
||||
'function fork(url: URL) { return url; }\nfork(new URL("./child.ts", import.meta.url));\n',
|
||||
}),
|
||||
).toEqual({ targets: [], queried: false });
|
||||
});
|
||||
|
||||
test("fork discovery reads a module specifier spelled with escapes", () => {
|
||||
expect(
|
||||
discover({
|
||||
"main.ts":
|
||||
'import { fork } from "node:child\\u005fprocess";\nfork(new URL("./child.ts", import.meta.url));\n',
|
||||
}),
|
||||
).toEqual({ targets: ["child.ts"], queried: true });
|
||||
});
|
||||
|
||||
test("fork discovery reads a package imports entry that maps node:child_process", () => {
|
||||
const result = discover({
|
||||
"package.json": JSON.stringify({ type: "module", imports: { "#cp": "node:child_process" } }),
|
||||
"main.ts": 'import { fork } from "#cp";\nfork(new URL("./child.ts", import.meta.url));\n',
|
||||
});
|
||||
expect(result.queried).toBe(true);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
import { fallbackDtsPath, isNodeTypesPath, tsgoPath } from "./dts-paths.js";
|
||||
import {
|
||||
ambientDtsPath,
|
||||
fallbackDtsPath,
|
||||
isNodeTypesPath,
|
||||
overridesDtsPath,
|
||||
tsgoPath,
|
||||
} from "./dts-paths.js";
|
||||
import { nearestPackageImports } from "./resolve.js";
|
||||
import { canonicalBuiltinModule } from "./builtin-modules.js";
|
||||
import { moduleSourceCandidates } from "./module-source-candidates.js";
|
||||
|
||||
@@ -292,9 +299,77 @@ export function forkCallModulePath(program: ts.Program, call: ts.CallExpression)
|
||||
return staticForkModulePath(program, call.arguments.nodes[0]);
|
||||
}
|
||||
|
||||
/** The ways a string or template literal can spell one character of a
|
||||
* module name: the character, \xHH, \uHHHH, \u{H…}, a legacy octal escape,
|
||||
* or the character after a backslash where that is not another escape. */
|
||||
function spelledCharacter(ch: string): string {
|
||||
const code = ch.charCodeAt(0);
|
||||
const hex = code.toString(16);
|
||||
const forms = [
|
||||
ch,
|
||||
"\\\\x" + hex.padStart(2, "0"),
|
||||
"\\\\u" + hex.padStart(4, "0"),
|
||||
"\\\\u\\{0*" + hex + "\\}",
|
||||
"\\\\" + code.toString(8),
|
||||
];
|
||||
if (!"bfnrtvux0123456789".includes(ch)) forms.push("\\\\" + ch);
|
||||
return "(?:" + forms.join("|") + ")";
|
||||
}
|
||||
|
||||
/** "child_process" with escapes and line continuations in any of its
|
||||
* characters (case-insensitive, a superset). */
|
||||
const SPELLED_CHILD_PROCESS = new RegExp(
|
||||
[..."child_process"].map(spelledCharacter).join("(?:\\\\(?:\\r\\n|[\\n\\r\\u2028\\u2029]))*"),
|
||||
"i",
|
||||
);
|
||||
|
||||
/** Whether a call in the program can reach node:child_process's fork. A
|
||||
* fork binding is an import or alias whose chain names the module in a
|
||||
* module specifier (a string), or a '#' specifier the nearest package.json
|
||||
* "imports" field maps. Node's type declarations and scriptc's own declare
|
||||
* the module, and alias it only as node:child_process, so they are not
|
||||
* read; any other file that names the module in a string holds
|
||||
* "child_process" in its text, or an escape that spells it. */
|
||||
function namesChildProcess(program: ts.Program): boolean {
|
||||
const known = program.analysis.namesChildProcess;
|
||||
if (known !== null) return known;
|
||||
const own = new Set([ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]);
|
||||
// Implementation files are already fetched; any other file is read as
|
||||
// the host serves it, without fetching its syntax tree.
|
||||
const texts = new Map<string, string>();
|
||||
for (const sourceFile of program.getImplementationSourceFiles())
|
||||
texts.set(tsgoPath(sourceFile.fileName), sourceFile.text);
|
||||
const scopes = new Set<unknown>();
|
||||
let names = false;
|
||||
for (const fileName of program.getSourceFileNames()) {
|
||||
const path = tsgoPath(fileName);
|
||||
if (isNodeTypesPath(fileName) || own.has(path)) continue;
|
||||
const text = texts.get(path) ?? program.getSourceText(fileName) ?? "";
|
||||
if (
|
||||
text.includes("child_process") ||
|
||||
(text.includes("\\") && SPELLED_CHILD_PROCESS.test(text))
|
||||
) {
|
||||
names = true;
|
||||
break;
|
||||
}
|
||||
const imports = nearestPackageImports(fileName);
|
||||
if (imports === undefined || scopes.has(imports)) continue;
|
||||
scopes.add(imports);
|
||||
if (JSON.stringify(imports).includes("child_process")) {
|
||||
names = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
program.analysis.namesChildProcess = names;
|
||||
return names;
|
||||
}
|
||||
|
||||
/** Source-order static fork targets in the supplied files, deduplicated by
|
||||
* resolved path. */
|
||||
export function forkTargetPaths(program: ts.Program, files: readonly ts.SourceFile[]): string[] {
|
||||
// Without a binding of node:child_process no call is a fork, and the
|
||||
// checker queries below are skipped.
|
||||
if (!namesChildProcess(program)) return [];
|
||||
const targets: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const sourceFile of files) {
|
||||
|
||||
@@ -193,8 +193,7 @@ export function usedAsValue(
|
||||
const symbol = symbolAt(name);
|
||||
if (!symbol) return false;
|
||||
let found = false;
|
||||
for (const sf of lowerer.program.getSourceFiles()) {
|
||||
if (sf.isDeclarationFile) continue;
|
||||
for (const sf of lowerer.program.getImplementationSourceFiles()) {
|
||||
ts.walkPreorder(sf, (n) => {
|
||||
if (found) return "stop";
|
||||
if (ts.isTypeNode(n)) return "skip";
|
||||
|
||||
@@ -312,6 +312,140 @@ function readsMutableLocal(lowerer: Lowerer, e: IrExpr): boolean {
|
||||
});
|
||||
}
|
||||
|
||||
/** A number, string or boolean element type. Arrays of it hold a present
|
||||
* undefined in the element's state, so `(T | undefined)[]` stores T
|
||||
* elements (the type mapper) and a T[] seen as one stays the same array. */
|
||||
export function primitiveElement(elem: IrType): boolean {
|
||||
return elem.kind === "f64" || elem.kind === "string" || elem.kind === "bool";
|
||||
}
|
||||
|
||||
/** `value` is the element type with undefined, stored into an array of a
|
||||
* primitive element type: a present undefined or the element. */
|
||||
export function optionalElementValue(lowerer: Lowerer, value: IrExpr, elem: IrType): boolean {
|
||||
if (!primitiveElement(elem)) return false;
|
||||
if (value.type.kind === "undefinedT") return true;
|
||||
return (
|
||||
value.type.kind === "union" &&
|
||||
lowerer.armTag(value.type.unionId, UNDEFINED_T) >= 0 &&
|
||||
typeEquals(lowerer.stripUndefinedArm(value.type), elem)
|
||||
);
|
||||
}
|
||||
|
||||
/** A value of a primitive element type that holds a slot until the value
|
||||
* stored there replaces it (insertedValues), or stands for a slot that is
|
||||
* not read. */
|
||||
export function elementPlaceholder(elem: IrType, loc: SrcLoc): IrExpr {
|
||||
if (elem.kind === "f64") return { kind: "numLit", value: 0, type: F64, loc };
|
||||
if (elem.kind === "bool") return { kind: "boolLit", value: false, type: BOOL, loc };
|
||||
return { kind: "strLit", value: "", type: STRING, loc };
|
||||
}
|
||||
|
||||
/** Values inserted into an array (push, unshift, splice, a literal), in
|
||||
* source order. A value that can be undefined (optionalElementValue) is
|
||||
* kept in a temporary: the insertion takes a placeholder for it, and
|
||||
* `fill` then stores the value at the placeholder's index. */
|
||||
export function insertedValues(
|
||||
lowerer: Lowerer,
|
||||
values: readonly IrExpr[],
|
||||
elem: IrType,
|
||||
coerce: (value: IrExpr, index: number) => IrExpr,
|
||||
loc: SrcLoc,
|
||||
): {
|
||||
stmts: IrStmt[];
|
||||
args: IrExpr[];
|
||||
optional: boolean;
|
||||
fill: (arr: IrExpr, first: IrExpr) => IrStmt[];
|
||||
} {
|
||||
if (!values.some((value) => optionalElementValue(lowerer, value, elem)))
|
||||
return { stmts: [], args: values.map(coerce), optional: false, fill: () => [] };
|
||||
const stmts: IrStmt[] = [];
|
||||
const args: IrExpr[] = [];
|
||||
const held: { offset: number; value: IrExpr }[] = [];
|
||||
values.forEach((value, offset) => {
|
||||
// Every value is evaluated in order before the insertion.
|
||||
if (value.type.kind === "undefinedT") {
|
||||
if (value.kind !== "unitLit") stmts.push({ kind: "exprStmt", expr: value, loc });
|
||||
args.push(elementPlaceholder(elem, loc));
|
||||
held.push({ offset, value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc } });
|
||||
return;
|
||||
}
|
||||
const temp = lowerer.declareHiddenLocal("%inserted", value.type);
|
||||
stmts.push({ kind: "varDecl", localId: temp.id, init: value, loc });
|
||||
const stable = varRef(temp.id, temp.type, loc);
|
||||
if (optionalElementValue(lowerer, value, elem)) {
|
||||
args.push(elementPlaceholder(elem, loc));
|
||||
held.push({ offset, value: stable });
|
||||
} else {
|
||||
args.push(coerce(stable, offset));
|
||||
}
|
||||
});
|
||||
const fill = (arr: IrExpr, first: IrExpr): IrStmt[] =>
|
||||
held.map(({ offset, value }) =>
|
||||
arrayValueStore(
|
||||
lowerer,
|
||||
arr,
|
||||
offset === 0
|
||||
? first
|
||||
: {
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left: first,
|
||||
right: { kind: "numLit", value: offset, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
value,
|
||||
elem,
|
||||
loc,
|
||||
),
|
||||
);
|
||||
return { stmts, args, optional: true, fill };
|
||||
}
|
||||
|
||||
/** Statements appending `value` to `arr` inside a lifted helper. A value
|
||||
* that can be undefined in a primitive array (optionalElementValue) is held
|
||||
* in the helper's local `slot` (which the caller declares with the value's
|
||||
* type) and stored as a present undefined or the element; any other value
|
||||
* converts to the element type. */
|
||||
export function appendElement(
|
||||
lowerer: Lowerer,
|
||||
arr: IrExpr,
|
||||
value: IrExpr,
|
||||
elem: IrType,
|
||||
slot: string,
|
||||
loc: SrcLoc,
|
||||
): IrStmt[] {
|
||||
if (optionalElementValue(lowerer, value, elem) && value.type.kind !== "undefinedT") {
|
||||
const held = varRef(slot, value.type, loc);
|
||||
const length: IrExpr = {
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: arr,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
return [
|
||||
{ kind: "varDecl", localId: slot, init: value, loc },
|
||||
arrayValueStore(lowerer, arr, length, held, elem, loc),
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: arr,
|
||||
args: [lowerer.coerceToExpected(value, elem)],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** Store a stabilized value without changing the array's element ABI. */
|
||||
export function arrayValueStore(
|
||||
lowerer: Lowerer,
|
||||
@@ -322,6 +456,12 @@ export function arrayValueStore(
|
||||
loc: SrcLoc,
|
||||
): IrStmt {
|
||||
if (typeEquals(value.type, elem)) return { kind: "arraySet", arr, index, value, loc };
|
||||
// An undefined value of a `(T | undefined)[]` (primitiveElement).
|
||||
if (value.type.kind === "undefinedT" && primitiveElement(elem)) {
|
||||
const write: IrStmt = { kind: "arraySetUndefined", arr, index, loc };
|
||||
if (value.kind === "unitLit") return write;
|
||||
return { kind: "block", body: [{ kind: "exprStmt", expr: value, loc }, write], loc };
|
||||
}
|
||||
if (value.type.kind === "union" && lowerer.armTag(value.type.unionId, UNDEFINED_T) >= 0) {
|
||||
// The tag test and the VALUE arm extraction both consume the value. Keep
|
||||
// an arbitrary expression (especially an array read or call) single
|
||||
|
||||
@@ -2,7 +2,7 @@ import { dynUndefinedExpr, boolLit, numLit, varRef } from "../../../ir/build.js"
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { lowerFsPath } from "./fs-paths.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import {
|
||||
type BuiltinModuleFn,
|
||||
FS_WRITE_FILE_DOCUMENTED_OPTIONS,
|
||||
@@ -403,6 +403,28 @@ export function lowerFsReadSyncCall(
|
||||
}
|
||||
lowerer.noLowering("readSync with a non-literal options argument", optionsNode, supported);
|
||||
}
|
||||
// JavaScript arguments outside the typed signature (an untyped fd or
|
||||
// buffer, a bigint or untyped position) take Node's argument checks at
|
||||
// run time, then the same read.
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(expr))) {
|
||||
const nodes = expr.arguments.nodes;
|
||||
const typed = [F64, BYTES_U8, F64, F64, F64].every((want, i) => {
|
||||
const node = nodes[i];
|
||||
if (node === undefined) return true;
|
||||
let value: ts.Expression = node;
|
||||
while (ts.isParenthesizedExpression(value)) value = value.expression;
|
||||
if (i === 4 && value.kind === ts.SyntaxKind.NullKeyword) return true;
|
||||
const t = lowerer.mapTypeOf(lowerer.typeOf(node));
|
||||
return t !== null && (want.kind === "bytes" ? t.kind === "bytes" : t.kind === "f64");
|
||||
});
|
||||
if (!typed) {
|
||||
const args = [0, 1, 2, 3, 4].map((i): IrExpr => {
|
||||
const node = nodes[i];
|
||||
return node === undefined ? dynUndefinedExpr(loc) : lowerer.lowerExprExpecting(node, DYN);
|
||||
});
|
||||
return { kind: "libCall", fn: "fs.readSyncDyn", args, type: F64, loc };
|
||||
}
|
||||
}
|
||||
const args: IrExpr[] = [
|
||||
lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, F64),
|
||||
lowerer.lowerExprExpecting(expr.arguments.nodes[1]!, BYTES_U8),
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* and everything inherited goes through the stream spoke. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { isNodeTypesPath } from "../../dts-paths.js";
|
||||
import { dynUndefinedExpr } from "../../../ir/build.js";
|
||||
import {
|
||||
@@ -178,3 +178,32 @@ export function lowerFsStreamNew(
|
||||
locOf(expr),
|
||||
);
|
||||
}
|
||||
|
||||
/** `fs.ReadStream(path, options)` / `fs.WriteStream(...)` called without
|
||||
* `new` in JavaScript: Node's stream constructors construct when called as
|
||||
* functions, so the call is the `new` form. Null for other calls. */
|
||||
export function lowerFsStreamCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr | null {
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null;
|
||||
const callee = expr.expression;
|
||||
const name = ts.isPropertyAccessExpression(callee)
|
||||
? callee.name
|
||||
: ts.isIdentifier(callee)
|
||||
? callee
|
||||
: null;
|
||||
if (name === null || !ts.isIdentifier(name)) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(name);
|
||||
const resolved =
|
||||
symbol && symbol.flags & ts.SymbolFlags.Alias
|
||||
? lowerer.checker.getAliasedSymbol(symbol)
|
||||
: symbol;
|
||||
const cls = fsStreamClassOf(lowerer, resolved);
|
||||
if (cls === null) return null;
|
||||
return fsStreamCreateExpr(
|
||||
lowerer,
|
||||
expr.arguments.nodes,
|
||||
cls === "ReadStream",
|
||||
`fs.${cls}`,
|
||||
expr,
|
||||
locOf(expr),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -239,7 +239,9 @@ export function builtinNamespaceDestructureModuleOf(
|
||||
if (ts.isCallExpression(init)) {
|
||||
const cr = createRequireSpecOf(lowerer, init);
|
||||
module = cr !== null && cr.spec !== null ? canonicalBuiltinModule(cr.spec) : null;
|
||||
} else if (ts.isIdentifier(init)) {
|
||||
} else if (ts.isIdentifier(init) || ts.isPropertyAccessExpression(init)) {
|
||||
// `const { open } = fs.promises` / `= require("fs").promises`: a member
|
||||
// that is itself a supported module.
|
||||
module = lowerer.builtinNamespaceModuleOf(init);
|
||||
}
|
||||
if (module === null) return null;
|
||||
|
||||
@@ -237,7 +237,8 @@ export function lowerCreateRequireCall(
|
||||
const surface = "createRequire's require with a computed specifier or this argument shape";
|
||||
const hint =
|
||||
'the compiled module graph is fixed at build time — the one lowered form is require("<static string literal>"); native addons are not loaded';
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(call))) lowerer.noLowering(surface, call, hint);
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(call)))
|
||||
lowerer.noLowering(surface, call, hint, undefined, "dynamic-require");
|
||||
return deferredCreateRequireThrow(lowerer, call, surface, hint, loc);
|
||||
}
|
||||
const spec = cr.spec;
|
||||
|
||||
@@ -3,7 +3,8 @@ import { checkedPromiseAll, thenReader } from "../checked-promise-all.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { countedFor, varRef } from "../../../ir/build.js";
|
||||
import { arrayValueStore, optionalElementValue, primitiveElement } from "../array-values.js";
|
||||
import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import {
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
UNDEFINED_T,
|
||||
VOID,
|
||||
arrayOf,
|
||||
@@ -435,6 +437,28 @@ export function lowerPromiseStaticCall(
|
||||
inner.kind === "void"
|
||||
? { kind: "promise", inner: VOID }
|
||||
: lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
// Promise<T | undefined>[] combines into a `(T | undefined)[]`, which
|
||||
// stores T elements (primitiveElement): collect the values, then copy
|
||||
// them in with their present undefined values.
|
||||
if (
|
||||
resultT?.kind === "promise" &&
|
||||
resultT.inner.kind === "array" &&
|
||||
primitiveElement(resultT.inner.elem) &&
|
||||
optionalElementValue(
|
||||
lowerer,
|
||||
{ kind: "unitLit", unit: "undefined", type: inner, loc },
|
||||
resultT.inner.elem,
|
||||
)
|
||||
) {
|
||||
const joined = optionalPromiseAll(
|
||||
lowerer,
|
||||
entries,
|
||||
inner,
|
||||
resultT as IrType & { kind: "promise" },
|
||||
loc,
|
||||
);
|
||||
if (joined) return joined;
|
||||
}
|
||||
if (
|
||||
resultT?.kind !== "promise" ||
|
||||
(inner.kind !== "void" &&
|
||||
@@ -954,3 +978,139 @@ export function lowerAwaitedUniformPromiseAll(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `Promise.all(ps)` over `Promise<T | undefined>[]` for a number, string or
|
||||
* boolean T, whose result is a `(T | undefined)[]` (stored as T elements).
|
||||
* The combinator joins the entries (promise.allTuple), and its builder
|
||||
* reads each fulfilled entry into the result with its present undefined,
|
||||
* inside the combinator's settlement, so the result settles on Node's
|
||||
* turn. Entries of another type keep the general combinator. */
|
||||
function optionalPromiseAll(
|
||||
lowerer: Lowerer,
|
||||
entries: IrExpr,
|
||||
inner: IrType,
|
||||
resultT: IrType & { kind: "promise" },
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const entriesT = entries.type;
|
||||
if (entriesT.kind !== "array" || entriesT.elem.kind !== "promise") return null;
|
||||
if (!typeEquals(entriesT.elem.inner, inner)) return null;
|
||||
const outT = resultT.inner as IrType & { kind: "array" };
|
||||
const voidPromise: IrType & { kind: "promise" } = { kind: "promise", inner: VOID };
|
||||
const joinedT = arrayOf(voidPromise);
|
||||
const key = `promise.allOptional:${typeKey(entriesT)}:${typeKey(outT)}`;
|
||||
let helper = lowerer.arrHofHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = keyedHelperName(lowerer, "%promise.allOptional", key);
|
||||
lowerer.arrHofHelpers.set(key, helper);
|
||||
const builder = keyedHelperName(lowerer, "%promise.allOptionalValues", key);
|
||||
const length = (arr: IrExpr): IrExpr => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: arr,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const joined = varRef("joined.0", joinedT, loc);
|
||||
const out = varRef("out.0", outT, loc);
|
||||
lowerer.liftedFns.push({
|
||||
name: builder,
|
||||
params: [{ localId: "joined.0", name: "joined", type: joinedT }],
|
||||
returnType: outT,
|
||||
locals: [
|
||||
{ id: "joined.0", name: "joined", type: joinedT, mutable: false },
|
||||
{ id: "out.0", name: "out", type: outT, mutable: false },
|
||||
{ id: "i.0", name: "i", type: F64, mutable: true },
|
||||
{ id: "v.0", name: "v", type: inner, mutable: false },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "out.0",
|
||||
init: { kind: "arrayLit", elems: [], type: outT, loc },
|
||||
loc,
|
||||
},
|
||||
countedFor(loc, length(joined), (index) => [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "v.0",
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "promise.fulfilledValue",
|
||||
args: [{ kind: "arrayGet", arr: joined, index, type: voidPromise, loc }],
|
||||
type: inner,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
arrayValueStore(lowerer, out, index, varRef("v.0", inner, loc), outT.elem, loc),
|
||||
]),
|
||||
{ kind: "return", value: out, loc },
|
||||
],
|
||||
loc,
|
||||
});
|
||||
const ps = varRef("ps.0", entriesT, loc);
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [{ localId: "ps.0", name: "ps", type: entriesT }],
|
||||
returnType: resultT,
|
||||
locals: [
|
||||
{ id: "ps.0", name: "ps", type: entriesT, mutable: false },
|
||||
{ id: "joined.0", name: "joined", type: joinedT, mutable: false },
|
||||
{ id: "i.0", name: "i", type: F64, mutable: true },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "joined.0",
|
||||
init: { kind: "arrayLit", elems: [], type: joinedT, loc },
|
||||
loc,
|
||||
},
|
||||
countedFor(loc, length(ps), (index) => [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: joined,
|
||||
args: [
|
||||
{
|
||||
kind: "promiseVoidWiden",
|
||||
value: { kind: "arrayGet", arr: ps, index, type: entriesT.elem, loc },
|
||||
type: voidPromise,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
]),
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "promise.allTuple",
|
||||
args: [
|
||||
joined,
|
||||
{
|
||||
kind: "closure",
|
||||
fnName: builder,
|
||||
captures: [],
|
||||
type: funcOf([joinedT], outT),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: resultT,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [entries], type: resultT, loc };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,488 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import {
|
||||
BOOL,
|
||||
BYTES_U8,
|
||||
DYN,
|
||||
F64,
|
||||
STRING,
|
||||
UNDEFINED_T,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLibFn,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
} from "../../../ir/ir.js";
|
||||
import { everyStmtList } from "../../../ir/traverse.js";
|
||||
import { unsupportedDiag } from "../../../diagnostics/diagnostic.js";
|
||||
import { X509_DER_FIELD, X509_PUBLIC_KEY_FIELD } from "../../type-mapper.js";
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import { x509RecordCall } from "../lower-classes.js";
|
||||
|
||||
/* node:crypto's X509Certificate members beyond its fields: the methods
|
||||
* and the publicKey getter. A certificate is the Dirent-style record
|
||||
* (type-mapper.ts), and each member reads the record's hidden copy of its
|
||||
* DER, which writes into the `raw` Buffer leave alone, as in Node. checkHost,
|
||||
* checkEmail, checkIP, checkIssued, toLegacyObject and the PEM of
|
||||
* toString/toJSON run in scr_x509.c;
|
||||
* publicKey, verify and checkPrivateKey read keys in scr_pki.c. The first
|
||||
* publicKey read stores the KeyObject in the record's hidden slot, so
|
||||
* every read answers the same object, as in Node. */
|
||||
|
||||
/** Whether `t` is the X509Certificate record (its hidden publicKey slot
|
||||
* marks the shape; no program can name that field). */
|
||||
export function isX509Type(lowerer: Lowerer, t: IrType | null | undefined): boolean {
|
||||
return (
|
||||
t?.kind === "record" &&
|
||||
lowerer.shapes.get(t.shapeId)?.fields.some((f) => f.name === X509_PUBLIC_KEY_FIELD) === true
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether `node` is an X509Certificate record. */
|
||||
function isX509Record(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
const type = lowerer.typeOf(node);
|
||||
const sym = type.getSymbol();
|
||||
if (!sym || ts.symbolName(sym) !== "X509Certificate" || !lowerer.isStdlibSymbol(sym))
|
||||
return false;
|
||||
return isX509Type(lowerer, lowerer.mapTypeOf(type));
|
||||
}
|
||||
|
||||
/** A certificate in an `unknown` or `any` slot would cross as a plain
|
||||
* object of its fields, which Node's certificate is not: its fields are
|
||||
* prototype getters, and toString() and toJSON() give its PEM. Every such
|
||||
* conversion in the lowered program, including a certificate inside an
|
||||
* array or record that converts, is refused where it happens. */
|
||||
export function refuseX509DynValues(lowerer: Lowerer, functions: readonly IrFunction[]): void {
|
||||
// Certificates exist only where `new X509Certificate` built one.
|
||||
if (
|
||||
!lowerer.valueHelpers.has("x509.record:bytes") &&
|
||||
!lowerer.valueHelpers.has("x509.record:str") &&
|
||||
!lowerer.valueHelpers.has("x509.record:der")
|
||||
)
|
||||
return;
|
||||
const seen = new Map<string, boolean>();
|
||||
const holds = (t: IrType): boolean => {
|
||||
switch (t.kind) {
|
||||
case "record": {
|
||||
const known = seen.get(t.shapeId);
|
||||
if (known !== undefined) return known;
|
||||
seen.set(t.shapeId, false);
|
||||
const shape = lowerer.shapes.get(t.shapeId);
|
||||
const result =
|
||||
isX509Type(lowerer, t) ||
|
||||
(shape !== undefined &&
|
||||
(shape.fields.some((f) => holds(f.type)) ||
|
||||
(shape.indexValue !== undefined && holds(shape.indexValue))));
|
||||
seen.set(t.shapeId, result);
|
||||
return result;
|
||||
}
|
||||
case "array":
|
||||
case "set":
|
||||
return holds(t.elem);
|
||||
case "map":
|
||||
return holds(t.key) || holds(t.value);
|
||||
case "promise":
|
||||
return holds(t.inner);
|
||||
case "union":
|
||||
return lowerer.unions.get(t.unionId)?.arms.some(holds) === true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const visitor = {
|
||||
expr: (e: IrExpr): boolean => {
|
||||
if (e.kind === "dynFrom" && holds(e.value.type)) {
|
||||
lowerer.pushDiag(
|
||||
unsupportedDiag(
|
||||
"SC1090",
|
||||
e.loc,
|
||||
"X509Certificate values in 'unknown' or 'any' slots (Node keeps a certificate's fields as prototype getters, and its toString() and toJSON() give the PEM; keep the X509Certificate type)",
|
||||
),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
stmt: (): boolean => true,
|
||||
};
|
||||
for (const fn of functions) everyStmtList(fn.body, visitor);
|
||||
}
|
||||
|
||||
/** String(x509), `${x509}` and x509 + "": the certificate's toString(),
|
||||
* its PEM. */
|
||||
export function x509ToString(record: IrExpr, loc: SrcLoc): IrExpr {
|
||||
return { kind: "libCall", fn: "crypto.x509Pem", args: [derOf(record, loc)], type: STRING, loc };
|
||||
}
|
||||
|
||||
/** Node keeps a certificate's fields as prototype getters, so own-key
|
||||
* walks (Object.keys/values/entries, for-in, spread, hasOwnProperty) see
|
||||
* none of them. Those walks over a certificate fence instead of listing
|
||||
* the fields. */
|
||||
export function fenceX509KeyWalk(
|
||||
lowerer: Lowerer,
|
||||
t: IrType | null | undefined,
|
||||
node: ts.Node,
|
||||
what: string,
|
||||
): void {
|
||||
if (!isX509Type(lowerer, t)) return;
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
`${what} over X509Certificate values (Node keeps their fields as prototype getters, so own-key walks see none; read the fields)`,
|
||||
);
|
||||
}
|
||||
|
||||
function derOf(record: IrExpr, loc: SrcLoc): IrExpr {
|
||||
if (record.type.kind !== "record") return record;
|
||||
return {
|
||||
kind: "recordGet",
|
||||
obj: record,
|
||||
shapeId: record.type.shapeId,
|
||||
field: X509_DER_FIELD,
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
const dynUndefined = (loc: SrcLoc): IrExpr => ({
|
||||
kind: "dynFrom",
|
||||
value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
|
||||
/** The method's string answer as `string | undefined`: "" is undefined.
|
||||
* A lifted helper takes the text once. */
|
||||
function optionalString(lowerer: Lowerer, text: IrExpr, type: IrType, loc: SrcLoc): IrExpr {
|
||||
if (type.kind !== "union") return text;
|
||||
const def = lowerer.unions.get(type.unionId);
|
||||
const stringTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
|
||||
const undefinedTag = def ? def.arms.findIndex((a) => a.kind === "undefinedT") : -1;
|
||||
if (stringTag < 0 || undefinedTag < 0) return text;
|
||||
const key = `x509.optional:${type.unionId}`;
|
||||
let helper = lowerer.valueHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = keyedHelperName(lowerer, "%x509.optional", key);
|
||||
lowerer.valueHelpers.set(key, helper);
|
||||
const s: IrExpr = { kind: "varRef", localId: "s.0", type: STRING, loc };
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [{ localId: "s.0", name: "s", type: STRING }],
|
||||
returnType: type,
|
||||
locals: [{ id: "s.0", name: "s", type: STRING, mutable: false }],
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left: s,
|
||||
right: { kind: "strLit", value: "", type: STRING, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: {
|
||||
kind: "unionWrap",
|
||||
unionId: type.unionId,
|
||||
tag: undefinedTag,
|
||||
value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
|
||||
type,
|
||||
loc,
|
||||
},
|
||||
else_: {
|
||||
kind: "unionWrap",
|
||||
unionId: type.unionId,
|
||||
tag: stringTag,
|
||||
value: s,
|
||||
type,
|
||||
loc,
|
||||
},
|
||||
type,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [text], type, loc };
|
||||
}
|
||||
|
||||
/** `x509.publicKey`: the cached KeyObject (a lifted helper fills the
|
||||
* hidden slot on the first read). */
|
||||
export function lowerX509PublicKey(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (access.name.text !== "publicKey" || access.questionDotToken) return null;
|
||||
if (!isX509Record(lowerer, access.expression)) return null;
|
||||
const loc = locOf(access);
|
||||
const record = lowerer.lowerExpr(access.expression);
|
||||
if (record.type.kind !== "record") return null;
|
||||
const shapeId = record.type.shapeId;
|
||||
const key = `x509.publicKey:${shapeId}`;
|
||||
let helper = lowerer.valueHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = keyedHelperName(lowerer, "%x509.publicKey", key);
|
||||
lowerer.valueHelpers.set(key, helper);
|
||||
const c: IrExpr = { kind: "varRef", localId: "c.0", type: record.type, loc };
|
||||
const slot: IrExpr = {
|
||||
kind: "recordGet",
|
||||
obj: c,
|
||||
shapeId,
|
||||
field: X509_PUBLIC_KEY_FIELD,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [{ localId: "c.0", name: "c", type: record.type }],
|
||||
returnType: DYN,
|
||||
locals: [{ id: "c.0", name: "c", type: record.type, mutable: false }],
|
||||
body: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: "undefined", value: slot, type: BOOL, loc },
|
||||
then: [
|
||||
{
|
||||
kind: "recordSet",
|
||||
obj: c,
|
||||
shapeId,
|
||||
field: X509_PUBLIC_KEY_FIELD,
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "crypto.x509PublicKey",
|
||||
args: [derOf(c, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: slot, loc },
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [record], type: DYN, loc };
|
||||
}
|
||||
|
||||
type X509Method = { fn: IrLibFn; min: number; max: number; form: string };
|
||||
|
||||
const METHODS: ReadonlyMap<string, X509Method> = new Map<string, X509Method>([
|
||||
["verify", { fn: "crypto.x509Verify", min: 1, max: 1, form: "verify(publicKey)" }],
|
||||
[
|
||||
"checkPrivateKey",
|
||||
{ fn: "crypto.x509CheckPrivateKey", min: 1, max: 1, form: "checkPrivateKey(privateKey)" },
|
||||
],
|
||||
["checkIssued", { fn: "crypto.x509CheckIssued", min: 1, max: 1, form: "checkIssued(otherCert)" }],
|
||||
["checkHost", { fn: "crypto.x509CheckHost", min: 1, max: 2, form: "checkHost(name[, options])" }],
|
||||
[
|
||||
"checkEmail",
|
||||
{ fn: "crypto.x509CheckEmail", min: 1, max: 2, form: "checkEmail(email[, options])" },
|
||||
],
|
||||
["checkIP", { fn: "crypto.x509CheckIP", min: 1, max: 2, form: "checkIP(ip[, options])" }],
|
||||
["toLegacyObject", { fn: "crypto.x509LegacyObject", min: 0, max: 0, form: "toLegacyObject()" }],
|
||||
["toString", { fn: "crypto.x509Pem", min: 0, max: 0, form: "toString()" }],
|
||||
["toJSON", { fn: "crypto.x509Pem", min: 0, max: 0, form: "toJSON()" }],
|
||||
]);
|
||||
|
||||
/** The X509Certificate methods: verify, checkPrivateKey, checkIssued,
|
||||
* checkHost, checkEmail, checkIP, toLegacyObject, toString and toJSON. */
|
||||
export function lowerX509Call(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
|
||||
if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null;
|
||||
const access = call.expression;
|
||||
if (access.questionDotToken) return null;
|
||||
const method = METHODS.get(access.name.text);
|
||||
if (!method) return null;
|
||||
if (!isX509Record(lowerer, access.expression)) return null;
|
||||
const loc = locOf(call);
|
||||
const args = call.arguments.nodes;
|
||||
if (args.length < method.min || args.length > method.max || args.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(
|
||||
`X509Certificate.${access.name.text} with ${args.length} argument(s)`,
|
||||
call,
|
||||
`the supported form is ${method.form}`,
|
||||
);
|
||||
}
|
||||
const raw = derOf(lowerer.lowerExpr(access.expression), loc);
|
||||
if (method.fn === "crypto.x509LegacyObject") {
|
||||
return { kind: "libCall", fn: method.fn, args: [raw], type: DYN, loc };
|
||||
}
|
||||
if (method.fn === "crypto.x509Pem")
|
||||
return { kind: "libCall", fn: method.fn, args: [raw], type: STRING, loc };
|
||||
if (method.fn === "crypto.x509CheckIssued") {
|
||||
if (!isX509Record(lowerer, args[0]!)) {
|
||||
// Node's validation of otherCert, after both operands evaluate.
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: raw, loc }],
|
||||
result: {
|
||||
kind: "libCall",
|
||||
fn: "error.argTypeThrow",
|
||||
args: [
|
||||
{ kind: "strLit", value: "otherCert", type: STRING, loc },
|
||||
{ kind: "strLit", value: "an instance of X509Certificate", type: STRING, loc },
|
||||
lowerer.lowerExprExpecting(args[0]!, DYN),
|
||||
],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const other = derOf(lowerer.lowerExpr(args[0]!), loc);
|
||||
return { kind: "libCall", fn: method.fn, args: [raw, other], type: BOOL, loc };
|
||||
}
|
||||
const first = lowerer.lowerExprExpecting(args[0]!, DYN);
|
||||
if (method.fn === "crypto.x509Verify" || method.fn === "crypto.x509CheckPrivateKey") {
|
||||
return { kind: "libCall", fn: method.fn, args: [raw, first], type: BOOL, loc };
|
||||
}
|
||||
const options = args.length > 1 ? lowerer.lowerExprExpecting(args[1]!, DYN) : dynUndefined(loc);
|
||||
const text: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: method.fn,
|
||||
args: [raw, first, options],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
return optionalString(lowerer, text, lowerer.mapTypeOf(lowerer.typeOf(call)) ?? STRING, loc);
|
||||
}
|
||||
|
||||
/** The TLS socket certificate accessors. getPeerCertificate([detailed])
|
||||
* and getCertificate() answer Node's legacy object (scr_tls_x509.c):
|
||||
* Node reads `detailed` only when it is true. getPeerX509Certificate() and
|
||||
* getX509Certificate() build a new X509Certificate from the certificate's
|
||||
* DER on each call, or answer undefined without one. Null for other
|
||||
* members. */
|
||||
export function lowerTlsCertificateCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
name: string,
|
||||
receiver: () => IrExpr,
|
||||
): IrExpr | null {
|
||||
const loc = locOf(call);
|
||||
const args = call.arguments.nodes;
|
||||
if (name === "getPeerCertificate" || name === "getCertificate") {
|
||||
const max = name === "getPeerCertificate" ? 1 : 0;
|
||||
if (args.length > max || args.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(
|
||||
`${name} with ${args.length} argument(s)`,
|
||||
call,
|
||||
name === "getPeerCertificate"
|
||||
? "the form is getPeerCertificate(detailed?)"
|
||||
: "getCertificate() takes no arguments",
|
||||
);
|
||||
}
|
||||
const socket = receiver();
|
||||
const value: IrExpr =
|
||||
name === "getCertificate"
|
||||
? { kind: "libCall", fn: "tls.sockGetCertificate", args: [socket], type: DYN, loc }
|
||||
: {
|
||||
kind: "libCall",
|
||||
fn: "tls.sockGetPeerCertificate",
|
||||
args: [
|
||||
socket,
|
||||
args.length > 0 ? lowerer.lowerExprExpecting(args[0]!, DYN) : dynUndefined(loc),
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const declared = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
return declared === null || declared.kind === "dyn"
|
||||
? value
|
||||
: lowerer.coerceInto(call, value, declared);
|
||||
}
|
||||
if (name !== "getPeerX509Certificate" && name !== "getX509Certificate") return null;
|
||||
if (args.length > 0) {
|
||||
lowerer.noLowering(
|
||||
`${name} with ${args.length} argument(s)`,
|
||||
call,
|
||||
`${name}() takes no arguments`,
|
||||
);
|
||||
}
|
||||
const type = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const def = type?.kind === "union" ? lowerer.unions.get(type.unionId) : undefined;
|
||||
const recordTag = def ? def.arms.findIndex((arm) => isX509Type(lowerer, arm)) : -1;
|
||||
const undefinedTag = def ? def.arms.findIndex((arm) => arm.kind === "undefinedT") : -1;
|
||||
if (type?.kind !== "union" || !def || recordTag < 0 || undefinedTag < 0) {
|
||||
lowerer.noLowering(
|
||||
`tlsSocket.${name} with this result type`,
|
||||
call,
|
||||
"its type is X509Certificate | undefined",
|
||||
);
|
||||
}
|
||||
const record = def.arms[recordTag] as IrType & { kind: "record" };
|
||||
const der: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: name === "getPeerX509Certificate" ? "tls.sockPeerCertDer" : "tls.sockOwnCertDer",
|
||||
args: [receiver()],
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
};
|
||||
const key = `tls.x509:${type.unionId}`;
|
||||
let helper = lowerer.valueHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = keyedHelperName(lowerer, "%tls.x509", key);
|
||||
lowerer.valueHelpers.set(key, helper);
|
||||
const d: IrExpr = { kind: "varRef", localId: "d.0", type: BYTES_U8, loc };
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [{ localId: "d.0", name: "der", type: BYTES_U8 }],
|
||||
returnType: type,
|
||||
locals: [{ id: "d.0", name: "der", type: BYTES_U8, mutable: false }],
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "===",
|
||||
left: {
|
||||
kind: "bytesIntrinsic",
|
||||
method: "length",
|
||||
receiver: d,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
right: { kind: "numLit", value: 0, type: F64, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: {
|
||||
kind: "unionWrap",
|
||||
unionId: type.unionId,
|
||||
tag: undefinedTag,
|
||||
value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
|
||||
type,
|
||||
loc,
|
||||
},
|
||||
else_: {
|
||||
kind: "unionWrap",
|
||||
unionId: type.unionId,
|
||||
tag: recordTag,
|
||||
// The handshake's DER, read without PEM detection.
|
||||
value: x509RecordCall(lowerer, d, record, loc, true),
|
||||
type,
|
||||
loc,
|
||||
},
|
||||
type,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [der], type, loc };
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import { checkedIterableSpread } from "./checked-iterable-spread.js";
|
||||
import { type ParamShape } from "./call-signatures.js";
|
||||
import { arrayValueRead, arrayValueType } from "./array-values.js";
|
||||
import { buildArrayConversion } from "./coercions/builders.js";
|
||||
import { lowerArraySpreadItems, lowerArrayValueItems } from "./containers/array-construction.js";
|
||||
import { restTupleElementTypes, typeKey } from "../type-mapper.js";
|
||||
|
||||
function fixedTupleSpreadInfo(
|
||||
@@ -403,6 +404,51 @@ export function completeArgs(
|
||||
}
|
||||
}
|
||||
if (restType.kind !== "array") lowerer.unsupported("SC1090", blame, "this call form");
|
||||
// A `(T | undefined)[]` rest (stored as T elements) packs undefined
|
||||
// arguments as present undefined elements.
|
||||
const restNodes = sources.slice(restAt);
|
||||
// Plain arguments and spreads, also together with spread tuple fields
|
||||
// (pre-lowered): each item, in argument order.
|
||||
if (
|
||||
restType.optionalElements === true &&
|
||||
(!restNodes.some(isIr) || restNodes.some((a) => !isIr(a) && ts.isSpreadElement(a)))
|
||||
) {
|
||||
const items: (ts.Expression | IrExpr)[] = [];
|
||||
for (const a of restNodes) {
|
||||
if (isIr(a)) items.push(a.ir);
|
||||
else items.push(a);
|
||||
}
|
||||
out.push(lowerArraySpreadItems(lowerer, items, restType.elem, restType, loc));
|
||||
return out;
|
||||
}
|
||||
// Tuple-spread values (pre-lowered) and plain arguments pack the same
|
||||
// way. Each value evaluates once, in argument order.
|
||||
if (
|
||||
restType.optionalElements === true &&
|
||||
!restNodes.some((a) => !isIr(a) && ts.isSpreadElement(a))
|
||||
) {
|
||||
const stmts: IrStmt[] = [];
|
||||
const values = restNodes.map((a): IrExpr => {
|
||||
// A tuple field is read where its spread appears: a later
|
||||
// argument may write the tuple.
|
||||
let value: IrExpr;
|
||||
if (isIr(a)) {
|
||||
value = a.ir;
|
||||
if (value.kind === "seqExpr") {
|
||||
stmts.push(...value.stmts);
|
||||
value = value.result;
|
||||
}
|
||||
} else value = lowerer.lowerExpr(a as ts.Expression);
|
||||
const saved = lowerer.declareHiddenLocal("%callArg", value.type);
|
||||
stmts.push({ kind: "varDecl", localId: saved.id, init: value, loc: value.loc });
|
||||
return { kind: "varRef", localId: saved.id, type: value.type, loc: value.loc };
|
||||
});
|
||||
const items = lowerArrayValueItems(lowerer, values, restType.elem, restType, loc);
|
||||
out.push(
|
||||
stmts.length === 0 ? items : { kind: "seqExpr", stmts, result: items, type: restType, loc },
|
||||
);
|
||||
return out;
|
||||
}
|
||||
// The rest pack is a fresh array per call; surplus SPREADS copy
|
||||
// their elements in (JS-exact — `f(a, ...xs, b, ...ys)` packs in
|
||||
// order, sources untouched).
|
||||
|
||||
@@ -946,6 +946,8 @@ function appendArgumentsParameter(
|
||||
"SC1090",
|
||||
node,
|
||||
"parameterized 'arguments' outside an ES module (sloppy-mode parameter aliases)",
|
||||
undefined,
|
||||
"sloppy-arguments",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,6 +532,8 @@ export function lowerUncompiledRequireCall(
|
||||
"SC1090",
|
||||
call,
|
||||
'require() with a computed specifier (the compiled module graph is fixed at build time: require("<string literal>") lowers)',
|
||||
undefined,
|
||||
"dynamic-require",
|
||||
);
|
||||
}
|
||||
if (canonicalBuiltinModule(spec) !== null) return null;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import { constructedValue } from "../constructor-returns.js";
|
||||
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
|
||||
import { countedFor, dynUndefinedExpr, varRef } from "../../../ir/build.js";
|
||||
import type { FieldLift } from "./structural-plans.js";
|
||||
import { buildArrayConversion } from "./builders.js";
|
||||
import { arrayValueRead, arrayValueStore, primitiveElement } from "../array-values.js";
|
||||
import type { WidthLift } from "../width-lift.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType, SrcLoc } from "../../../ir/ir.js";
|
||||
import { BOOL, DYN, F64, STRING, UNDEFINED_T } from "../../../ir/ir.js";
|
||||
import { BOOL, DYN, F64, STRING, UNDEFINED_T, typeEquals } from "../../../ir/ir.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import { findStaticOn, findGenericStaticOn } from "../lower-classes.js";
|
||||
import { lowerRecordOvfCaptureHelper } from "../containers/indexed-objects.js";
|
||||
@@ -54,6 +55,9 @@ export function widthCoerce(lowerer: Lowerer, expr: IrExpr, expected: IrType): I
|
||||
return lowerer.classStaticsProjection(expr.type.className, expected.shapeId, expr.loc);
|
||||
}
|
||||
if (expected.kind === "array" && expr.type.kind === "array") {
|
||||
const optional = optionalArrayHelper(lowerer, expr.type, expected, expr.loc);
|
||||
if (optional)
|
||||
return { kind: "call", callee: optional, args: [expr], type: expected, loc: expr.loc };
|
||||
const helper = lowerer.arrayWidthHelper(expr.type, expected, expr.loc);
|
||||
if (helper)
|
||||
return { kind: "call", callee: helper, args: [expr], type: expected, loc: expr.loc };
|
||||
@@ -518,6 +522,69 @@ export function emptyArrayLiftHelper(
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Copy an array of `T | undefined` unions (a collection's values, an
|
||||
* iterator drain) into a `(T | undefined)[]` of a number, string or
|
||||
* boolean T, which stores T elements: undefined values become present
|
||||
* undefined elements. Null for other element types. */
|
||||
function optionalArrayHelper(
|
||||
lowerer: Lowerer,
|
||||
fromT: IrType & { kind: "array" },
|
||||
toT: IrType & { kind: "array" },
|
||||
loc: SrcLoc,
|
||||
): string | null {
|
||||
const from = fromT.elem;
|
||||
const to = toT.elem;
|
||||
if (
|
||||
!primitiveElement(to) ||
|
||||
from.kind !== "union" ||
|
||||
lowerer.armTag(from.unionId, UNDEFINED_T) < 0 ||
|
||||
!typeEquals(lowerer.stripUndefinedArm(from), to)
|
||||
)
|
||||
return null;
|
||||
const key = `arrOptional:${typeKey(from)}:${typeKey(to)}`;
|
||||
const existing = lowerer.valueHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, "%arr.width", key);
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
const input = varRef("a.0", fromT, loc);
|
||||
const out = varRef("out.0", toT, loc);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "a.0", name: "a", type: fromT }],
|
||||
returnType: toT,
|
||||
locals: [
|
||||
{ id: "a.0", name: "a", type: fromT, mutable: true },
|
||||
{ id: "out.0", name: "out", type: toT, mutable: false },
|
||||
{ id: "i.0", name: "i", type: F64, mutable: true },
|
||||
{ id: "v.0", name: "v", type: from, mutable: false },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "out.0",
|
||||
init: { kind: "arrayLit", elems: [], type: toT, loc },
|
||||
loc,
|
||||
},
|
||||
countedFor(
|
||||
loc,
|
||||
{ kind: "arrIntrinsic", method: "length", receiver: input, args: [], type: F64, loc },
|
||||
(index) => [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "v.0",
|
||||
init: arrayValueRead(lowerer, input, index, from, loc),
|
||||
loc,
|
||||
},
|
||||
arrayValueStore(lowerer, out, index, varRef("v.0", from, loc), to, loc),
|
||||
],
|
||||
),
|
||||
{ kind: "return", value: out, loc },
|
||||
],
|
||||
loc,
|
||||
});
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Copy an array through its validated element conversion. Intern before
|
||||
* building the body so recursive element conversions can reuse this helper. */
|
||||
export function arrayWidthHelper(
|
||||
@@ -542,14 +609,37 @@ export function arrayWidthHelper(
|
||||
name,
|
||||
arrT,
|
||||
{ kind: "arrayLit", elems: [], type: outT, loc },
|
||||
(element, _index, result) => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: result,
|
||||
args: [lowerer.applyWidthLift(elemLift, element, toElem, loc)],
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
(element, index, result) => {
|
||||
let value = lowerer.applyWidthLift(elemLift, element, toElem, loc);
|
||||
// A number, string or boolean array can hold present undefined
|
||||
// elements (`(T | undefined)[]` stores T): they, and holes, copy
|
||||
// as the target's undefined.
|
||||
const missing = primitiveElement(fromElem) ? lowerer.wrappedUndefined(toElem, loc) : null;
|
||||
if (missing !== null)
|
||||
value = {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "===",
|
||||
left: { kind: "arrayState", arr: varRef("a.0", arrT, loc), index, type: F64, loc },
|
||||
right: { kind: "numLit", value: 1, type: F64, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: value,
|
||||
else_: missing,
|
||||
type: toElem,
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: result,
|
||||
args: [value],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
},
|
||||
loc,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
currentArrayIndexPresent,
|
||||
elementMayBeForeign,
|
||||
foreignElementThrow,
|
||||
primitiveElement,
|
||||
} from "../array-values.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import {
|
||||
@@ -829,15 +830,16 @@ function buildArrayHofFn(
|
||||
);
|
||||
returnType = outT;
|
||||
body = [
|
||||
readLen,
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "out.0",
|
||||
init: { kind: "arrayLit", elems: [], type: outT, loc },
|
||||
loc,
|
||||
},
|
||||
readLen,
|
||||
// map's result has the receiver's length, holes where the receiver
|
||||
// has them. Storage for it is reserved first and the length is set
|
||||
// has them. Storage for it is reserved first (with the result's
|
||||
// construction, which directly precedes it) and the length is set
|
||||
// after the loop, so a result without holes is filled by appends and
|
||||
// gets no state bytes.
|
||||
...(method === "map"
|
||||
@@ -1691,6 +1693,8 @@ function flatMapUnionPlan(
|
||||
}
|
||||
const converts = (t: IrType): boolean => {
|
||||
if (typeEquals(t, outElem)) return true;
|
||||
// A `(T | undefined)[]` result stores T elements (primitiveElement).
|
||||
if (t.kind === "undefinedT" && primitiveElement(outElem)) return true;
|
||||
if (outElem.kind !== "union") return false;
|
||||
return lowerer.armTag(outElem.unionId, t) >= 0;
|
||||
};
|
||||
@@ -1756,24 +1760,21 @@ function flatMapUnionHelper(
|
||||
};
|
||||
if (plan.arms[tag] === "empty") return [];
|
||||
if (plan.arms[tag] === "value") {
|
||||
// A unit arm's value is the unit itself, which has no narrowed
|
||||
// representation.
|
||||
const value: IrExpr = isUnitType(arm)
|
||||
? { kind: "unitLit", unit: arm.kind === "nullT" ? "null" : "undefined", type: arm, loc }
|
||||
: narrowed;
|
||||
return [
|
||||
arrayValueStore(
|
||||
lowerer,
|
||||
outRef(),
|
||||
outLength(),
|
||||
lowerer.coerceToExpected(
|
||||
// A unit arm's value is the unit itself, which has no
|
||||
// narrowed representation.
|
||||
isUnitType(arm)
|
||||
? {
|
||||
kind: "unitLit",
|
||||
unit: arm.kind === "nullT" ? "null" : "undefined",
|
||||
type: arm,
|
||||
loc,
|
||||
}
|
||||
: narrowed,
|
||||
plan.outElem,
|
||||
),
|
||||
// An undefined value is a present undefined element of a
|
||||
// primitive array (arrayValueStore).
|
||||
arm.kind === "undefinedT" && primitiveElement(plan.outElem)
|
||||
? value
|
||||
: lowerer.coerceToExpected(value, plan.outElem),
|
||||
plan.outElem,
|
||||
loc,
|
||||
),
|
||||
@@ -2349,6 +2350,43 @@ function reduceHelper(
|
||||
return name;
|
||||
}
|
||||
|
||||
/** The comparator of a sort over a `(T | undefined)[]`, which stores T
|
||||
* elements. The comparator only receives present elements (undefined
|
||||
* elements sort last without a call), so the unannotated parameters of an
|
||||
* inline comparator take the element type, and the runtime calls it
|
||||
* without an adapter. */
|
||||
function lowerOptionalElementComparator(
|
||||
lowerer: Lowerer,
|
||||
argNode: ts.Expression,
|
||||
comparator: ts.Expression,
|
||||
elem: IrType,
|
||||
arrT: IrType & { kind: "array" },
|
||||
): IrExpr {
|
||||
if (
|
||||
arrT.optionalElements !== true ||
|
||||
!(ts.isArrowFunction(comparator) || ts.isFunctionExpression(comparator)) ||
|
||||
isJsSourceFile(ts.getSourceFileOfNode(comparator))
|
||||
)
|
||||
return lowerer.lowerExpr(argNode);
|
||||
const rebound: { symbol: ts.Symbol; previous: IrType | undefined }[] = [];
|
||||
for (const param of comparator.parameters.nodes.slice(0, 2)) {
|
||||
if (param.type || param.initializer || param.dotDotDotToken || !ts.isIdentifier(param.name))
|
||||
continue;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
|
||||
if (!symbol) continue;
|
||||
rebound.push({ symbol, previous: lowerer.runtimeOptionalBindingTypes.get(symbol) });
|
||||
lowerer.runtimeOptionalBindingTypes.set(symbol, elem);
|
||||
}
|
||||
try {
|
||||
return lowerer.lowerExpr(argNode);
|
||||
} finally {
|
||||
for (const { symbol, previous } of rebound) {
|
||||
if (previous === undefined) lowerer.runtimeOptionalBindingTypes.delete(symbol);
|
||||
else lowerer.runtimeOptionalBindingTypes.set(symbol, previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** `a.sort(cmp)` / `a.toSorted(cmp)`, desugared to an interned synthetic
|
||||
* function like the other array HOFs. sort mutates and returns the receiver;
|
||||
* toSorted takes its shallow snapshot INSIDE the helper, after the receiver
|
||||
@@ -2437,7 +2475,8 @@ export function lowerArraySortCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
let fnArg = undefinedArg ?? lowerer.lowerExpr(argNode);
|
||||
let fnArg =
|
||||
undefinedArg ?? lowerOptionalElementComparator(lowerer, argNode, unwrappedArg, elem, arrT);
|
||||
// Reusable JS comparators often accept checked values. Adapt the array's
|
||||
// element ABI just as other array callbacks do before sorting.
|
||||
if (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { generatorDrain } from "../iterator-adapters.js";
|
||||
import { identityPreservingWidening } from "../coercions/identity.js";
|
||||
import { liftedFreshSpread } from "../coercions/fresh-spread.js";
|
||||
import { checkedIterableSpread } from "../checked-iterable-spread.js";
|
||||
import { AstNode } from "../../ts7/ast-node.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import {
|
||||
dynUndefinedExpr,
|
||||
@@ -27,7 +28,9 @@ import {
|
||||
type IrType,
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
UNDEFINED_T,
|
||||
arrayOf,
|
||||
isUnitType,
|
||||
canConvertToDyn,
|
||||
funcOf,
|
||||
isRefCounted,
|
||||
@@ -35,7 +38,13 @@ import {
|
||||
typeEquals,
|
||||
} from "../../../ir/ir.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { arrayValueRead, arrayValueStore, arrayValueType } from "../array-values.js";
|
||||
import {
|
||||
arrayValueRead,
|
||||
arrayValueStore,
|
||||
arrayValueType,
|
||||
optionalElementValue,
|
||||
primitiveElement,
|
||||
} from "../array-values.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import { newFnCtx } from "../lowerer.js";
|
||||
import { iteratorCanStep, iteratorValue } from "../iterator-consumption.js";
|
||||
@@ -51,6 +60,34 @@ import {
|
||||
} from "./callback-arguments.js";
|
||||
import { classGeneratorIterable } from "../class-iterables.js";
|
||||
|
||||
/** A value whose type admits only null and undefined, stored into an array
|
||||
* that keeps undefined elements: the checker admits it there only when its
|
||||
* type is `undefined` (a tuple element typed `undefined` maps to such a
|
||||
* union), so it is evaluated and stored as undefined. */
|
||||
function undefinedForNullish(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
arrType: IrType & { kind: "array" },
|
||||
): IrExpr {
|
||||
if (arrType.optionalElements !== true || value.type.kind !== "union") return value;
|
||||
const arms = lowerer.unions.get(value.type.unionId)?.arms ?? [];
|
||||
if (arms.length === 0 || !arms.every(isUnitType)) return value;
|
||||
const loc = value.loc;
|
||||
const optional: IrType = {
|
||||
kind: "union",
|
||||
unionId: lowerer.unions.intern([arrType.elem, UNDEFINED_T]),
|
||||
};
|
||||
const missing = lowerer.wrappedUndefined(optional, loc);
|
||||
if (!missing) return value;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: value, loc }],
|
||||
result: missing,
|
||||
type: optional,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Build the argument array for a mutating/copying operation while keeping
|
||||
* the receiver's scalar payload ABI. A missing read is represented as the
|
||||
* present UNDEFINED state in this temporary, so push/unshift and toSpliced
|
||||
@@ -86,9 +123,11 @@ export function lowerArrayValueItems(
|
||||
loc,
|
||||
},
|
||||
];
|
||||
for (const value of values) {
|
||||
for (const item of values) {
|
||||
const value = undefinedForNullish(lowerer, item, arrType);
|
||||
const stored =
|
||||
lowerer.runtimeOptionalWidening(value.type, elem) !== null
|
||||
lowerer.runtimeOptionalWidening(value.type, elem) !== null ||
|
||||
optionalElementValue(lowerer, value, elem)
|
||||
? value
|
||||
: lowerer.coerceToExpected(value, elem);
|
||||
body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc));
|
||||
@@ -96,16 +135,21 @@ export function lowerArrayValueItems(
|
||||
return { kind: "seqExpr", stmts: body, result: outRef, type: arrType, loc };
|
||||
}
|
||||
|
||||
/** The items of an array built from argument nodes (a spread copies its
|
||||
* elements in) and values already lowered (a spread tuple's fields), in
|
||||
* order. Each value is stored when its turn comes. */
|
||||
export function lowerArraySpreadItems(
|
||||
lowerer: Lowerer,
|
||||
nodes: readonly ts.Expression[],
|
||||
nodes: readonly (ts.Expression | IrExpr)[],
|
||||
elem: IrType,
|
||||
arrType: IrType & { kind: "array" },
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const out = lowerer.declareHiddenLocal("%arrayItems", arrType);
|
||||
const outRef = varRef(out.id, arrType, loc);
|
||||
const plainItems = nodes.filter((node) => !ts.isSpreadElement(node)).length;
|
||||
const plainItems = nodes.filter(
|
||||
(node) => !(node instanceof AstNode) || !ts.isSpreadElement(node),
|
||||
).length;
|
||||
const length = (): IrExpr => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
@@ -129,7 +173,23 @@ export function lowerArraySpreadItems(
|
||||
},
|
||||
];
|
||||
for (const node of nodes) {
|
||||
if (ts.isSpreadElement(node)) {
|
||||
if (!(node instanceof AstNode)) {
|
||||
// A spread tuple's first field declares the tuple's storage, which
|
||||
// the later fields read: its statements join this sequence, whose
|
||||
// frame lasts until every item is stored.
|
||||
let item = node;
|
||||
if (item.kind === "seqExpr") {
|
||||
body.push(...item.stmts);
|
||||
item = item.result;
|
||||
}
|
||||
const value = undefinedForNullish(lowerer, item, arrType);
|
||||
const stored =
|
||||
lowerer.runtimeOptionalWidening(value.type, elem) !== null ||
|
||||
optionalElementValue(lowerer, value, elem)
|
||||
? value
|
||||
: lowerer.coerceToExpected(value, elem);
|
||||
body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc));
|
||||
} else if (ts.isSpreadElement(node)) {
|
||||
let source = lowerer.lowerExpr(node.expression);
|
||||
if (
|
||||
source.type.kind === "set" &&
|
||||
@@ -212,7 +272,8 @@ export function lowerArraySpreadItems(
|
||||
} else {
|
||||
const value = lowerer.lowerExpr(node);
|
||||
const stored =
|
||||
lowerer.runtimeOptionalWidening(value.type, elem) !== null
|
||||
lowerer.runtimeOptionalWidening(value.type, elem) !== null ||
|
||||
optionalElementValue(lowerer, value, elem)
|
||||
? value
|
||||
: lowerer.coerceInto(node, value, elem);
|
||||
body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc));
|
||||
@@ -518,6 +579,20 @@ export function lowerArrayOfCall(
|
||||
if (call.arguments.nodes.some(ts.isSpreadElement)) {
|
||||
return lowerArraySpreadItems(lowerer, call.arguments.nodes, result.elem, result, locOf(call));
|
||||
}
|
||||
// A `(T | undefined)[]` (primitiveElement) stores undefined arguments as
|
||||
// present undefined elements.
|
||||
if (result.optionalElements === true) {
|
||||
const nodes = call.arguments.nodes;
|
||||
const values = nodes.map((arg) => lowerer.lowerExpr(arg));
|
||||
if (values.some((value) => optionalElementValue(lowerer, value, result.elem)))
|
||||
return lowerArrayValueItems(lowerer, values, result.elem, result, locOf(call));
|
||||
return {
|
||||
kind: "arrayLit",
|
||||
elems: values.map((value, i) => lowerer.coerceInto(nodes[i]!, value, result.elem)),
|
||||
type: result,
|
||||
loc: locOf(call),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "arrayLit",
|
||||
elems: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, result.elem)),
|
||||
@@ -959,6 +1034,31 @@ export function lowerArrayFromCall(
|
||||
const arrT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (arrT?.kind !== "array") lowerer.badType(call, lowerer.typeOf(call));
|
||||
const elem = arrT.elem;
|
||||
// A `(T | undefined)[]` (primitiveElement): n present undefined
|
||||
// elements.
|
||||
const elemTs = lowerer.checker.isArrayType(lowerer.typeOf(call))
|
||||
? lowerer.checker.getTypeArguments(lowerer.typeOf(call) as ts.TypeReference)[0]
|
||||
: undefined;
|
||||
if (
|
||||
primitiveElement(elem) &&
|
||||
elemTs !== undefined &&
|
||||
(elemTs.isUnionType() ? ts.constituentTypes(elemTs) : [elemTs]).some(
|
||||
(part) => (part.flags & ts.TypeFlags.Undefined) !== 0,
|
||||
)
|
||||
) {
|
||||
return {
|
||||
kind: "arrIntrinsic",
|
||||
method: "fillUndefined",
|
||||
receiver: { kind: "arrayNewLen", length: n, type: arrT, loc },
|
||||
// To the end: fill clamps an infinite end to the length.
|
||||
args: [
|
||||
numLit(0, loc),
|
||||
{ kind: "bin", op: "/", left: numLit(1, loc), right: numLit(0, loc), type: F64, loc },
|
||||
],
|
||||
type: arrT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const absent =
|
||||
elem.kind === "union"
|
||||
? lowerer.wrappedUndefined(elem, loc) !== null ||
|
||||
@@ -1085,7 +1185,10 @@ export function lowerArrayFromCall(
|
||||
// A generator (or an iterator adapted into one) drains in order.
|
||||
if (src.type.kind === "generator" && args.length === 1) {
|
||||
const arrT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const drained = arrT?.kind === "array" ? generatorDrain(lowerer, src, arrT.elem, loc) : null;
|
||||
const drained =
|
||||
arrT?.kind === "array"
|
||||
? generatorDrain(lowerer, src, arrT.elem, loc, arrT.optionalElements === true)
|
||||
: null;
|
||||
if (drained) return drained;
|
||||
}
|
||||
lowerer.noLowering(
|
||||
@@ -1159,6 +1262,11 @@ function buildArrayFromArrayFn(
|
||||
type: fnRet,
|
||||
loc,
|
||||
};
|
||||
// A result of another type than the element (`T | undefined` into a
|
||||
// `(T | undefined)[]`, which stores T elements) is held in a local, so
|
||||
// the store tests and converts one value.
|
||||
const held = !typeEquals(fnRet, outElem);
|
||||
const value: IrExpr = held ? varRef("v.0", fnRet, loc) : mapped;
|
||||
return {
|
||||
name,
|
||||
params: [
|
||||
@@ -1171,6 +1279,7 @@ function buildArrayFromArrayFn(
|
||||
{ id: "f.0", name: "f", type: fnT, mutable: true },
|
||||
{ id: "out.0", name: "out", type: outT, mutable: false },
|
||||
{ id: "i.0", name: "i", type: F64, mutable: true },
|
||||
...(held ? [{ id: "v.0", name: "v", type: fnRet, mutable: false }] : []),
|
||||
],
|
||||
body: [
|
||||
{
|
||||
@@ -1191,7 +1300,8 @@ function buildArrayFromArrayFn(
|
||||
loc,
|
||||
},
|
||||
body: [
|
||||
arrayValueStore(lowerer, out, i, mapped, outElem, loc),
|
||||
...(held ? [{ kind: "varDecl", localId: "v.0", init: mapped, loc } as const] : []),
|
||||
arrayValueStore(lowerer, out, i, value, outElem, loc),
|
||||
{
|
||||
kind: "assign",
|
||||
localId: "i.0",
|
||||
|
||||
@@ -32,6 +32,10 @@ import {
|
||||
arrayValueRead,
|
||||
arrayValueStore,
|
||||
arrayValueType,
|
||||
elementPlaceholder,
|
||||
insertedValues,
|
||||
optionalElementValue,
|
||||
primitiveElement,
|
||||
} from "../array-values.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import {
|
||||
@@ -125,6 +129,14 @@ function lowerOptionalArrayPush(
|
||||
return { kind: "seqExpr", stmts, result: length(), type: F64, loc };
|
||||
}
|
||||
|
||||
/** The checker type of `node` admits undefined. */
|
||||
function checkerTypeAdmitsUndefined(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
const type = lowerer.typeOf(node);
|
||||
return (type.isUnionType() ? ts.constituentTypes(type) : [type]).some(
|
||||
(part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0,
|
||||
);
|
||||
}
|
||||
|
||||
/** `a.splice(start, count, v)` where v may hold undefined at run time.
|
||||
* Receiver, start, count, and v evaluate once, in order; a present v is
|
||||
* inserted as a plain element literal (which a statement-position splice
|
||||
@@ -658,7 +670,10 @@ export function lowerArrayMethodCall(
|
||||
const hasSpread = itemNodes.some(ts.isSpreadElement);
|
||||
const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg));
|
||||
const statefulItems = itemProbes.some(
|
||||
(probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null,
|
||||
(probe) =>
|
||||
probe !== null &&
|
||||
(lowerer.runtimeOptionalWidening(probe.type, elem) !== null ||
|
||||
optionalElementValue(lowerer, probe, elem)),
|
||||
);
|
||||
const items: IrExpr = hasSpread
|
||||
? lowerArraySpreadItems(lowerer, itemNodes, elem, receiverIr, loc)
|
||||
@@ -875,6 +890,65 @@ export function lowerArrayMethodCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// A `(T | undefined)[]` (primitiveElement) takes undefined values as
|
||||
// present undefined elements, stored after the insertion.
|
||||
if (
|
||||
(name === "push" || name === "unshift") &&
|
||||
primitiveElement(elem) &&
|
||||
call.arguments.nodes.some((a) => checkerTypeAdmitsUndefined(lowerer, a))
|
||||
) {
|
||||
const nodes = call.arguments.nodes;
|
||||
const values = nodes.map((a) => lowerer.lowerExpr(a));
|
||||
const inserted = insertedValues(
|
||||
lowerer,
|
||||
values,
|
||||
elem,
|
||||
(value, i) => lowerer.coerceInto(nodes[i]!, value, elem),
|
||||
loc,
|
||||
);
|
||||
if (inserted.optional) {
|
||||
const array = lowerer.declareHiddenLocal("%insertInto", receiver.type);
|
||||
const arr = varRef(array.id, array.type, loc);
|
||||
const length = lowerer.declareHiddenLocal("%insertLength", F64);
|
||||
const count: IrExpr = numLit(values.length, loc);
|
||||
const first: IrExpr =
|
||||
name === "push"
|
||||
? {
|
||||
kind: "bin",
|
||||
op: "-",
|
||||
left: varRef(length.id, F64, loc),
|
||||
right: count,
|
||||
type: F64,
|
||||
loc,
|
||||
}
|
||||
: numLit(0, loc);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: array.id, init: receiver, loc },
|
||||
...inserted.stmts,
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: length.id,
|
||||
init: {
|
||||
kind: "arrIntrinsic",
|
||||
method: name,
|
||||
receiver: arr,
|
||||
args: inserted.args,
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
...inserted.fill(arr, first),
|
||||
],
|
||||
result: varRef(length.id, F64, loc),
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return { kind: "arrIntrinsic", method: name, receiver, args: inserted.args, type: F64, loc };
|
||||
}
|
||||
// Inserted values flow into the element slot like an assignment would:
|
||||
// union-element arrays wrap plain arm values (coerceInto is inert
|
||||
// when the types already agree).
|
||||
@@ -996,9 +1070,12 @@ export function lowerArrayMethodCall(
|
||||
const hasSpread = itemNodes.some(ts.isSpreadElement);
|
||||
const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg));
|
||||
const statefulItems = itemProbes.some(
|
||||
(probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null,
|
||||
(probe) =>
|
||||
probe !== null &&
|
||||
(lowerer.runtimeOptionalWidening(probe.type, elem) !== null ||
|
||||
optionalElementValue(lowerer, probe, elem)),
|
||||
);
|
||||
if (statefulItems && itemNodes.length === 1) {
|
||||
if (statefulItems && itemNodes.length === 1 && itemProbes[0]?.type.kind === "union") {
|
||||
return lowerOptionalSpliceItem(
|
||||
lowerer,
|
||||
receiver,
|
||||
@@ -1734,7 +1811,28 @@ function arraySearchHelper(
|
||||
type: valueT,
|
||||
loc,
|
||||
};
|
||||
const value = directNeedle ? rawRead : optionalValue;
|
||||
// A direct needle matches only a present value (`match` tests the state
|
||||
// first); a present undefined of a `(T | undefined)[]` (primitiveElement)
|
||||
// must not be read as T.
|
||||
const value = !directNeedle
|
||||
? optionalValue
|
||||
: primitiveElement(elem)
|
||||
? {
|
||||
kind: "ternary" as const,
|
||||
cond: {
|
||||
kind: "bin" as const,
|
||||
op: "===" as const,
|
||||
left: stateRef,
|
||||
right: numLit(1, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: rawRead,
|
||||
else_: elementPlaceholder(elem, loc),
|
||||
type: elem,
|
||||
loc,
|
||||
}
|
||||
: rawRead;
|
||||
const readT = directNeedle ? elem : valueT;
|
||||
const equal = arraySearchEquality(
|
||||
lowerer,
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { CONTRACT_TYPES_MODULE } from "../../contract-types.js";
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import { writtenLocals } from "../../../ir/callback-specialization.js";
|
||||
import { AmbientReceiverReaders } from "../../../ir/receiver-readers.js";
|
||||
import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js";
|
||||
import {
|
||||
BOOL,
|
||||
F64,
|
||||
VOID,
|
||||
type IrBindingSource,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrGlobal,
|
||||
@@ -16,9 +21,12 @@ import {
|
||||
typeKey,
|
||||
} from "../../../ir/ir.js";
|
||||
import {
|
||||
everyExpr,
|
||||
everyExprChild,
|
||||
everyStmt,
|
||||
everyStmtChild,
|
||||
everyStmtList,
|
||||
transformExpr,
|
||||
transformStmtList,
|
||||
} from "../../../ir/traverse.js";
|
||||
import {
|
||||
@@ -538,6 +546,65 @@ function matchChain(lowerer: Lowerer, e: IrExpr): Chain | null {
|
||||
};
|
||||
}
|
||||
|
||||
/** The outermost array-HOF call of `e` on its own, over its receiver. */
|
||||
function loneChain(lowerer: Lowerer, e: IrExpr): Chain | null {
|
||||
const top = stageOf(lowerer, e);
|
||||
if (!top) return null;
|
||||
const { stage, receiver } = top;
|
||||
if (receiver.type.kind !== "array") return null;
|
||||
const stages: Stage[] = [];
|
||||
let terminal: Terminal;
|
||||
if (TERMINALS.has(stage.info.method)) {
|
||||
const init =
|
||||
stage.info.method === "reduce" && stage.info.hasInit && e.kind === "call" ? e.args[2]! : null;
|
||||
terminal = { kind: "call", stage, init };
|
||||
} else if (PRODUCERS.has(stage.info.method)) {
|
||||
terminal = { kind: "collect" };
|
||||
stages.push(stage);
|
||||
} else return null;
|
||||
return {
|
||||
source: receiver,
|
||||
sourceType: receiver.type,
|
||||
slice: null,
|
||||
stages,
|
||||
terminal,
|
||||
type: e.type,
|
||||
loc: e.loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The callbacks of `chain` in order, or null when its stages cannot run
|
||||
* in one loop: element types that do not line up, an array argument past
|
||||
* the first stage, reduce without a seed, or findIndex over holes. */
|
||||
function chainCallbacks(chain: Chain): Stage[] | null {
|
||||
const { stages, terminal } = chain;
|
||||
const terminalStage = terminal.kind === "call" ? terminal.stage : null;
|
||||
const callbacks = terminalStage ? [...stages, terminalStage] : stages;
|
||||
// Element types must line up stage to stage.
|
||||
let elem = chain.sourceType.elem;
|
||||
for (const stage of callbacks) {
|
||||
if (!typeEquals(stage.info.elem, elem)) return null;
|
||||
if (stage.info.method === "filter" && !typeEquals(stage.info.outElem, stage.info.elem))
|
||||
return null;
|
||||
if (
|
||||
(stage.info.method === "map" || stage.info.method === "flatMap") &&
|
||||
!typeEquals(stage.info.outElem, stage.info.fnRet)
|
||||
)
|
||||
return null;
|
||||
elem = stage.info.outElem;
|
||||
}
|
||||
// Index arguments are reproduced; an array argument only names the source.
|
||||
for (let k = 0; k < callbacks.length; k++) {
|
||||
const stage = callbacks[k]!;
|
||||
const lead = stage.info.method === "reduce" ? 1 : 0;
|
||||
if (stage.info.arity > lead + 3) return null;
|
||||
if (stage.info.arity === lead + 3 && (k !== 0 || chain.slice)) return null;
|
||||
}
|
||||
if (terminalStage?.info.method === "reduce" && !terminalStage.info.hasInit) return null;
|
||||
if (terminalStage?.info.method === "findIndex" && holeCarrying(chain)) return null;
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
/* ── the fusion pass ─────────────────────────────────────────────────── */
|
||||
|
||||
/** The values a fused loop hands from one stage to the next: a local of a
|
||||
@@ -551,6 +618,10 @@ interface Plan {
|
||||
writes: Set<string>;
|
||||
/** Some callback may throw or loop; at most one does. */
|
||||
risky: boolean;
|
||||
/** How the loop inlines callbacks, when not by their effect summaries. */
|
||||
inline?: InlineTarget;
|
||||
/** Stack frame name of the loop: the inlined callback's, for a lone call. */
|
||||
frame?: string;
|
||||
}
|
||||
|
||||
/** Fuse chains of array-HOF helper calls into one loop.
|
||||
@@ -597,12 +668,35 @@ class PipelineFusion {
|
||||
readonly added: IrFunction[] = [];
|
||||
private readonly effects: CallbackEffects;
|
||||
private readonly helpers = new Map<string, string>();
|
||||
/** Bindings each function, or a closure it creates, writes after their
|
||||
* declaration. */
|
||||
private readonly written: Map<string, Set<string>>;
|
||||
/** classCaptured's answers by function name. */
|
||||
private readonly classCaptures = new Map<string, ReadonlySet<string>>();
|
||||
/** Whether a module imports or re-exports the contract types. */
|
||||
private readonly contractTypes: boolean;
|
||||
/** Bindings of the function being fused that inlined callbacks captured. */
|
||||
private inlinedCaptures = new Set<string>();
|
||||
/** The helper calls each callback with an undefined receiver; a copied
|
||||
* body runs under the receiver around the array call instead. */
|
||||
private readonly receivers: AmbientReceiverReaders;
|
||||
|
||||
constructor(
|
||||
private readonly lowerer: Lowerer,
|
||||
functions: readonly IrFunction[],
|
||||
) {
|
||||
this.effects = new CallbackEffects(functions, lowerer.globalsList);
|
||||
this.written = writtenLocals({ functions });
|
||||
this.receivers = new AmbientReceiverReaders(new Map(functions.map((fn) => [fn.name, fn])));
|
||||
this.contractTypes = lowerer.moduleOrder.some((sf) =>
|
||||
sf.statements.nodes.some(
|
||||
(st) =>
|
||||
(ts.isImportDeclaration(st) || ts.isExportDeclaration(st)) &&
|
||||
st.moduleSpecifier !== undefined &&
|
||||
ts.isStringLiteral(st.moduleSpecifier) &&
|
||||
st.moduleSpecifier.text === CONTRACT_TYPES_MODULE,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
run(fn: IrFunction): void {
|
||||
@@ -613,6 +707,7 @@ class PipelineFusion {
|
||||
});
|
||||
if (!candidate) return;
|
||||
const removed = new Set<string>();
|
||||
this.inlinedCaptures = new Set();
|
||||
let body = this.lists(fn.body, fn, removed);
|
||||
body = transformStmtList(body, {
|
||||
stmt: (s) => s,
|
||||
@@ -620,6 +715,7 @@ class PipelineFusion {
|
||||
});
|
||||
fn.body = body;
|
||||
if (removed.size > 0) fn.locals = fn.locals.filter((l) => !removed.has(l.id));
|
||||
if (this.inlinedCaptures.size > 0) unboxUncaptured(fn, this.inlinedCaptures);
|
||||
}
|
||||
|
||||
/* ── statement lists: fold single-use chain bindings ── */
|
||||
@@ -734,40 +830,51 @@ class PipelineFusion {
|
||||
/* ── expressions ── */
|
||||
|
||||
private fuse(e: IrExpr, fn: IrFunction): IrExpr | null {
|
||||
const plan = this.plan(e, fn);
|
||||
const plan = this.plan(e, fn) ?? this.lonePlan(e, fn);
|
||||
return plan ? this.emit(plan) : null;
|
||||
}
|
||||
|
||||
/** One array-HOF call on its own (or the last call of a chain that does
|
||||
* not fuse), when every callback is a closure literal whose body can be
|
||||
* copied into the loop. A single stage reorders nothing, so the
|
||||
* callbacks' effects need no model; their captured bindings must keep
|
||||
* one value while the loop runs (stableTarget), since the loop reads
|
||||
* them once, when it starts. The loop keeps the helper's order: the
|
||||
* length read once, each element read when it is visited, holes
|
||||
* skipped (and kept by map). */
|
||||
private lonePlan(e: IrExpr, fn: IrFunction): Plan | null {
|
||||
const chain = loneChain(this.lowerer, e);
|
||||
if (!chain) return null;
|
||||
const callbacks = chainCallbacks(chain);
|
||||
if (!callbacks) return null;
|
||||
const inline = this.stableTarget(fn);
|
||||
let frame: string | undefined;
|
||||
for (const stage of callbacks) {
|
||||
for (const arg of [stage.slow, stage.fast]) {
|
||||
if (!arg) continue;
|
||||
const target = inline(arg);
|
||||
if (!target) return null;
|
||||
frame ??= target.sourceName;
|
||||
}
|
||||
}
|
||||
return {
|
||||
chain,
|
||||
callbacks,
|
||||
writes: new Set(),
|
||||
risky: true,
|
||||
inline,
|
||||
...(frame === undefined ? {} : { frame }),
|
||||
};
|
||||
}
|
||||
|
||||
/** Decide whether `e` is a fusable chain; null when it is not. */
|
||||
private plan(e: IrExpr, fn: IrFunction): Plan | null {
|
||||
const chain = matchChain(this.lowerer, e);
|
||||
if (!chain) return null;
|
||||
const callbacks = chainCallbacks(chain);
|
||||
if (!callbacks) return null;
|
||||
const { stages, terminal } = chain;
|
||||
const terminalStage = terminal.kind === "call" ? terminal.stage : null;
|
||||
const callbacks = terminalStage ? [...stages, terminalStage] : stages;
|
||||
// Element types must line up stage to stage.
|
||||
let elem = chain.sourceType.elem;
|
||||
for (const stage of callbacks) {
|
||||
if (!typeEquals(stage.info.elem, elem)) return null;
|
||||
if (stage.info.method === "filter" && !typeEquals(stage.info.outElem, stage.info.elem))
|
||||
return null;
|
||||
if (
|
||||
(stage.info.method === "map" || stage.info.method === "flatMap") &&
|
||||
!typeEquals(stage.info.outElem, stage.info.fnRet)
|
||||
)
|
||||
return null;
|
||||
elem = stage.info.outElem;
|
||||
}
|
||||
// Index arguments are reproduced; an array argument only names the source.
|
||||
for (let k = 0; k < callbacks.length; k++) {
|
||||
const stage = callbacks[k]!;
|
||||
const lead = stage.info.method === "reduce" ? 1 : 0;
|
||||
if (stage.info.arity > lead + 3) return null;
|
||||
if (stage.info.arity === lead + 3 && (k !== 0 || chain.slice)) return null;
|
||||
}
|
||||
if (terminalStage?.info.method === "reduce" && !terminalStage.info.hasInit) return null;
|
||||
const holes = holeCarrying(chain);
|
||||
if (terminalStage?.info.method === "findIndex" && holes) return null;
|
||||
// Effects of each callback, in chain order.
|
||||
const scope = this.effects.scope(fn, true);
|
||||
const summaries: Effects[] = [];
|
||||
@@ -832,30 +939,89 @@ class PipelineFusion {
|
||||
* are passed in by value, read when the fused call starts; no callback
|
||||
* in the chain assigns a binding another one reads). */
|
||||
private readonly inlineTarget: InlineTarget = (arg) => {
|
||||
const fn = this.closureTarget(arg);
|
||||
if (!fn) return null;
|
||||
const captures = new Set((fn.captures ?? []).map((c) => c.localId));
|
||||
const effects = this.effects.ofFunction(fn);
|
||||
if (!effects || intersects(effects.writes, captures)) return null;
|
||||
return inlinableBody(fn, captures) ? fn : null;
|
||||
};
|
||||
|
||||
/** inlineTarget for a callback created in `fn` whose captured bindings
|
||||
* nothing assigns after their declaration: `fn`'s own locals and
|
||||
* parameters, written neither by `fn` nor by any closure that shares
|
||||
* them, and not by the callback body. A binding a local class captures
|
||||
* counts as written, since its methods may assign it; so does a binding
|
||||
* a method reads through its class's captures. The callback must
|
||||
* not observe the ambient receiver (AmbientReceiverReaders). */
|
||||
private stableTarget(fn: IrFunction): InlineTarget {
|
||||
const locals = new Map(fn.locals.map((l) => [l.id, l]));
|
||||
const inherited = new Set(
|
||||
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
|
||||
);
|
||||
const written = this.written.get(fn.name);
|
||||
const classCaptured = this.classCaptured(fn);
|
||||
return (arg) => {
|
||||
const target = this.closureTarget(arg);
|
||||
if (!target || !written || arg.kind !== "closure") return null;
|
||||
if (!this.receivers.ignores(target)) return null;
|
||||
for (const id of arg.captures) {
|
||||
const local = locals.get(id);
|
||||
if (!local || local.tdz || inherited.has(id) || written.has(id) || classCaptured.has(id))
|
||||
return null;
|
||||
}
|
||||
const captures = new Set((target.captures ?? []).map((c) => c.localId));
|
||||
const assigns = !everyStmtList(target.body, {
|
||||
stmt: (st) => !((st.kind === "assign" || st.kind === "forOf") && captures.has(st.localId)),
|
||||
expr: (ex) =>
|
||||
!((ex.kind === "assignExpr" || ex.kind === "incDec") && captures.has(ex.localId)),
|
||||
});
|
||||
return !assigns && inlinableBody(target, captures) ? target : null;
|
||||
};
|
||||
}
|
||||
|
||||
/** The bindings of `fn` that a local class it creates captures. */
|
||||
private classCaptured(fn: IrFunction): ReadonlySet<string> {
|
||||
const known = this.classCaptures.get(fn.name);
|
||||
if (known) return known;
|
||||
const ids = new Set<string>();
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "classRef") for (const id of e.captures ?? []) ids.add(id);
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
this.classCaptures.set(fn.name, ids);
|
||||
return ids;
|
||||
}
|
||||
|
||||
/** The function behind a closure-literal callback the loop can inline. */
|
||||
private closureTarget(arg: IrExpr): IrFunction | null {
|
||||
if (arg.kind !== "closure" || arg.adapts) return null;
|
||||
const fn = this.effects.fn(arg.fnName);
|
||||
if (!fn || fn.async || fn.generator || fn.classCaptures?.length) return null;
|
||||
const captures = new Set((fn.captures ?? []).map((c) => c.localId));
|
||||
if (captures.size !== arg.captures.length) return null;
|
||||
const effects = this.effects.ofFunction(fn);
|
||||
if (!effects || intersects(effects.writes, captures)) return null;
|
||||
if (fn.locals.some((l) => l.tdz || (l.boxed && !captures.has(l.id)))) return null;
|
||||
let size = 0;
|
||||
const small = everyStmtList(fn.body, {
|
||||
expr: (e) => ++size <= MAX_INLINE_NODES && e.kind !== "closure" && e.kind !== "selfRef",
|
||||
stmt: (s) => ++size <= MAX_INLINE_NODES && s.kind !== "tryCatch",
|
||||
});
|
||||
return small ? fn : null;
|
||||
};
|
||||
if ((fn.captures ?? []).length !== arg.captures.length) return null;
|
||||
return fn;
|
||||
}
|
||||
|
||||
private emit(plan: Plan): IrExpr | null {
|
||||
const built = new FusedLoop(this.lowerer, plan.chain, this.inlineTarget).build();
|
||||
const built = new FusedLoop(
|
||||
this.lowerer,
|
||||
plan.chain,
|
||||
plan.inline ?? this.inlineTarget,
|
||||
this.contractTypes,
|
||||
).build();
|
||||
if (!built) return null;
|
||||
for (const id of built.inlinedCaptures) this.inlinedCaptures.add(id);
|
||||
let name = this.helpers.get(built.key);
|
||||
if (!name) {
|
||||
name = keyedHelperName(this.lowerer, "%arr.fused", built.key);
|
||||
name = keyedHelperName(this.lowerer, plan.inline ? "%arr.loop" : "%arr.fused", built.key);
|
||||
this.helpers.set(built.key, name);
|
||||
this.added.push({ ...built.fn, name });
|
||||
const helper: IrFunction = { ...built.fn, name };
|
||||
// A lone call's loop stands in the inlined callback's stack frame.
|
||||
if (plan.frame !== undefined) helper.sourceName = plan.frame;
|
||||
this.added.push(helper);
|
||||
}
|
||||
return {
|
||||
kind: "call",
|
||||
@@ -867,6 +1033,48 @@ class PipelineFusion {
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a statement inside `e` (a sequence expression's) returns. */
|
||||
function returns(e: IrExpr): boolean {
|
||||
return !everyExpr(e, { expr: () => true, stmt: (s) => s.kind !== "return" });
|
||||
}
|
||||
|
||||
/** Drops the box of each binding in `inlined` that no closure or class
|
||||
* created in `fn` captures any more: the callbacks that captured it were
|
||||
* copied into loops, which receive its value. A dead-zone binding keeps the
|
||||
* box that represents its uninitialized state, and bindings `fn` itself
|
||||
* captures keep theirs. */
|
||||
function unboxUncaptured(fn: IrFunction, inlined: ReadonlySet<string>): void {
|
||||
const captured = new Set<string>();
|
||||
everyStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
if (e.kind === "closure" || e.kind === "classRef")
|
||||
for (const id of e.captures ?? []) captured.add(id);
|
||||
return true;
|
||||
},
|
||||
stmt: () => true,
|
||||
});
|
||||
const inherited = new Set(
|
||||
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
|
||||
);
|
||||
fn.locals = fn.locals.map((local): IrLocal => {
|
||||
if (local.boxed !== true || local.tdz === true || !inlined.has(local.id)) return local;
|
||||
if (captured.has(local.id) || inherited.has(local.id)) return local;
|
||||
const { boxed: _boxed, ...plain } = local;
|
||||
return plain;
|
||||
});
|
||||
}
|
||||
|
||||
/** A callback body the loop can copy: small, no nested closures, recursion
|
||||
* or try statements, and no boxed locals besides its captures. */
|
||||
function inlinableBody(fn: IrFunction, captures: ReadonlySet<string>): boolean {
|
||||
if (fn.locals.some((l) => l.tdz || (l.boxed && !captures.has(l.id)))) return false;
|
||||
let size = 0;
|
||||
return everyStmtList(fn.body, {
|
||||
expr: (e) => ++size <= MAX_INLINE_NODES && e.kind !== "closure" && e.kind !== "selfRef",
|
||||
stmt: (s) => ++size <= MAX_INLINE_NODES && s.kind !== "tryCatch",
|
||||
});
|
||||
}
|
||||
|
||||
function intersects(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
|
||||
for (const id of a) if (b.has(id)) return true;
|
||||
return false;
|
||||
@@ -989,10 +1197,13 @@ class FusedLoop {
|
||||
private next = 0;
|
||||
private failed = false;
|
||||
|
||||
/** `contractTypes`: the program writes contract types, which the
|
||||
* contract checker binds to source bindings. */
|
||||
constructor(
|
||||
private readonly lowerer: Lowerer,
|
||||
private readonly chain: Chain,
|
||||
private readonly inlineTarget: InlineTarget,
|
||||
private readonly contractTypes: boolean,
|
||||
) {
|
||||
this.loc = chain.loc;
|
||||
}
|
||||
@@ -1001,9 +1212,9 @@ class FusedLoop {
|
||||
return `${name}.${this.next++}`;
|
||||
}
|
||||
|
||||
private local(name: string, type: IrType, mutable = false): string {
|
||||
private local(name: string, type: IrType, mutable = false, source?: IrBindingSource): string {
|
||||
const id = this.fresh(name);
|
||||
this.locals.push({ id, name, type, mutable });
|
||||
this.locals.push(source ? { id, name, type, mutable, source } : { id, name, type, mutable });
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -1070,21 +1281,50 @@ class FusedLoop {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
// Copies keep their source bindings, so contract types written on
|
||||
// them are checked against what the loop stores. In a program that
|
||||
// writes contract types, a parameter with a source binding keeps its
|
||||
// own local for the same reason.
|
||||
const sources = new Map(fn.locals.map((l) => [l.id, l.source]));
|
||||
const aliased = new Set<string>();
|
||||
fn.params.forEach((p, index) => {
|
||||
const arg = args[index]!;
|
||||
if (arg.kind === "varRef" && !assigned.has(p.localId) && typeEquals(arg.type, p.type)) {
|
||||
if (
|
||||
arg.kind === "varRef" &&
|
||||
!assigned.has(p.localId) &&
|
||||
typeEquals(arg.type, p.type) &&
|
||||
!(this.contractTypes && sources.get(p.localId) !== undefined)
|
||||
) {
|
||||
renamed.set(p.localId, arg.localId);
|
||||
aliased.add(p.localId);
|
||||
}
|
||||
});
|
||||
for (const l of fn.locals) {
|
||||
if (renamed.has(l.id)) continue;
|
||||
const id = this.local(l.name, l.type, true);
|
||||
const id = this.local(l.name, l.type, true, l.source);
|
||||
renamed.set(l.id, id);
|
||||
}
|
||||
const rename = (id: string): string => renamed.get(id) ?? id;
|
||||
const renameExpr = (e: IrExpr): IrExpr => {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
case "incDec":
|
||||
case "assignExpr":
|
||||
return { ...e, localId: rename(e.localId) };
|
||||
default:
|
||||
return e;
|
||||
}
|
||||
};
|
||||
// A bound parameter's value is an element or index with no source text
|
||||
// of its own; it is located at the parameter, without a span.
|
||||
const bind: IrStmt[] = fn.params.flatMap((p, index): IrStmt[] => {
|
||||
if (aliased.has(p.localId)) return [];
|
||||
const source = sources.get(p.localId);
|
||||
const arg = args[index]!;
|
||||
const init = source ? { ...arg, loc: { ...source.loc, end: source.loc.start } } : arg;
|
||||
return [{ kind: "varDecl", localId: rename(p.localId), init, loc }];
|
||||
});
|
||||
const labels = new Map<string, string>();
|
||||
const exit = this.fresh("%fused.exit");
|
||||
const label = (name: string): string => {
|
||||
let id = labels.get(name);
|
||||
if (!id) {
|
||||
@@ -1093,19 +1333,53 @@ class FusedLoop {
|
||||
}
|
||||
return id;
|
||||
};
|
||||
const result = fn.returnType.kind === "void" ? null : this.local("ret", fn.returnType, true);
|
||||
const rename = (id: string): string => renamed.get(id) ?? id;
|
||||
const body = transformStmtList(fn.body, {
|
||||
expr: (e) => {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
case "incDec":
|
||||
case "assignExpr":
|
||||
return { ...e, localId: rename(e.localId) };
|
||||
default:
|
||||
return e;
|
||||
/** Locals and labels of a copied statement; returns are the caller's. */
|
||||
const renameStmt = (st: IrStmt): IrStmt => {
|
||||
switch (st.kind) {
|
||||
case "varDecl":
|
||||
case "assign":
|
||||
return { ...st, localId: rename(st.localId) };
|
||||
case "break":
|
||||
case "continue":
|
||||
return st.label ? { ...st, label: label(st.label) } : st;
|
||||
case "forOf": {
|
||||
const renamedFor = { ...st, localId: rename(st.localId) };
|
||||
return st.labels ? { ...renamedFor, labels: st.labels.map(label) } : renamedFor;
|
||||
}
|
||||
},
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "for":
|
||||
case "switch":
|
||||
case "block":
|
||||
return st.labels ? { ...st, labels: st.labels.map(label) } : st;
|
||||
default:
|
||||
return st;
|
||||
}
|
||||
};
|
||||
// A body whose only return is its last statement (`(x) => x * k`,
|
||||
// `{ const y = x * 2; return y > 4; }`) needs no exit block: its other
|
||||
// statements run after the bindings, and the call's value is the
|
||||
// returned expression.
|
||||
const last = fn.body[fn.body.length - 1];
|
||||
const prefix = fn.body.slice(0, -1);
|
||||
if (
|
||||
last !== undefined &&
|
||||
last.kind === "return" &&
|
||||
last.value !== null &&
|
||||
typeEquals(last.value.type, fn.returnType) &&
|
||||
!returns(last.value) &&
|
||||
prefix.every((st) => everyStmt(st, { expr: () => true, stmt: (x) => x.kind !== "return" }))
|
||||
) {
|
||||
const copy = { expr: renameExpr, stmt: renameStmt };
|
||||
return {
|
||||
pre: [...bind, ...transformStmtList(prefix, copy)],
|
||||
value: transformExpr(last.value, copy),
|
||||
};
|
||||
}
|
||||
const exit = this.fresh("%fused.exit");
|
||||
const result = fn.returnType.kind === "void" ? null : this.local("ret", fn.returnType, true);
|
||||
const body = transformStmtList(fn.body, {
|
||||
expr: renameExpr,
|
||||
stmt: (s): IrStmt => {
|
||||
switch (s.kind) {
|
||||
case "return": {
|
||||
@@ -1125,27 +1399,14 @@ class FusedLoop {
|
||||
loc: s.loc,
|
||||
};
|
||||
}
|
||||
case "varDecl":
|
||||
case "assign":
|
||||
return { ...s, localId: rename(s.localId) };
|
||||
case "break":
|
||||
case "continue":
|
||||
return s.label && s.label !== exit ? { ...s, label: label(s.label) } : s;
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "for":
|
||||
case "block":
|
||||
return s.labels ? { ...s, labels: s.labels.map(label) } : s;
|
||||
default:
|
||||
return s;
|
||||
return renameStmt(s);
|
||||
}
|
||||
},
|
||||
});
|
||||
const bind: IrStmt[] = fn.params.flatMap((p, index): IrStmt[] =>
|
||||
aliased.has(p.localId)
|
||||
? []
|
||||
: [{ kind: "varDecl", localId: rename(p.localId), init: args[index]!, loc }],
|
||||
);
|
||||
const pre: IrStmt[] = [];
|
||||
if (result !== null) pre.push({ kind: "varDecl", localId: result, init: null, loc });
|
||||
pre.push({ kind: "block", labels: [exit], body: [...bind, ...body], loc });
|
||||
@@ -1158,7 +1419,13 @@ class FusedLoop {
|
||||
};
|
||||
}
|
||||
|
||||
build(): { fn: Omit<IrFunction, "name">; args: IrExpr[]; key: string } | null {
|
||||
build(): {
|
||||
fn: Omit<IrFunction, "name">;
|
||||
args: IrExpr[];
|
||||
key: string;
|
||||
/** Bindings the callbacks copied into the loop captured at this site. */
|
||||
inlinedCaptures: string[];
|
||||
} | null {
|
||||
const { chain, loc } = this;
|
||||
const arrT = chain.sourceType;
|
||||
const a = this.param("a", chain.source, "src");
|
||||
@@ -1645,7 +1912,10 @@ class FusedLoop {
|
||||
};
|
||||
foldKnownUnions(fn);
|
||||
scalarizeRecordTemporaries(this.lowerer, fn);
|
||||
return { fn, args: this.args, key: this.keyParts.join("|") };
|
||||
const inlinedCaptures = this.callees.flatMap((c) =>
|
||||
c.inline && c.arg.kind === "closure" ? c.arg.captures : [],
|
||||
);
|
||||
return { fn, args: this.args, key: this.keyParts.join("|"), inlinedCaptures };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1744,8 +2014,11 @@ function foldKnownUnions(fn: Omit<IrFunction, "name">): void {
|
||||
case "for":
|
||||
case "while":
|
||||
case "doWhile":
|
||||
case "forOf":
|
||||
case "block":
|
||||
return { ...s, body: prune(s.body) };
|
||||
case "switch":
|
||||
return { ...s, cases: s.cases.map((c) => ({ ...c, body: prune(c.body) })) };
|
||||
default:
|
||||
return s;
|
||||
}
|
||||
@@ -1933,6 +2206,19 @@ function scalarizeRecordTemporaries(lowerer: Lowerer, fn: Omit<IrFunction, "name
|
||||
body: rewriteList(s.body),
|
||||
},
|
||||
];
|
||||
case "forOf":
|
||||
return [{ ...s, iterable: rewriteExpr(s.iterable), body: rewriteList(s.body) }];
|
||||
case "switch":
|
||||
return [
|
||||
{
|
||||
...s,
|
||||
disc: rewriteExpr(s.disc),
|
||||
cases: s.cases.map((c) => ({
|
||||
test: c.test === null ? null : rewriteExpr(c.test),
|
||||
body: rewriteList(c.body),
|
||||
})),
|
||||
},
|
||||
];
|
||||
case "block":
|
||||
return [{ ...s, body: rewriteList(s.body) }];
|
||||
default: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { keyedHelperName } from "../stable-names.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { primitiveElement } from "../array-values.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import {
|
||||
@@ -144,12 +145,22 @@ export function lowerArrayCallback(
|
||||
const untyped = (checkerType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0;
|
||||
const raw = untyped ? null : lowerer.mapTypeOf(checkerType);
|
||||
const mapped = raw ? lowerer.runtimeOptionalBindingType(param.name, raw) : null;
|
||||
// A helper that passes present number, string or boolean elements
|
||||
// (a `(T | undefined)[]` stores T elements) binds a `T | undefined`
|
||||
// parameter to T, so the callback takes them without an adapter.
|
||||
const presentElement =
|
||||
mapped !== null &&
|
||||
primitiveElement(expected) &&
|
||||
mapped.kind === "union" &&
|
||||
lowerer.armTag(mapped.unionId, UNDEFINED_T) >= 0 &&
|
||||
typeEquals(lowerer.stripUndefinedArm(mapped), expected);
|
||||
if (
|
||||
untyped
|
||||
? !bindUntyped
|
||||
: mapped === null ||
|
||||
(lowerer.coercibleValue(expected, mapped) &&
|
||||
!runtimeOptionalPairVariant(lowerer, expected, mapped))
|
||||
!runtimeOptionalPairVariant(lowerer, expected, mapped) &&
|
||||
!presentElement)
|
||||
)
|
||||
return;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user