mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Fix the latin1 decode bounds 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>
|
||||
|
||||
@@ -31,18 +31,25 @@
|
||||
`checkPrime`, `checkPrimeSync`, `generatePrime`, and `generatePrimeSync` support bigint, ArrayBuffer, typed-array, Buffer, and DataView values and the `checks`, `safe`, `add`, `rem`, and `bigint` options on native targets, with Node's errors; the callback forms compute before they return and call back in a later macrotask.
|
||||
Candidates and sizes longer than 262144 bits throw a coded SC2020 error, and wasm32-wasi builds are refused.
|
||||
Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`.
|
||||
RSA, EC, Ed25519, and X25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets.
|
||||
RSA, RSA-PSS, EC, Ed25519, and X25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets.
|
||||
`generateKeyPairSync` and `generateKeyPair` check their arguments in Node's order, with Node's errors, including the OpenSSL errors for RSA modulus lengths below 512 or above 2147483647 bits and for public exponents of 0, 1, or an even number, which `generateKeyPair` passes to its callback.
|
||||
RSA keys generate with even modulus lengths from 1024 to 8192 bits and public exponents up to 2147483647; other lengths and exponents that Node accepts, and `paramEncoding: 'explicit'`, throw a coded SC2020 error.
|
||||
RSA keys generate with modulus lengths from 512 to 8192 bits, an odd length from 2048 bits yielding one bit less as in Node, and odd public exponents up to 4294967295; longer moduli that Node accepts, and `paramEncoding: 'explicit'`, throw a coded SC2020 error.
|
||||
The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1.
|
||||
Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 and X25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate.
|
||||
EC, Ed25519, and X25519 keys also read and write the `raw-public` and `raw-private` formats, including compressed EC points.
|
||||
Raw X448, Ed448, and post-quantum keys, EC scalars of 0 or at least the curve order, and the EC point at infinity, which Node accepts, throw a coded SC2020 error.
|
||||
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.
|
||||
RSA-PSS keys follow their RSASSA-PSS-params as Node does: PSS padding only, the key's digest and MGF1 digest, and salts of at least the key's length, with OpenSSL's errors; their details name the parameters, they export as PKCS#8 and SubjectPublicKeyInfo only, and digests other than SHA-1 and SHA-2 in their options throw a coded SC2020 error.
|
||||
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.
|
||||
Private keys export encrypted (`cipher`, `passphrase`) with aes-128, aes-192, and aes-256 in CBC and ECB modes and with des-ede3-cbc, PKCS#8 as PBES2 with PBKDF2-HMAC-SHA256 and PKCS#1 and SEC1 as traditional PEM, and the ciphers OpenSSL refuses for a form throw its errors; a cipher option can also be an OID; other ciphers Node encrypts with throw a coded SC2020 error.
|
||||
Encrypted private keys import with a `passphrase` from PBES2 with HMAC-SHA PRFs and the CBC and ECB ciphers, and from traditional PEM with the CBC ciphers, with Node's errors for a missing or wrong passphrase; other schemes and ciphers throw a coded SC2020 error once a passphrase is given.
|
||||
The other key types (`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.
|
||||
`publicEncrypt`/`privateDecrypt` remain unsupported.
|
||||
The WebCrypto `crypto` global, which node:crypto also exports as `webcrypto`, supports `randomUUID()` and `getRandomValues()`, also read as values from untyped JavaScript.
|
||||
|
||||
@@ -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: 45, marker: "scr_runtime_abi_v45" },
|
||||
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_v45");
|
||||
});
|
||||
});
|
||||
|
||||
+141
-17
@@ -2914,6 +2914,8 @@ declare module "crypto" {
|
||||
export interface AsymmetricKeyExportOptions<T extends KeyFormat> {
|
||||
type: "pkcs1" | "spki" | "pkcs8" | "sec1";
|
||||
format: T;
|
||||
cipher?: string | undefined;
|
||||
passphrase?: string | Buffer | undefined;
|
||||
}
|
||||
export interface AsymmetricKeyDetails {
|
||||
modulusLength?: number;
|
||||
@@ -2938,6 +2940,7 @@ declare module "crypto" {
|
||||
format?: "pem" | "der" | undefined;
|
||||
type?: "pkcs1" | "pkcs8" | "sec1" | undefined;
|
||||
encoding?: string | undefined;
|
||||
passphrase?: string | Buffer | undefined;
|
||||
}
|
||||
export interface PublicKeyInput {
|
||||
key: string | Buffer;
|
||||
@@ -2974,12 +2977,22 @@ declare module "crypto" {
|
||||
modulusLength: number;
|
||||
publicExponent?: number | undefined;
|
||||
publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF };
|
||||
privateKeyEncoding: { type: "pkcs1" | "pkcs8"; format: PrivF };
|
||||
privateKeyEncoding: {
|
||||
type: "pkcs1" | "pkcs8";
|
||||
format: PrivF;
|
||||
cipher?: string | undefined;
|
||||
passphrase?: string | undefined;
|
||||
};
|
||||
}
|
||||
export interface ECKeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
|
||||
namedCurve: string;
|
||||
publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF };
|
||||
privateKeyEncoding: { type: "sec1" | "pkcs8"; format: PrivF };
|
||||
privateKeyEncoding: {
|
||||
type: "sec1" | "pkcs8";
|
||||
format: PrivF;
|
||||
cipher?: string | undefined;
|
||||
passphrase?: string | undefined;
|
||||
};
|
||||
}
|
||||
export function generateKeyPairSync(
|
||||
type: "rsa",
|
||||
@@ -3007,7 +3020,12 @@ declare module "crypto" {
|
||||
): KeyPairKeyObjectResult;
|
||||
export interface ED25519KeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
|
||||
publicKeyEncoding: { type: "spki"; format: PubF };
|
||||
privateKeyEncoding: { type: "pkcs8"; format: PrivF };
|
||||
privateKeyEncoding: {
|
||||
type: "pkcs8";
|
||||
format: PrivF;
|
||||
cipher?: string | undefined;
|
||||
passphrase?: string | undefined;
|
||||
};
|
||||
}
|
||||
export function generateKeyPairSync(
|
||||
type: "ed25519",
|
||||
@@ -3020,7 +3038,12 @@ declare module "crypto" {
|
||||
export function generateKeyPairSync(type: "ed25519", options?: {}): KeyPairKeyObjectResult;
|
||||
export interface X25519KeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
|
||||
publicKeyEncoding: { type: "spki"; format: PubF };
|
||||
privateKeyEncoding: { type: "pkcs8"; format: PrivF };
|
||||
privateKeyEncoding: {
|
||||
type: "pkcs8";
|
||||
format: PrivF;
|
||||
cipher?: string | undefined;
|
||||
passphrase?: string | undefined;
|
||||
};
|
||||
}
|
||||
export function generateKeyPairSync(
|
||||
type: "x25519",
|
||||
@@ -3551,17 +3574,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
|
||||
@@ -5294,10 +5345,10 @@ declare module "node:zlib" {
|
||||
* with the real port discoverable as address().port (the composed read
|
||||
* is the lowered form — bare address() has no lowering); close([cb])
|
||||
* firing once connections drain; socket write/end/destroy/pipe and the
|
||||
* data/end/close/error/connect events (on and once — listeners take at
|
||||
* most the declared parameter). write/end return void here (Node returns
|
||||
* the backpressure boolean / the socket); on/once return void (chaining
|
||||
* is fenced). connect's host is a numeric IP or "localhost" (pinned to
|
||||
* data/end/close/error/connect/drain events (on and once — listeners
|
||||
* take at most the declared parameter). write returns Node's
|
||||
* backpressure boolean; end returns void here (Node returns the socket);
|
||||
* on/once return void (chaining is fenced). connect's host is a numeric IP or "localhost" (pinned to
|
||||
* 127.0.0.1 — SEMANTICS.md); every failure is the async 'error' event,
|
||||
* and an 'error' with no listener exits 1 like an unhandled EventEmitter
|
||||
* 'error'. */
|
||||
@@ -5329,7 +5380,13 @@ declare module "net" {
|
||||
readonly bytesWritten: number;
|
||||
/* write's callback runs once the data is flushed, with null; end's
|
||||
* once the socket finishes. */
|
||||
write(data: string | Uint8Array, callback?: (err?: Error | null) => void): void;
|
||||
/* false when the bytes this write left unsent reach the 65536-byte
|
||||
* high-water mark, as in Node; 'drain' fires when they are all sent. */
|
||||
write(data: string | Uint8Array, callback?: (err?: Error | null) => void): boolean;
|
||||
/* the bytes written and not yet sent */
|
||||
readonly writableLength: number;
|
||||
/* a write answered false and 'drain' has not fired yet */
|
||||
readonly writableNeedDrain: boolean;
|
||||
end(callback?: () => void): void;
|
||||
end(data: string | Uint8Array, callback?: () => void): void;
|
||||
destroy(): void;
|
||||
@@ -5377,6 +5434,7 @@ declare module "net" {
|
||||
| "connect"
|
||||
| "ready"
|
||||
| "timeout"
|
||||
| "drain"
|
||||
| "readable"
|
||||
| "finish"
|
||||
| "secureConnect"
|
||||
@@ -5401,6 +5459,7 @@ declare module "net" {
|
||||
| "connect"
|
||||
| "ready"
|
||||
| "timeout"
|
||||
| "drain"
|
||||
| "readable"
|
||||
| "finish"
|
||||
| "secureConnect"
|
||||
@@ -5424,6 +5483,7 @@ declare module "net" {
|
||||
| "connect"
|
||||
| "ready"
|
||||
| "timeout"
|
||||
| "drain"
|
||||
| "readable"
|
||||
| "finish"
|
||||
| "secureConnect"
|
||||
@@ -5473,6 +5533,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 +5557,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 +5575,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 +6033,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 +6075,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 +6170,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
|
||||
|
||||
@@ -135,3 +135,16 @@ void scr_cyc_set_growth_cap(long cap);
|
||||
void scriptc_native_bound_cycle_growth(void) {
|
||||
scr_cyc_set_growth_cap(2);
|
||||
}
|
||||
|
||||
/* 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. 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,25 @@ import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
* receiver slots loop-invariant. Untagged accesses and calls still alias
|
||||
* every category.
|
||||
*
|
||||
* Header loads are also `!invariant.load`: no store or call between the
|
||||
* construction of a view and its release changes them. Without it, a data
|
||||
* pointer load ahead of a bounds branch is sunk into the in-bounds block,
|
||||
* and LICM keeps a conditionally executed load inside the loop. */
|
||||
* Header loads that only bound or address an access to the view are also
|
||||
* `!invariant.load`: no store or call between the construction of a view
|
||||
* and its release changes them, and their values reach only that access,
|
||||
* which runs while the view is alive. Without it, a data pointer load
|
||||
* ahead of a bounds branch is sunk into the in-bounds block, and LICM keeps
|
||||
* a conditionally executed load inside the loop. */
|
||||
export function bytesHeaderAlias(host: LlvmEmitterContext): string {
|
||||
const tag = host.memoryAliasAttachment("bytes:header");
|
||||
return tag === "" ? "" : `${tag}, !invariant.load !{}`;
|
||||
}
|
||||
|
||||
/** A header load whose value the program reads (`length`, `byteLength`)
|
||||
* keeps the alias tag only. The value can outlive the view, and LLVM may
|
||||
* move an invariant load to its use, past the release that frees the
|
||||
* view. */
|
||||
export function bytesHeaderValueAlias(host: LlvmEmitterContext): string {
|
||||
return host.memoryAliasAttachment("bytes:header");
|
||||
}
|
||||
|
||||
/** Element storage's tag, and the alias scope of the access being emitted
|
||||
* in a loop version's fast copy (bytesAccessScope). */
|
||||
export function bytesElementAlias(host: LlvmEmitterContext): string {
|
||||
|
||||
@@ -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,10 +20,11 @@ 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,
|
||||
STRING_CHAR_CODE_AT_DECL,
|
||||
STRING_SCAN_UNITS_DECL,
|
||||
stringScanType,
|
||||
} from "./string-reads.js";
|
||||
import { unitSafeLoop } from "./string-unit-versions.js";
|
||||
@@ -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,14 +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, after one runtime read publishes it
|
||||
* as the scan receiver, the published unit count (0 when the runtime does
|
||||
* not hold its units). Also the units pointer, whether the loop is to
|
||||
* 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 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;
|
||||
@@ -1378,16 +1445,30 @@ function emitUnitsLength(
|
||||
join = B.newLabel("bytes.version.length");
|
||||
B.condBr(ascii, join, publish);
|
||||
B.startBlock(publish);
|
||||
host.declare(STRING_CHAR_CODE_AT_DECL);
|
||||
const read = B.tmp(),
|
||||
receiver = B.tmp(),
|
||||
// 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. 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, `, !prof !{!"branch_weights", i32 1000, i32 1}`);
|
||||
B.startBlock(call);
|
||||
host.declare(STRING_SCAN_UNITS_DECL);
|
||||
const receiver = B.tmp(),
|
||||
same = B.tmp(),
|
||||
pointerSlot = B.tmp(),
|
||||
pointer = B.tmp(),
|
||||
countSlot = B.tmp(),
|
||||
count = B.tmp(),
|
||||
published = B.tmp();
|
||||
B.line(`${read} = call double @scr_str_char_code_at(ptr ${s}, double 0.0)`);
|
||||
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`);
|
||||
B.line(`${same} = icmp eq ptr ${receiver}, ${s}`);
|
||||
B.line(`${pointerSlot} = getelementptr inbounds ${type}, ptr @scr_str_scan, i32 0, i32 1`);
|
||||
|
||||
@@ -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;
|
||||
@@ -1796,7 +1981,8 @@ export class LlDyn {
|
||||
* BUILD the typed value (+1), or throw the catchable path-annotated
|
||||
* TypeError and return a dummy with the pending flag set. */
|
||||
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;
|
||||
@@ -2603,6 +2789,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(
|
||||
@@ -2629,7 +2830,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") {
|
||||
@@ -2657,14 +2869,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)})`,
|
||||
);
|
||||
@@ -2685,6 +2905,7 @@ export class LlDyn {
|
||||
unmatched,
|
||||
preserveRefs,
|
||||
`@${name}.cases`,
|
||||
trustedKinds,
|
||||
);
|
||||
for (const i of order) {
|
||||
B.startBlock(hits[i]!);
|
||||
@@ -2695,7 +2916,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,8 +205,20 @@ 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();
|
||||
emitter.mapEntryFusion = emitter.debug === null ? findMapEntryFusions(fn) : null;
|
||||
emitter.localStackUnions = findLocalStackUnions(
|
||||
fn,
|
||||
|
||||
@@ -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) {
|
||||
@@ -1301,6 +1299,42 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
` ret ptr %hashed`,
|
||||
`}`,
|
||||
``,
|
||||
// The same lookup for a key the emitter knows is an integer (`%ki`,
|
||||
// the double `%k`'s exact value): the direct index tests the integer
|
||||
// itself, so the double is converted only by the hashed probe. A
|
||||
// negative key is not below the index's size as an unsigned value.
|
||||
`define internal ptr @sc_map_entry_f64_int(ptr %m, i64 %ki, double %k) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`,
|
||||
` %nd32 = load i32, ptr %ndp`,
|
||||
` %nd = zext i32 %nd32 to i64`,
|
||||
` %inside = icmp ult i64 %ki, %nd`,
|
||||
` br i1 %inside, label %slot, label %outside`,
|
||||
`slot:`,
|
||||
sz === "i64" ? ` %i = add i64 %ki, 0` : ` %i = trunc i64 %ki to ${sz}`,
|
||||
` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`,
|
||||
` %dense = load ptr, ptr %dp`,
|
||||
` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`,
|
||||
` %s = load i32, ptr %sp`,
|
||||
` %empty = icmp eq i32 %s, 0`,
|
||||
` br i1 %empty, label %miss, label %hit`,
|
||||
`hit:`,
|
||||
` %e32 = sub i32 %s, 1`,
|
||||
sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`,
|
||||
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
|
||||
` %entries = load ptr, ptr %ep`,
|
||||
` %found = getelementptr inbounds %ScrMapNumEntry, ptr %entries, ${sz} %e`,
|
||||
` ret ptr %found`,
|
||||
`miss:`,
|
||||
` ret ptr null`,
|
||||
`outside:`,
|
||||
` %dense.any = icmp ne i64 %nd, 0`,
|
||||
` br i1 %dense.any, label %miss, label %slow`,
|
||||
`slow:`,
|
||||
` %hashed = call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)`,
|
||||
` ret ptr %hashed`,
|
||||
`}`,
|
||||
``,
|
||||
// Hashing (scr_map_find_f64): the SameValueZero key word, its
|
||||
// scr_key_mix hash (zero remapped to one) and the absent-key filter.
|
||||
`define internal ptr @sc_map_entry_f64_hashed(ptr %m, double %k) noinline ${FN_ATTRS} {`,
|
||||
@@ -1641,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";
|
||||
@@ -1772,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,
|
||||
@@ -91,6 +91,7 @@ import {
|
||||
} from "./string-construction.js";
|
||||
import { emitStringInputs } from "./string-lifetimes.js";
|
||||
import { LOOPS } from "./loop-state.js";
|
||||
import { loopAccessesTypedArrays } from "../loop-polls.js";
|
||||
|
||||
/** A callback loop version copies the loop; keep the copies small. */
|
||||
const MAX_CALLBACK_VERSION_COST = 400;
|
||||
@@ -105,6 +106,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 +479,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")
|
||||
@@ -1195,9 +1212,68 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
B.line(`store ptr ${fresh}, ptr ${slot}`);
|
||||
}
|
||||
};
|
||||
const workerBudget = emitter.workerLoopBudget(s);
|
||||
// A counted loop over typed arrays, or a loop to a typed array's
|
||||
// length, polls between blocks of WORKER_POLL_BLOCK iterations instead
|
||||
// of counting a budget in its body: the inner loop over a block then
|
||||
// has no exit besides its own bound, which LLVM can vectorize. A typed
|
||||
// array's length is fixed, so each block can take it as its limit when
|
||||
// the loop does not assign the array's binding.
|
||||
const stripMined =
|
||||
capturedLets.length === 0 &&
|
||||
integerSlot !== null &&
|
||||
((countedLoop !== null &&
|
||||
countedLimit !== null &&
|
||||
Math.abs(countedLoop.step) * WORKER_POLL_BLOCK <= 2 ** 52) ||
|
||||
(integerLoop !== null && integerArrayLoop === null && integerLoop.stableReceiver)) &&
|
||||
emitter.workerLoopPolls(s) &&
|
||||
loopAccessesTypedArrays(s);
|
||||
const workerBudget = stripMined ? null : emitter.workerLoopBudget(s);
|
||||
// One slot for every copy of the loop: a speculative copy resumes an
|
||||
// iteration in another copy's body, which then reads this bound.
|
||||
let blockBound: string | null = null;
|
||||
if (stripMined) {
|
||||
blockBound = B.slot();
|
||||
B.entryAllocas.push(
|
||||
`${blockBound} = alloca ${countedLoop ? "i64" : emitter.sizeType} ; cancellation poll block bound`,
|
||||
);
|
||||
}
|
||||
freshenBindings();
|
||||
emitter.emitLoopCopies(s, capturedLets.length === 0, le, (exit) => {
|
||||
if (stripMined) {
|
||||
const counter: StripMinedCounter = countedLoop
|
||||
? {
|
||||
slot: integerSlot!,
|
||||
type: "i64",
|
||||
signed: true,
|
||||
step: countedLoop.step,
|
||||
followers: countedLoop.followers.map((f, i) => ({
|
||||
slot: followerSlots[i]!,
|
||||
step: f.step,
|
||||
})),
|
||||
limit: () => countedLimit!,
|
||||
}
|
||||
: {
|
||||
slot: integerSlot!,
|
||||
type: emitter.sizeType,
|
||||
signed: false,
|
||||
step: 1,
|
||||
followers: [],
|
||||
limit: () => {
|
||||
const receiver = emitter.emitStableReceiver(integerLoop!.limitReceiver, []);
|
||||
const lenPtr = B.tmp();
|
||||
const len = B.tmp();
|
||||
B.line(
|
||||
`${lenPtr} = getelementptr inbounds %ScrBytes, ptr ${receiver.name}, i64 0, i32 1`,
|
||||
);
|
||||
B.line(
|
||||
`${len} = load ${emitter.sizeType}, ptr ${lenPtr}${bytesHeaderAlias(emitter)}`,
|
||||
);
|
||||
return len;
|
||||
},
|
||||
};
|
||||
emitStripMinedCountedLoop(emitter, s, counter, blockBound!, exit);
|
||||
return;
|
||||
}
|
||||
const lc = B.newLabel("loop.c");
|
||||
const lb = B.newLabel("loop.b");
|
||||
const lu =
|
||||
@@ -2061,7 +2137,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 +2199,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 +2213,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 +2228,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 +2256,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);
|
||||
@@ -2528,6 +2625,102 @@ function emitInt32KindLoop(
|
||||
|
||||
/** At the top of a loop copy's header: lets a speculative loop find its
|
||||
* ordinary copy's header, and keeps its third copy out of the loop. */
|
||||
/** Iterations between cancellation polls of a strip-mined counted loop. */
|
||||
const WORKER_POLL_BLOCK = 64;
|
||||
|
||||
/** The counter of a strip-mined loop: its slot, the LLVM integer type and
|
||||
* signedness of its values, its step, the counters stepped beside it, and
|
||||
* the limit, emitted in the outer header before each block. */
|
||||
interface StripMinedCounter {
|
||||
slot: string;
|
||||
type: string;
|
||||
signed: boolean;
|
||||
step: number;
|
||||
followers: { slot: string; step: number }[];
|
||||
limit: () => string;
|
||||
}
|
||||
|
||||
/** A counted `for` loop of a worker program as blocks of WORKER_POLL_BLOCK
|
||||
* iterations, with the cancellation poll between blocks: the outer header
|
||||
* tests the counter against the loop's limit, each block runs the body up
|
||||
* to the nearer of the limit and WORKER_POLL_BLOCK steps on, and the poll
|
||||
* follows the block. It polls as often as the budget it replaces, and the
|
||||
* inner loop's only exit besides `break` and `return` is its bound. */
|
||||
function emitStripMinedCountedLoop(
|
||||
emitter: LlEmitter,
|
||||
s: Extract<IrStmt, { kind: "for" }>,
|
||||
counter: StripMinedCounter,
|
||||
boundSlot: string,
|
||||
exit: string,
|
||||
): void {
|
||||
const B = emitter.B;
|
||||
const { slot, type, signed, step } = counter;
|
||||
const ascending = step > 0;
|
||||
const compare = !signed ? "ult" : ascending ? "slt" : "sgt";
|
||||
const saturating = `llvm.${signed ? "sadd" : "uadd"}.sat.${type}`;
|
||||
const nearer = `llvm.${!signed ? "umin" : ascending ? "smin" : "smax"}.${type}`;
|
||||
const wrap = signed ? "nsw" : "nuw";
|
||||
const lc = B.newLabel("loop.c");
|
||||
const block = B.newLabel("loop.block");
|
||||
const bc = B.newLabel("loop.bc");
|
||||
const lb = B.newLabel("loop.b");
|
||||
const lu = B.newLabel("loop.u");
|
||||
const poll = B.newLabel("loop.poll");
|
||||
emitter.declare(`declare ${type} @${saturating}(${type}, ${type})`);
|
||||
emitter.declare(`declare ${type} @${nearer}(${type}, ${type})`);
|
||||
B.br(lc);
|
||||
B.startBlock(lc);
|
||||
enterLoopHeader(emitter, lc);
|
||||
const start = B.tmp();
|
||||
const open = B.tmp();
|
||||
B.line(`${start} = load ${type}, ptr ${slot}`);
|
||||
const limit = counter.limit();
|
||||
B.line(`${open} = icmp ${compare} ${type} ${start}, ${limit}`);
|
||||
B.condBr(open, block, exit);
|
||||
B.startBlock(block);
|
||||
const stepped = B.tmp();
|
||||
const bound = B.tmp();
|
||||
B.line(
|
||||
`${stepped} = call ${type} @${saturating}(${type} ${start}, ${type} ${step * WORKER_POLL_BLOCK})`,
|
||||
);
|
||||
B.line(`${bound} = call ${type} @${nearer}(${type} ${stepped}, ${type} ${limit})`);
|
||||
B.line(`store ${type} ${bound}, ptr ${boundSlot}`);
|
||||
B.br(bc);
|
||||
B.startBlock(bc);
|
||||
const index = B.tmp();
|
||||
const end = B.tmp();
|
||||
const inBlock = B.tmp();
|
||||
B.line(`${index} = load ${type}, ptr ${slot}`);
|
||||
B.line(`${end} = load ${type}, ptr ${boundSlot}`);
|
||||
B.line(`${inBlock} = icmp ${compare} ${type} ${index}, ${end}`);
|
||||
B.condBr(inBlock, lb, poll);
|
||||
B.startBlock(lb);
|
||||
emitter.jumpTargets.push({
|
||||
kind: "loop",
|
||||
brkLabel: exit,
|
||||
contLabel: lu,
|
||||
labels: s.labels,
|
||||
frameDepth: emitter.frames.length,
|
||||
scopeDepth: emitter.scopes.length,
|
||||
finallyDepth: emitter.finallyStack.length,
|
||||
});
|
||||
emitter.emitBlock(s.body);
|
||||
emitter.jumpTargets.pop();
|
||||
B.br(lu);
|
||||
B.startBlock(lu);
|
||||
for (const { slot: stepSlot, step: by } of [{ slot, step }, ...counter.followers]) {
|
||||
const old = B.tmp();
|
||||
const next = B.tmp();
|
||||
B.line(`${old} = load ${type}, ptr ${stepSlot}`);
|
||||
B.line(`${next} = add ${wrap} ${type} ${old}, ${by}`);
|
||||
B.line(`store ${type} ${next}, ptr ${stepSlot}`);
|
||||
}
|
||||
B.br(bc);
|
||||
B.startBlock(poll);
|
||||
emitter.emitWorkerPoll();
|
||||
B.br(lc);
|
||||
}
|
||||
|
||||
function enterLoopHeader(emitter: LlEmitter, header: string): void {
|
||||
const dispatch = emitter.speculationHeader;
|
||||
emitter.speculationHeader = null;
|
||||
|
||||
@@ -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";
|
||||
@@ -123,7 +127,7 @@ import {
|
||||
} from "./int32-abi.js";
|
||||
import { type Int32Local } from "./int32-locals.js";
|
||||
import { findIntegerClones, NO_INTEGER_CLONES, type IntegerClones } from "./integer-clones.js";
|
||||
import { exactInteger, narrowInteger, widenInteger } from "./integer-values.js";
|
||||
import { exactInteger, narrowInteger, widenInteger, type LlInteger } from "./integer-values.js";
|
||||
import { findInitializerBindings } from "../../ir/initializer-bindings.js";
|
||||
import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/constant-tables.js";
|
||||
import {
|
||||
@@ -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,7 +1360,13 @@ 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). */
|
||||
numberMapKeyIntegers = new Map<string, LlInteger>();
|
||||
/** The current function's fused string-key Map get-then-set pairs (map-entry-fusion.ts). */
|
||||
mapEntryFusion: MapEntryFusion | null = null;
|
||||
readonly callLifetimes: CallLifetimes;
|
||||
@@ -1352,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. */
|
||||
@@ -1435,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). */
|
||||
@@ -1444,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();
|
||||
@@ -1480,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:
|
||||
@@ -1511,7 +1658,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
|
||||
@@ -1594,7 +1746,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. */
|
||||
@@ -1668,7 +1824,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;
|
||||
@@ -1727,12 +1888,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,
|
||||
@@ -1751,18 +1919,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 &&
|
||||
@@ -2208,6 +2364,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). */
|
||||
@@ -3733,13 +3929,19 @@ export class LlEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether `loop` needs a cancellation poll: in a worker program, unless
|
||||
* the loop already runs a pending test on every iteration, which observes
|
||||
* a terminate request itself (a separate poll would only add work). */
|
||||
workerLoopPolls(loop: Extract<IrStmt, { kind: "while" | "doWhile" | "for" | "forOf" }>): boolean {
|
||||
return (
|
||||
this.mod.workers === true && !loopTestsEveryIteration(loop, (e) => this.callTestsPending(e))
|
||||
);
|
||||
}
|
||||
|
||||
workerLoopBudget(
|
||||
loop: Extract<IrStmt, { kind: "while" | "doWhile" | "for" | "forOf" }>,
|
||||
): string | null {
|
||||
if (!this.mod.workers) return null;
|
||||
// A loop that runs a pending test on every iteration observes a
|
||||
// terminate request through it; a separate poll would only add work.
|
||||
if (loopTestsEveryIteration(loop, (e) => this.callTestsPending(e))) return null;
|
||||
if (!this.workerLoopPolls(loop)) return null;
|
||||
const slot = this.B.slot();
|
||||
this.B.entryAllocas.push(`${slot} = alloca i32 ; worker cancellation budget`);
|
||||
this.B.line(`store i32 0, ptr ${slot}`);
|
||||
@@ -3857,14 +4059,19 @@ export class LlEmitter {
|
||||
B.condBr(due, poll, next);
|
||||
B.startBlock(poll);
|
||||
B.line(`store i32 63, ptr ${slot}`);
|
||||
// Calls and throwing operations retain their own checks. Amortize the
|
||||
// extra cancellation poll across bounded loop batches so scalar work
|
||||
// does not cross the runtime ABI on every iteration.
|
||||
this.emitPendingCheck(undefined, true);
|
||||
this.emitWorkerPoll();
|
||||
B.br(next);
|
||||
B.startBlock(next);
|
||||
}
|
||||
|
||||
/** The cancellation poll itself. Calls and throwing operations retain
|
||||
* their own checks. Loops amortize this extra poll across bounded
|
||||
* batches of iterations so scalar work does not cross the runtime ABI on
|
||||
* every iteration. */
|
||||
emitWorkerPoll(): void {
|
||||
this.emitPendingCheck(undefined, true);
|
||||
}
|
||||
|
||||
/** Moves an already-evaluated value into the runtime's exception cell —
|
||||
* the `throw` statement's kind dispatch (stmts.ts's), shared with
|
||||
* every synthetic thrower. Ownership of a refcounted payload must have
|
||||
@@ -5065,6 +5272,7 @@ export class LlEmitter {
|
||||
chunkResult(): ChunkResult {
|
||||
return {
|
||||
entries: this.journal!.entries,
|
||||
debugRequests: this.debug?.recorded() ?? [],
|
||||
flags: {
|
||||
usesCoroutines: this.usesCoroutines,
|
||||
usesTimers: this.usesTimers,
|
||||
@@ -5100,6 +5308,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;
|
||||
@@ -5120,7 +5330,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
|
||||
@@ -5248,7 +5464,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
|
||||
@@ -5850,6 +6072,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);
|
||||
@@ -7147,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { alwaysExactInteger, exactInteger } from "./integer-values.js";
|
||||
import { byteNumberAccess } from "../../ir/byte-numbers.js";
|
||||
import { emitByteNumber } from "./byte-numbers.js";
|
||||
import { integerOrNaNIndex, integerOrNaNRange } from "./wrapping-sums.js";
|
||||
import { bytesElementAlias, bytesHeaderAlias } from "./bytes-alias.js";
|
||||
import { bytesElementAlias, bytesHeaderAlias, bytesHeaderValueAlias } from "./bytes-alias.js";
|
||||
|
||||
const BYTES_NUM_KIND: Record<string, { kind: number; le: boolean } | undefined> = {
|
||||
u8: { kind: 0, le: false },
|
||||
@@ -160,12 +160,19 @@ export function emitBytesIndex(
|
||||
return idx;
|
||||
}
|
||||
|
||||
/** The element count of a typed array, in the target's size type. */
|
||||
export function emitBytesHeaderLength(host: LlvmEmitterContext, receiver: string): string {
|
||||
/** The element count of a typed array, in the target's size type. `value`:
|
||||
* the program reads the count itself (bytesHeaderValueAlias), rather than
|
||||
* only bounding an access. */
|
||||
export function emitBytesHeaderLength(
|
||||
host: LlvmEmitterContext,
|
||||
receiver: string,
|
||||
value = false,
|
||||
): string {
|
||||
const p = host.B.tmp();
|
||||
const len = host.B.tmp();
|
||||
const alias = value ? bytesHeaderValueAlias(host) : bytesHeaderAlias(host);
|
||||
host.B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`);
|
||||
host.B.line(`${len} = load ${host.sizeType}, ptr ${p}${bytesHeaderAlias(host)}`);
|
||||
host.B.line(`${len} = load ${host.sizeType}, ptr ${p}${alias}`);
|
||||
return len;
|
||||
}
|
||||
|
||||
@@ -184,7 +191,7 @@ export function emitBytesLength(
|
||||
bytes: boolean,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const len = emitBytesHeaderLength(host, receiver);
|
||||
const len = emitBytesHeaderLength(host, receiver, true);
|
||||
const size = BYTES_ELEMENT_SIZE[elem];
|
||||
const count = bytes && size !== 1 ? B.tmp() : len;
|
||||
if (count !== len) B.line(`${count} = shl ${host.sizeType} ${len}, ${Math.log2(size)}`);
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
emitStringMapGetRef,
|
||||
emitStringMapGetScalar,
|
||||
emitStringMapHas,
|
||||
noteNumberMapKey,
|
||||
} from "./map-number-lookup.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { emitFusedMapSet, rememberMapEntry } from "./map-entry-fusion.js";
|
||||
@@ -1144,6 +1145,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,
|
||||
@@ -1548,6 +1608,7 @@ export function emitMapLikeIntrinsic(
|
||||
// Key borrowed (the runtime retains stored string keys); the
|
||||
// value MOVES in (replacement releases the old value inside).
|
||||
const k = emitBorrowedInput(host, e.args[0]!);
|
||||
if (kAcc === "f64") noteNumberMapKey(host, k, e.args[0]!);
|
||||
const v = host.emitExpr(e.args[1]!);
|
||||
const vAcc = elemAccess(receiverType.value);
|
||||
if (vAcc === "ref") host.toHeap(v);
|
||||
@@ -1574,6 +1635,7 @@ export function emitMapLikeIntrinsic(
|
||||
// Element borrowed (the runtime retains stored strings); the unit
|
||||
// value is 0. Re-adding overwrites in place, preserving insertion.
|
||||
const k = emitBorrowedInput(host, e.args[0]!);
|
||||
if (kAcc === "f64") noteNumberMapKey(host, k, e.args[0]!);
|
||||
if (kAcc === "f64" && host.readNumberMapWrites.has(e))
|
||||
emitNumberMapSetScalar(host, r.name, k.name, null, () =>
|
||||
host.mapSet(r.name, kAcc, "f64", k.name, f64Lit(0)),
|
||||
@@ -1583,7 +1645,10 @@ export function emitMapLikeIntrinsic(
|
||||
}
|
||||
case "has": {
|
||||
const k = borrowInputs ? host.emitReadReceiver(e.args[0]!) : host.emitExpr(e.args[0]!);
|
||||
if (kAcc === "f64") return { name: emitNumberMapHas(host, r.name, k.name), type: e.type };
|
||||
if (kAcc === "f64") {
|
||||
noteNumberMapKey(host, k, e.args[0]!);
|
||||
return { name: emitNumberMapHas(host, r.name, k.name), type: e.type };
|
||||
}
|
||||
if (kAcc === "str")
|
||||
return { name: emitStringMapHas(host, r.name, k.name, compact), type: e.type };
|
||||
host.declare(`declare zeroext i1 @scr_map_has_${kAcc}(ptr, ${kTy})`);
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -792,13 +792,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",
|
||||
@@ -1312,6 +1315,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();
|
||||
@@ -1129,6 +1164,36 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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
|
||||
@@ -1141,26 +1206,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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -309,6 +309,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
e.fn === "net.sockOnClose" ||
|
||||
e.fn === "net.sockOnConnect" ||
|
||||
e.fn === "net.sockOnTimeout" ||
|
||||
e.fn === "net.sockOnDrain" ||
|
||||
e.fn === "net.sockOnReadable" ||
|
||||
e.fn === "net.sockOnReady" ||
|
||||
e.fn === "http.reqOnEnd" ||
|
||||
@@ -329,6 +330,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
"net.sockOnClose": "scr_net_sock_on_close",
|
||||
"net.sockOnConnect": "scr_net_sock_on_connect",
|
||||
"net.sockOnTimeout": "scr_net_sock_on_timeout",
|
||||
"net.sockOnDrain": "scr_net_sock_on_drain",
|
||||
"net.sockOnReadable": "scr_net_sock_on_readable",
|
||||
"net.sockOnReady": "scr_net_sock_on_ready",
|
||||
"http.reqOnEnd": "scr_http_req_on_end",
|
||||
|
||||
@@ -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",
|
||||
@@ -1314,6 +1337,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"net.sockResetAndDestroy": "scr_net_sock_reset_and_destroy",
|
||||
"net.sockAddress": "scr_net_sock_address",
|
||||
"net.sockBytesWritten": "scr_net_sock_bytes_written",
|
||||
"net.sockWritableLength": "scr_net_sock_writable_length",
|
||||
"net.sockWritableNeedDrain": "scr_net_sock_writable_need_drain",
|
||||
"net.sockAddrField": "scr_net_sock_addr_field",
|
||||
"net.sockBytesRead": "scr_net_sock_bytes_read",
|
||||
"net.sockConnecting": "scr_net_sock_connecting",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -113,7 +113,8 @@ test("conditional and reassigned locals own payload snapshots without heap wrapp
|
||||
expect(facts(mod).get("item")?.ownerType).toEqual(STRING);
|
||||
for (const bits of [32, 64] as const) {
|
||||
const ir = body(mod, bits);
|
||||
expect(ir).toContain("call ptr @sc_map_entry_f64("); // inline number-key read
|
||||
// The inline number-key read, on the integer view of the literal key.
|
||||
expect(ir).toMatch(/call ptr @sc_map_entry_f64_int\(ptr %\S+, i64 \d+, double /);
|
||||
expect(ir).toContain("@scr_str_release");
|
||||
expect(ir).not.toMatch(/@scr_union_(?:new|retain|release)/);
|
||||
expect(ir).toContain("store i64 0");
|
||||
|
||||
@@ -118,7 +118,7 @@ test("string keys and reference values are retained inline, boolean slots compar
|
||||
});
|
||||
|
||||
/** work(map, k): map.has(k) on a number-keyed map. */
|
||||
function lookupFixture(): IrModule {
|
||||
function lookupFixture(key: IrExpr = ref("k", F64)): IrModule {
|
||||
const map: IrType = { kind: "map", key: F64, value: F64 };
|
||||
const params = [
|
||||
{ localId: "map", name: "map", type: map },
|
||||
@@ -128,7 +128,7 @@ function lookupFixture(): IrModule {
|
||||
kind: "mapIntrinsic",
|
||||
method: "has",
|
||||
receiver: ref("map", map),
|
||||
args: [ref("k", F64)],
|
||||
args: [key],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
@@ -182,6 +182,79 @@ test("hashed number-key lookups probe inline and call the runtime only for wide
|
||||
}
|
||||
});
|
||||
|
||||
test("a key with an integer view tests the direct index on the integer", () => {
|
||||
for (const bits of [32, 64] as const) {
|
||||
const sz = bits === 64 ? "i64" : "i32";
|
||||
const integral = emitted(lookupFixture({ kind: "numLit", value: 12, type: F64, loc }), bits);
|
||||
expect(integral.work).toMatch(/call ptr @sc_map_entry_f64_int\(ptr %\S+, i64 12, double /);
|
||||
const helper =
|
||||
/^define internal ptr @sc_map_entry_f64_int\(ptr %m, i64 %ki, double %k\)[^]*?^}/m.exec(
|
||||
integral.module,
|
||||
)![0];
|
||||
// An unsigned compare also rejects negative keys; the hashed probe takes
|
||||
// the double.
|
||||
expect(helper).toContain("icmp ult i64 %ki, %nd");
|
||||
expect(helper).toContain(`getelementptr inbounds i32, ptr %dense, ${sz} %i`);
|
||||
expect(helper).toContain("call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)");
|
||||
expect(helper).not.toContain("fptosi");
|
||||
// A fractional key and a key without a proof keep the double lookup.
|
||||
for (const key of [{ kind: "numLit", value: 0.5, type: F64, loc } as IrExpr, ref("k", F64)]) {
|
||||
const { work } = emitted(lookupFixture(key), bits);
|
||||
expect(work).toContain("call ptr @sc_map_entry_f64(");
|
||||
expect(work).not.toContain("@sc_map_entry_f64_int(");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("a key view converted in a branch is not reused by a later lookup of the same constant", () => {
|
||||
// work(map, k, taken): if (taken) return map.has((k = 2 ** 40)); return map.has(2 ** 40).
|
||||
const wide = 2 ** 40;
|
||||
const mod = lookupFixture();
|
||||
const work = mod.functions[1]!;
|
||||
const map = work.params[0]!.type;
|
||||
const has = (key: IrExpr): IrExpr => ({
|
||||
kind: "mapIntrinsic",
|
||||
method: "has",
|
||||
receiver: ref("map", map),
|
||||
args: [key],
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const literal: IrExpr = { kind: "numLit", value: wide, type: F64, loc };
|
||||
const taken = { localId: "taken", name: "taken", type: BOOL };
|
||||
work.params.push(taken);
|
||||
work.locals.push({ id: "taken", name: "taken", type: BOOL, mutable: true });
|
||||
work.body = [
|
||||
{
|
||||
kind: "if",
|
||||
cond: ref("taken", BOOL),
|
||||
then: [
|
||||
{
|
||||
kind: "return",
|
||||
value: has({
|
||||
kind: "assignExpr",
|
||||
localId: "k",
|
||||
value: literal,
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: has(literal), loc },
|
||||
];
|
||||
for (const bits of [32, 64] as const) {
|
||||
const { work: text } = emitted(mod, bits);
|
||||
// The lookup after the branch takes the literal, not a conversion made
|
||||
// inside the branch.
|
||||
expect(text).toMatch(/call ptr @sc_map_entry_f64_int\(ptr %\S+, i64 1099511627776, double /);
|
||||
expect(text).not.toMatch(/@sc_map_entry_f64_int\(ptr %\S+, i64 %/);
|
||||
}
|
||||
});
|
||||
|
||||
/** work(map): map.size, and keep(map): map returned as an owned value. */
|
||||
function sizeFixture(): IrModule {
|
||||
const map: IrType = { kind: "map", key: F64, value: F64 };
|
||||
|
||||
@@ -28,16 +28,44 @@
|
||||
* guard excludes. for-of starts at scr_map_iter_start, past a deleted
|
||||
* prefix.
|
||||
*/
|
||||
import type { IrType } from "../../ir/ir.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import type { IrExpr, IrType } from "../../ir/ir.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { mapKeyAccess } from "./shapes.js";
|
||||
|
||||
export const NUMBER_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_f64(ptr, double)";
|
||||
/** Record the integer view of a number Map/Set key operand for the entry
|
||||
* lookups of that key, when the view needs no instruction of its own: the
|
||||
* value's integer or uint32 view, defined with the value, or an integral
|
||||
* literal. A view exactInteger converts at this lookup is not recorded: the
|
||||
* key cache spans the function, and a later lookup of the same operand (a
|
||||
* constant, say) can be in a block the conversion does not dominate. */
|
||||
export function noteNumberMapKey(host: LlvmEmitterContext, key: LlValue, expr: IrExpr): void {
|
||||
if (key.type.kind !== "f64" || host.numberMapKeyIntegers.has(key.name)) return;
|
||||
if (!key.integer && key.uint32 === undefined && expr.kind !== "numLit") return;
|
||||
const integer = exactInteger(host, key, expr);
|
||||
if (!integer) return;
|
||||
const defined =
|
||||
integer.name === key.integer?.name ||
|
||||
integer.name === key.uint32 ||
|
||||
/^-?\d+$/.test(integer.name);
|
||||
if (defined) host.numberMapKeyIntegers.set(key.name, integer);
|
||||
}
|
||||
|
||||
/** The entry for `key`, or null when absent. Valid until the next mutation. */
|
||||
/** The entry for `key`, or null when absent. Valid until the next mutation.
|
||||
* A key with an integer view (noteNumberMapKey) tests the direct index on
|
||||
* that integer instead of converting the double back. */
|
||||
export function emitNumberMapEntry(host: LlvmEmitterContext, map: string, key: string): string {
|
||||
host.declare(NUMBER_MAP_ENTRY_DECL);
|
||||
const entry = host.B.tmp();
|
||||
const integer = host.numberMapKeyIntegers.get(key);
|
||||
if (integer) {
|
||||
const wide = widenInteger(host, integer);
|
||||
host.B.line(
|
||||
`${entry} = call ptr @sc_map_entry_f64_int(ptr ${map}, i64 ${wide}, double ${key})`,
|
||||
);
|
||||
return entry;
|
||||
}
|
||||
host.B.line(`${entry} = call ptr @sc_map_entry_f64(ptr ${map}, double ${key})`);
|
||||
return entry;
|
||||
}
|
||||
@@ -122,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
|
||||
@@ -141,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();
|
||||
@@ -160,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,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
emitStringMapGetScalar,
|
||||
emitStringMapGetRef,
|
||||
compactMapValues,
|
||||
noteNumberMapKey,
|
||||
} from "./map-number-lookup.js";
|
||||
import { mapKeyAccess, mapKeyParamType } from "./shapes.js";
|
||||
import { rememberMapEntry } from "./map-entry-fusion.js";
|
||||
@@ -68,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;
|
||||
@@ -126,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;
|
||||
@@ -150,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;
|
||||
}
|
||||
|
||||
@@ -265,6 +391,7 @@ export function emitMapLookupKey(
|
||||
const value = borrow ? host.emitReadReceiver(key) : host.emitExpr(key);
|
||||
const access = mapKeyAccess(key.type);
|
||||
const type = mapKeyParamType(access);
|
||||
if (access === "f64") noteNumberMapKey(host, value, key);
|
||||
return { access, types: type, args: `${type} ${value.name}`, value: value.name };
|
||||
}
|
||||
|
||||
@@ -303,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);
|
||||
@@ -327,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})`);
|
||||
@@ -337,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)",
|
||||
);
|
||||
|
||||
@@ -24,6 +24,12 @@ 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
|
||||
* 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
|
||||
|
||||
@@ -106,3 +106,34 @@ export function loopTestsEveryIteration(loop: Loop, tested: (call: IrExpr) => bo
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** True when the body of `loop`, outside its nested loops and closures,
|
||||
* reads or writes a typed-array element: the loops whose cancellation poll
|
||||
* the emitter moves between blocks of iterations, so that LLVM can
|
||||
* vectorize them. Other loops keep the budget in their body, which costs
|
||||
* less in a loop of a few iterations. */
|
||||
export function loopAccessesTypedArrays(loop: Loop): boolean {
|
||||
const inExpr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "bytesIntrinsic" && e.method === "get") return true;
|
||||
let found = false;
|
||||
everyExprChild(
|
||||
e,
|
||||
(child) => !(found = inExpr(child)),
|
||||
(child) => !(found = inStmt(child)),
|
||||
);
|
||||
return found;
|
||||
};
|
||||
const inStmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "bytesSet") return true;
|
||||
if (s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf")
|
||||
return false;
|
||||
let found = false;
|
||||
everyStmtChild(
|
||||
s,
|
||||
(child) => !(found = inExpr(child)),
|
||||
(child) => !(found = inStmt(child)),
|
||||
);
|
||||
return found;
|
||||
};
|
||||
return loop.body.some(inStmt);
|
||||
}
|
||||
|
||||
@@ -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 = 45 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v45" 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: 45, marker: "scr_runtime_abi_v45" },
|
||||
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: 45, marker: "scr_runtime_abi_v45" },
|
||||
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";
|
||||
|
||||
@@ -269,6 +269,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,
|
||||
@@ -279,6 +413,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;
|
||||
|
||||
@@ -12,7 +12,10 @@ export class CoercionState {
|
||||
// Provenance for checked extraction; consumers must not infer it from names.
|
||||
readonly checkedNarrows = new Set<string>();
|
||||
// The extracted arm of each checked (non-defaulting) extraction helper.
|
||||
readonly checkedNarrowArms = new Map<string, { unionId: string; tag: number; target: IrType }>();
|
||||
readonly checkedNarrowArms = new Map<
|
||||
string,
|
||||
{ unionId: string; tag: number; target: IrType; message?: string }
|
||||
>();
|
||||
readonly retags = new Map<string, string>();
|
||||
readonly narrows = new Map<string, string>();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user