mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-04 08:04:37 +08:00
Merge main
This commit is contained in:
@@ -42,17 +42,16 @@ Parsing, type checking, lowering, and LLVM IR generation run one after another o
|
||||
|
||||
### Partitions
|
||||
|
||||
The LLVM helper divides an executable's LLVM IR into partitions by its size: one partition per 4 MiB of LLVM text, rounded up to a power of two, and at most 64. `--emit=llvm` writes this text, so its file size shows how many partitions a program gets.
|
||||
The LLVM helper divides an executable's LLVM IR into partitions by its size. Up to 1 MiB of LLVM text is one partition, and up to 2 MiB is four. Larger programs get one partition per 4 MiB of LLVM text, rounded up to a power of two, and at least 8 and at most 64. `--emit=llvm` writes this text, so its file size shows how many partitions a program gets.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>LLVM text</th><th>Partitions</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Up to 4 MiB</td><td>1</td></tr>
|
||||
<tr><td>Over 4 MiB, up to 8 MiB</td><td>2</td></tr>
|
||||
<tr><td>Over 8 MiB, up to 16 MiB</td><td>4</td></tr>
|
||||
<tr><td>Over 16 MiB, up to 32 MiB</td><td>8</td></tr>
|
||||
<tr><td>Up to 1 MiB</td><td>1</td></tr>
|
||||
<tr><td>Over 1 MiB, up to 2 MiB</td><td>4</td></tr>
|
||||
<tr><td>Over 2 MiB, up to 32 MiB</td><td>8</td></tr>
|
||||
<tr><td>Over 32 MiB, up to 64 MiB</td><td>16</td></tr>
|
||||
<tr><td>Over 64 MiB, up to 128 MiB</td><td>32</td></tr>
|
||||
<tr><td>Over 128 MiB</td><td>64</td></tr>
|
||||
@@ -63,7 +62,7 @@ The count depends only on the size of the LLVM text, so a program has the same p
|
||||
|
||||
These builds are not divided into partitions and compile on one thread: `--emit=asm`, `--emit=obj`, and `--print=native-link-info` output, library builds (`--lib`; `dev` library builds are compiled in pieces, one after another), and `--sanitize` builds, which compile the LLVM IR with the C compiler.
|
||||
|
||||
The helper compiles partitions on a pool with one thread per hardware thread of the host, so an executable with two partitions uses at most two cores in the backend. A program of one partition is optimized and compiled on one thread.
|
||||
The helper compiles partitions on a pool with one thread per hardware thread of the host, so an executable with four partitions uses at most four cores in the backend. A program of one partition is optimized and compiled on one thread.
|
||||
|
||||
How partitions are compiled depends on `--optimization`:
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
`Buffer.from` accepts untyped strings, byte arrays, arrays, and plain array-like or Buffer JSON objects.
|
||||
Encoding arguments must be supported literals.
|
||||
`Buffer.from(arrayBuffer, byteOffset, length)` is a view of the ArrayBuffer or SharedArrayBuffer, with Node's offset and length checks.
|
||||
Custom input `valueOf` hooks and opaque class or native-handle inputs throw an unsupported-operation error.
|
||||
The deprecated `Buffer()` and `new Buffer()` take up to three arguments, without spreads, and follow Node: the first call made outside `node_modules` reports DEP0005 on the next tick, a number allocates zero-filled bytes, and other arguments convert as `Buffer.from` does.
|
||||
In JavaScript, `Buffer` read as a value (the global or the buffer module's `Buffer`) is one function with Node's statics and `poolSize`.
|
||||
Calling or constructing it through a stored value, reading its `prototype`, and its `copyBytesFrom` and `of` statics throw an unsupported-operation error.
|
||||
`Buffer.prototype` members read as values are functions that run on Buffer receivers; inherited and symbol-keyed members throw an unsupported-operation error.
|
||||
|
||||
@@ -1,2 +1,12 @@
|
||||
- **AsyncLocalStorage:** timer, interval, `setImmediate`, `process.nextTick` and `queueMicrotask` callbacks run with the stores that were active when they were scheduled, as in Node.
|
||||
Completion callbacks of callback-style I/O APIs, such as `fs.readFile` and `crypto.randomBytes`, run with no store, where Node runs them with the stores that were active when the operation started.
|
||||
The completion callbacks of `fs`, `crypto` and `zlib` callback APIs, `dns.lookup`, `child_process.execFile`, `server.listen`, `net.connect`, `http.request`, `http.get` and `dgram` run with the stores that were active at the call, as in Node.
|
||||
Events of `net`, `tls`, `http` and `https` sockets, servers, requests and responses run with the stores Node runs them with.
|
||||
A socket's or client request's events see the context of the call that created it, and a server's events and its requests' and responses' events see the context of its `listen()` call.
|
||||
Socket `write` and `end` callbacks, `'finish'` and `server.close(callback)` see the context of their call.
|
||||
Stream events and callbacks run with the stores of the code that triggers them, as in Node.
|
||||
Stream steps that Node defers with `process.nextTick` see the context that scheduled them, and `stream.finished` and `stream.pipeline` callbacks see the context of their call.
|
||||
`fs` read and write streams and `zlib` streams run their events and callbacks in the context that created them.
|
||||
Events of `child_process` children, their standard streams and their IPC channels see the context of the `spawn()` or `fork()` call; stdin's `'finish'` and `send()` callbacks see the context of their call.
|
||||
A client `http2` session's events see the context of its `http2.connect()` call, and a client stream's events the context of its `request()` call.
|
||||
An `http2` server's `'session'` and `'stream'` events and the compatibility API's request handler see the context of its `listen()` call; a server stream's own events, and its compatibility request's and response's, run with no store, as in Node.
|
||||
`AsyncResource` has no lowering yet.
|
||||
|
||||
@@ -27,20 +27,22 @@
|
||||
A call on a DiffieHellman value whose arguments no @types/node overload accepts, or whose output encoding is typed `any`, is refused at compile time, as are its `Object.prototype` methods; other property reads, and property writes, throw a coded SC2020 error.
|
||||
`Object.keys()` and `in` do not see a DiffieHellman's own `verifyError` property, and `JSON.stringify()` and `instanceof` are refused.
|
||||
After `createDiffieHellman(primeLength)` throws `Invalid DH parameters`, the next OpenSSL error Node throws can list the failed generation's errors in `opensslErrorStack`; errors in a compiled binary list only their own.
|
||||
`crypto.diffieHellman`, which takes DH, EC, or X25519 KeyObjects, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available.
|
||||
`crypto.diffieHellman` computes ECDH and X25519 secrets from EC and X25519 keys, given as KeyObjects or in any form `createPrivateKey` and `createPublicKey` read, with Node's errors; DH keys have no KeyObjects here, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available.
|
||||
`checkPrime`, `checkPrimeSync`, `generatePrime`, and `generatePrimeSync` support bigint, ArrayBuffer, typed-array, Buffer, and DataView values and the `checks`, `safe`, `add`, `rem`, and `bigint` options on native targets, with Node's errors; the callback forms compute before they return and call back in a later macrotask.
|
||||
Candidates and sizes longer than 262144 bits throw a coded SC2020 error, and wasm32-wasi builds are refused.
|
||||
Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`.
|
||||
RSA, EC, and Ed25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets.
|
||||
RSA, 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, the `raw-public`, `raw-private`, and `raw-seed` encodings, and `paramEncoding: 'explicit'` throw a coded SC2020 error.
|
||||
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.
|
||||
The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1.
|
||||
Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate.
|
||||
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.
|
||||
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.
|
||||
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`, `x25519`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused.
|
||||
Encrypted private keys (`passphrase`, `cipher`), the other key types (`rsa-pss`, `dsa`, `ed448`, `x448`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused.
|
||||
Comparing keys of different types with `equals()` answers false; Node also leaves an OpenSSL error queued, which a later key operation can report.
|
||||
`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,5 +1,6 @@
|
||||
- **Date:** Date unions, spread constructor arguments, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale formatters are unsupported.
|
||||
Zero-argument, single number/string, and local-time year/month field constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile.
|
||||
JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization.
|
||||
A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`.
|
||||
- **Date:** Spread constructor arguments, setters, thrown Date values, and locale formatters are unsupported.
|
||||
Dates are objects: `===` compares identity, `new Date(date)` makes a new Date, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in `Date | undefined` and `Date | null` slots, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators.
|
||||
Unions of a Date with other non-nullish types are unsupported.
|
||||
Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, relational comparison, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile.
|
||||
A Date converted to `unknown` is the same Date object.
|
||||
See [Dates and timezones](#dates-and-timezones) for timezone data.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
- **Filesystem URL paths:** Synchronous `fs` functions accept `file:` URL paths and convert them as `fileURLToPath` does.
|
||||
`existsSync` and the `fs.promises` functions refuse URL paths; pass `fileURLToPath(url)` instead.
|
||||
A URL path after a string path, or with options that are not literals, is refused, because Node checks those arguments before it converts the URL.
|
||||
- **Filesystem URL paths:** `fs` and `fs.promises` functions accept `file:` URL paths and convert them as `fileURLToPath` does; `existsSync` answers `false` and the `fs.promises` functions reject for a URL that does not convert.
|
||||
A URL path with options that are not literals is refused where Node checks those options before it converts the URL, except a `readFileSync` encoding.
|
||||
`fs.promises` functions also refuse a string path before a URL path; pass `fileURLToPath(url)` instead.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
- **Text encoding:** `TextEncoder` and `TextDecoder` can be stored in fields, arguments, return values, arrays, and closures.
|
||||
Encoding supports UTF-8, including `encodeInto` into a Uint8Array or Buffer view.
|
||||
Codec properties expose the canonical encoding and decoder flags.
|
||||
Codecs held in `any` or `unknown` storage keep their identity and streaming state, and dynamic code can call `encode`, `encodeInto` and `decode`, with options objects, and read their properties.
|
||||
Decoding accepts recognized WHATWG labels, including runtime labels and omitted or undefined encodings.
|
||||
Literal constructor options support `fatal` and `ignoreBOM`; literal decode options support streaming UTF-8, UTF-16, and single-byte encodings.
|
||||
Other legacy streaming encodings, runtime options objects, inspection, JSON conversion, and module-scope declarations using `var` or lacking an initializer are unsupported.
|
||||
|
||||
@@ -11476,9 +11476,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1387,7 +1387,20 @@
|
||||
"evidence": "derived:descendants",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
"tests/corpus/5815-als-callback-context.ts",
|
||||
"tests/corpus/7110-als-completion-callbacks.ts",
|
||||
"tests/corpus/7111-als-bind-snapshot-options.ts",
|
||||
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
|
||||
"tests/corpus/7119-als-net-event-context.mjs",
|
||||
"tests/corpus/7380-als-http-event-context.mjs",
|
||||
"tests/corpus/7383-als-stream-events.ts",
|
||||
"tests/corpus/7384-als-stream-events.mjs",
|
||||
"tests/corpus/7385-als-child-process-events.ts",
|
||||
"tests/corpus/7386-als-child-fork-events/main.mjs",
|
||||
"tests/corpus/7387-als-http2-events.mjs",
|
||||
"tests/corpus/7388-als-http2-compat-events.mjs",
|
||||
"tests/corpus/7389-als-http2-secure-events.mjs",
|
||||
"tests/corpus/7391-als-response-end-and-finished.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
@@ -1437,7 +1450,20 @@
|
||||
"evidence": "compiler-dedicated:AsyncLocalStorage",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
"tests/corpus/5815-als-callback-context.ts",
|
||||
"tests/corpus/7110-als-completion-callbacks.ts",
|
||||
"tests/corpus/7111-als-bind-snapshot-options.ts",
|
||||
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
|
||||
"tests/corpus/7119-als-net-event-context.mjs",
|
||||
"tests/corpus/7380-als-http-event-context.mjs",
|
||||
"tests/corpus/7383-als-stream-events.ts",
|
||||
"tests/corpus/7384-als-stream-events.mjs",
|
||||
"tests/corpus/7385-als-child-process-events.ts",
|
||||
"tests/corpus/7386-als-child-fork-events/main.mjs",
|
||||
"tests/corpus/7387-als-http2-events.mjs",
|
||||
"tests/corpus/7388-als-http2-compat-events.mjs",
|
||||
"tests/corpus/7389-als-http2-secure-events.mjs",
|
||||
"tests/corpus/7391-als-response-end-and-finished.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -1510,7 +1536,20 @@
|
||||
"evidence": "compiler-dedicated:asyncLocalStorage.disable",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
"tests/corpus/5815-als-callback-context.ts",
|
||||
"tests/corpus/7110-als-completion-callbacks.ts",
|
||||
"tests/corpus/7111-als-bind-snapshot-options.ts",
|
||||
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
|
||||
"tests/corpus/7119-als-net-event-context.mjs",
|
||||
"tests/corpus/7380-als-http-event-context.mjs",
|
||||
"tests/corpus/7383-als-stream-events.ts",
|
||||
"tests/corpus/7384-als-stream-events.mjs",
|
||||
"tests/corpus/7385-als-child-process-events.ts",
|
||||
"tests/corpus/7386-als-child-fork-events/main.mjs",
|
||||
"tests/corpus/7387-als-http2-events.mjs",
|
||||
"tests/corpus/7388-als-http2-compat-events.mjs",
|
||||
"tests/corpus/7389-als-http2-secure-events.mjs",
|
||||
"tests/corpus/7391-als-response-end-and-finished.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -1537,7 +1576,20 @@
|
||||
"evidence": "compiler-dedicated:asyncLocalStorage.getStore",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
"tests/corpus/5815-als-callback-context.ts",
|
||||
"tests/corpus/7110-als-completion-callbacks.ts",
|
||||
"tests/corpus/7111-als-bind-snapshot-options.ts",
|
||||
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
|
||||
"tests/corpus/7119-als-net-event-context.mjs",
|
||||
"tests/corpus/7380-als-http-event-context.mjs",
|
||||
"tests/corpus/7383-als-stream-events.ts",
|
||||
"tests/corpus/7384-als-stream-events.mjs",
|
||||
"tests/corpus/7385-als-child-process-events.ts",
|
||||
"tests/corpus/7386-als-child-fork-events/main.mjs",
|
||||
"tests/corpus/7387-als-http2-events.mjs",
|
||||
"tests/corpus/7388-als-http2-compat-events.mjs",
|
||||
"tests/corpus/7389-als-http2-secure-events.mjs",
|
||||
"tests/corpus/7391-als-response-end-and-finished.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -1564,7 +1616,20 @@
|
||||
"evidence": "compiler-dedicated:asyncLocalStorage.enterWith",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
"tests/corpus/5815-als-callback-context.ts",
|
||||
"tests/corpus/7110-als-completion-callbacks.ts",
|
||||
"tests/corpus/7111-als-bind-snapshot-options.ts",
|
||||
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
|
||||
"tests/corpus/7119-als-net-event-context.mjs",
|
||||
"tests/corpus/7380-als-http-event-context.mjs",
|
||||
"tests/corpus/7383-als-stream-events.ts",
|
||||
"tests/corpus/7384-als-stream-events.mjs",
|
||||
"tests/corpus/7385-als-child-process-events.ts",
|
||||
"tests/corpus/7386-als-child-fork-events/main.mjs",
|
||||
"tests/corpus/7387-als-http2-events.mjs",
|
||||
"tests/corpus/7388-als-http2-compat-events.mjs",
|
||||
"tests/corpus/7389-als-http2-secure-events.mjs",
|
||||
"tests/corpus/7391-als-response-end-and-finished.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -1591,7 +1656,20 @@
|
||||
"evidence": "compiler-dedicated:asyncLocalStorage.run",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
"tests/corpus/5815-als-callback-context.ts",
|
||||
"tests/corpus/7110-als-completion-callbacks.ts",
|
||||
"tests/corpus/7111-als-bind-snapshot-options.ts",
|
||||
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
|
||||
"tests/corpus/7119-als-net-event-context.mjs",
|
||||
"tests/corpus/7380-als-http-event-context.mjs",
|
||||
"tests/corpus/7383-als-stream-events.ts",
|
||||
"tests/corpus/7384-als-stream-events.mjs",
|
||||
"tests/corpus/7385-als-child-process-events.ts",
|
||||
"tests/corpus/7386-als-child-fork-events/main.mjs",
|
||||
"tests/corpus/7387-als-http2-events.mjs",
|
||||
"tests/corpus/7388-als-http2-compat-events.mjs",
|
||||
"tests/corpus/7389-als-http2-secure-events.mjs",
|
||||
"tests/corpus/7391-als-response-end-and-finished.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -1618,7 +1696,20 @@
|
||||
"evidence": "compiler-dedicated:asyncLocalStorage.exit",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
"tests/corpus/5815-als-callback-context.ts",
|
||||
"tests/corpus/7110-als-completion-callbacks.ts",
|
||||
"tests/corpus/7111-als-bind-snapshot-options.ts",
|
||||
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
|
||||
"tests/corpus/7119-als-net-event-context.mjs",
|
||||
"tests/corpus/7380-als-http-event-context.mjs",
|
||||
"tests/corpus/7383-als-stream-events.ts",
|
||||
"tests/corpus/7384-als-stream-events.mjs",
|
||||
"tests/corpus/7385-als-child-process-events.ts",
|
||||
"tests/corpus/7386-als-child-fork-events/main.mjs",
|
||||
"tests/corpus/7387-als-http2-events.mjs",
|
||||
"tests/corpus/7388-als-http2-compat-events.mjs",
|
||||
"tests/corpus/7389-als-http2-secure-events.mjs",
|
||||
"tests/corpus/7391-als-response-end-and-finished.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -1922,6 +2013,19 @@
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts",
|
||||
"tests/corpus/7110-als-completion-callbacks.ts",
|
||||
"tests/corpus/7111-als-bind-snapshot-options.ts",
|
||||
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
|
||||
"tests/corpus/7119-als-net-event-context.mjs",
|
||||
"tests/corpus/7380-als-http-event-context.mjs",
|
||||
"tests/corpus/7383-als-stream-events.ts",
|
||||
"tests/corpus/7384-als-stream-events.mjs",
|
||||
"tests/corpus/7385-als-child-process-events.ts",
|
||||
"tests/corpus/7386-als-child-fork-events/main.mjs",
|
||||
"tests/corpus/7387-als-http2-events.mjs",
|
||||
"tests/corpus/7388-als-http2-compat-events.mjs",
|
||||
"tests/corpus/7389-als-http2-secure-events.mjs",
|
||||
"tests/corpus/7391-als-response-end-and-finished.mjs",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
},
|
||||
@@ -2484,7 +2588,20 @@
|
||||
"evidence": "compiler-dedicated:AsyncLocalStorage",
|
||||
"tests": [
|
||||
"tests/corpus/2213-async-local-storage.cjs",
|
||||
"tests/corpus/5815-als-callback-context.ts"
|
||||
"tests/corpus/5815-als-callback-context.ts",
|
||||
"tests/corpus/7110-als-completion-callbacks.ts",
|
||||
"tests/corpus/7111-als-bind-snapshot-options.ts",
|
||||
"tests/corpus/7112-als-bind-snapshot-dynamic.mjs",
|
||||
"tests/corpus/7119-als-net-event-context.mjs",
|
||||
"tests/corpus/7380-als-http-event-context.mjs",
|
||||
"tests/corpus/7383-als-stream-events.ts",
|
||||
"tests/corpus/7384-als-stream-events.mjs",
|
||||
"tests/corpus/7385-als-child-process-events.ts",
|
||||
"tests/corpus/7386-als-child-fork-events/main.mjs",
|
||||
"tests/corpus/7387-als-http2-events.mjs",
|
||||
"tests/corpus/7388-als-http2-compat-events.mjs",
|
||||
"tests/corpus/7389-als-http2-secure-events.mjs",
|
||||
"tests/corpus/7391-als-response-end-and-finished.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -20228,8 +20345,19 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:node-builtin.crypto.diffieHellman"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.crypto.diffieHellman",
|
||||
"tests": [
|
||||
"tests/corpus/1358-crypto-random.ts",
|
||||
"tests/corpus/1534-crypto-hash-chain.ts",
|
||||
"tests/corpus/2556-crypto-introspection.ts",
|
||||
"tests/corpus/2890-crypto-static-utility.ts",
|
||||
"tests/harness/npm-static.test.ts",
|
||||
"tests/corpus/node24-crypto-sha2.ts",
|
||||
"tests/corpus/node24-crypto-kdf.ts",
|
||||
"tests/corpus/node24-crypto-kdf-errors.ts",
|
||||
"tests/corpus/node24-crypto-kdf-options.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -52195,6 +52323,7 @@
|
||||
"tests/corpus/6295-jose-hs256/main.mjs",
|
||||
"tests/corpus/6331-jose-root-entry/main.ts",
|
||||
"tests/corpus/7743-transform-stream-identity.ts",
|
||||
"tests/corpus/7747-text-codec-untyped-slot.js",
|
||||
"tests/corpus/text-codec-values/main.ts",
|
||||
"tests/corpus/webassembly-api.mjs",
|
||||
"tests/corpus/webassembly-dylink/main.mjs",
|
||||
@@ -53347,7 +53476,8 @@
|
||||
"tests": [
|
||||
"tests/corpus/1423-text-codec.ts",
|
||||
"tests/corpus/text-codec-values/main.ts",
|
||||
"tests/corpus/3133-text-decoder-streaming.ts"
|
||||
"tests/corpus/3133-text-decoder-streaming.ts",
|
||||
"tests/corpus/7747-text-codec-untyped-slot.js"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -53398,7 +53528,8 @@
|
||||
"tests": [
|
||||
"tests/corpus/1423-text-codec.ts",
|
||||
"tests/corpus/text-codec-values/main.ts",
|
||||
"tests/corpus/3133-text-decoder-streaming.ts"
|
||||
"tests/corpus/3133-text-decoder-streaming.ts",
|
||||
"tests/corpus/7747-text-codec-untyped-slot.js"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -101281,6 +101412,7 @@
|
||||
"tests/corpus/5835-util-format-integer-specifiers.ts",
|
||||
"tests/corpus/5884-inspect-layout-options.ts",
|
||||
"tests/corpus/6428-promisify-callback-functions.ts",
|
||||
"tests/corpus/7747-text-codec-untyped-slot.js",
|
||||
"tests/corpus/node24-deep-equality-properties.js",
|
||||
"tests/corpus/node24-deep-equality.cjs",
|
||||
"tests/corpus/node24-deep-equality.ts",
|
||||
@@ -101809,7 +101941,8 @@
|
||||
"tests/corpus/text-codec-values/main.ts",
|
||||
"tests/fixtures/node-types/text-codecs.mts",
|
||||
"tests/harness/project-config.test.ts",
|
||||
"tests/corpus/3133-text-decoder-streaming.ts"
|
||||
"tests/corpus/3133-text-decoder-streaming.ts",
|
||||
"tests/corpus/7747-text-codec-untyped-slot.js"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -101930,7 +102063,8 @@
|
||||
"tests/corpus/text-codec-values/main.ts",
|
||||
"tests/fixtures/node-types/text-codecs.mts",
|
||||
"tests/harness/project-config.test.ts",
|
||||
"tests/corpus/3133-text-decoder-streaming.ts"
|
||||
"tests/corpus/3133-text-decoder-streaming.ts",
|
||||
"tests/corpus/7747-text-codec-untyped-slot.js"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -102040,7 +102174,8 @@
|
||||
"tests/corpus/text-codec-values/main.ts",
|
||||
"tests/fixtures/node-types/text-codecs.mts",
|
||||
"tests/harness/project-config.test.ts",
|
||||
"tests/corpus/3133-text-decoder-streaming.ts"
|
||||
"tests/corpus/3133-text-decoder-streaming.ts",
|
||||
"tests/corpus/7747-text-codec-untyped-slot.js"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -102069,7 +102204,8 @@
|
||||
"tests/corpus/text-codec-values/main.ts",
|
||||
"tests/fixtures/node-types/text-codecs.mts",
|
||||
"tests/harness/project-config.test.ts",
|
||||
"tests/corpus/3133-text-decoder-streaming.ts"
|
||||
"tests/corpus/3133-text-decoder-streaming.ts",
|
||||
"tests/corpus/7747-text-codec-untyped-slot.js"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
|
||||
@@ -332,11 +332,11 @@
|
||||
{ "chapter": "buffer", "signatures": ["`buf.readBigInt64BE([offset])`", "`buf.readBigInt64LE([offset])`", "`buf.readBigUInt64BE([offset])`", "`buf.readBigUInt64LE([offset])`", "`buf.writeBigInt64BE(value[, offset])`", "`buf.writeBigInt64LE(value[, offset])`", "`buf.writeBigUInt64BE(value[, offset])`", "`buf.writeBigUInt64LE(value[, offset])`"], "status": "supported", "evidence": ["tests/corpus/2902-bigint-bytes.ts", "tests/corpus/2904-bigint-errors.ts"] },
|
||||
{ "chapter": "environment_variables", "symbols": ["export"], "status": "not-applicable" },
|
||||
{ "chapter": "globals", "symbols": ["fetch"], "status": "partial", "evidence": ["tests/harness/fetch-conformance.test.ts"] },
|
||||
{ "chapter": "globals", "signatures": ["Class: `TextEncoder`", "Class: `TextDecoder`"], "status": "partial", "evidence": ["tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts", "tests/corpus/3133-text-decoder-streaming.ts"] },
|
||||
{ "chapter": "globals", "signatures": ["Class: `TextEncoder`", "Class: `TextDecoder`"], "status": "partial", "evidence": ["tests/corpus/1423-text-codec.ts", "tests/corpus/text-codec-values/main.ts", "tests/corpus/3133-text-decoder-streaming.ts", "tests/corpus/7747-text-codec-untyped-slot.js"] },
|
||||
{ "chapter": "globals", "signatures": ["Class: `Blob`", "Class: `File`"], "status": "partial", "evidence": ["tests/corpus/5285-blob-construct.ts", "tests/corpus/5286-blob-readers.ts", "tests/corpus/5287-blob-inspect.ts", "tests/corpus/5288-blob-validation.js", "tests/corpus/5290-blob-dynamic.js"] },
|
||||
{ "chapter": "buffer", "signatures": ["Class: `Blob`", "Class: `File`"], "status": "partial", "evidence": ["tests/corpus/5285-blob-construct.ts", "tests/corpus/5288-blob-validation.js", "tests/fixtures/node-types/blob-file.ts"] },
|
||||
{ "chapter": "buffer", "signatures": ["`blob.arrayBuffer()`", "`blob.bytes()`", "`blob.text()`", "`blob.slice([start[, end[, type]]])`", "`blob.size`", "`type` Type: {string}", "`name` Type: {string}", "`lastModified` Type: {number}"], "status": "supported", "evidence": ["tests/corpus/5285-blob-construct.ts", "tests/corpus/5286-blob-readers.ts", "tests/corpus/5288-blob-validation.js", "tests/corpus/5289-blob-read-order.ts", "tests/corpus/5290-blob-dynamic.js", "tests/fixtures/node-types/blob-file.ts"] },
|
||||
{ "chapter": "util", "signatures": ["Class: `util.TextEncoder`", "Class: `util.TextDecoder`", "`textEncoder.encode([input])`", "`textDecoder.decode([input[, options]])`"], "status": "partial", "evidence": ["tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", "tests/corpus/3133-text-decoder-streaming.ts"] },
|
||||
{ "chapter": "util", "signatures": ["Class: `util.TextEncoder`", "Class: `util.TextDecoder`", "`textEncoder.encode([input])`", "`textDecoder.decode([input[, options]])`"], "status": "partial", "evidence": ["tests/corpus/text-codec-values/main.ts", "tests/fixtures/node-types/text-codecs.mts", "tests/harness/project-config.test.ts", "tests/corpus/3133-text-decoder-streaming.ts", "tests/corpus/7747-text-codec-untyped-slot.js"] },
|
||||
{ "chapter": "modules", "symbols": ["__dirname", "__filename", "exports", "require", "module.require", "main", "filename"], "status": "partial", "evidence": ["tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs", "tests/corpus/2830-cjs-require-self-name/main.cjs", "tests/corpus/2880-cjs-module-graph/main.cjs", "tests/corpus/2923-cjs-factory-export/main.cjs"] },
|
||||
{ "chapter": "modules", "symbols": ["require.resolve", "require.resolve.paths"], "status": "partial", "evidence": ["tests/corpus/2858-import-meta-resolve/main.mjs", "tests/corpus/2860-require-resolve/main.cjs"] },
|
||||
{ "chapter": "modules", "symbols": ["cache", "children", "id", "isPreloading", "loaded", "parent", "path", "paths", "Module"], "status": "partial", "evidence": ["tests/corpus/2880-cjs-module-graph/main.cjs"] },
|
||||
@@ -441,7 +441,7 @@
|
||||
{ "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"] },
|
||||
{ "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] },
|
||||
{ "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts"] },
|
||||
{ "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", "tests/corpus/7384-als-stream-events.mjs", "tests/corpus/7385-als-child-process-events.ts", "tests/corpus/7386-als-child-fork-events/main.mjs", "tests/corpus/7387-als-http2-events.mjs", "tests/corpus/7388-als-http2-compat-events.mjs", "tests/corpus/7389-als-http2-secure-events.mjs", "tests/corpus/7391-als-response-end-and-finished.mjs"] },
|
||||
{ "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] },
|
||||
{ "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] },
|
||||
{ "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] },
|
||||
|
||||
+73
-29
@@ -917,6 +917,14 @@ interface Buffer<
|
||||
readBigUint64LE(offset?: number): bigint;
|
||||
}
|
||||
interface BufferConstructor {
|
||||
/** @deprecated since v10.0.0 - Use `Buffer.from(string[, encoding])` instead. */
|
||||
new (str: string, encoding?: BufferEncoding): Buffer;
|
||||
/** @deprecated since v10.0.0 - Use `Buffer.alloc()` instead. */
|
||||
new (size: number): Buffer;
|
||||
/** @deprecated since v10.0.0 - Use `Buffer.from(array)` instead. */
|
||||
new (array: ArrayLike<number>): Buffer;
|
||||
/** @deprecated since v10.0.0 - Use `Buffer.from(arrayBuffer[, byteOffset[, length]])` instead. */
|
||||
new (arrayBuffer: ArrayBuffer | SharedArrayBuffer): Buffer;
|
||||
from(data: string, encoding?: BufferEncoding): Buffer;
|
||||
/* The ArrayBuffer form is the VIEW construction: x.buffer with an
|
||||
* optional byte offset/length shares x's storage (the subarray rule). */
|
||||
@@ -1582,7 +1590,7 @@ declare module "node:fs" {
|
||||
export function writeFileSync(path: string | URL, data: string, encoding: "utf8" | "utf-8"): void;
|
||||
export function writeFileSync(path: string | URL, data: string | Uint8Array): void;
|
||||
export function appendFileSync(path: string | URL, data: string | Uint8Array): void;
|
||||
export function existsSync(path: string): boolean;
|
||||
export function existsSync(path: string | URL): boolean;
|
||||
export function mkdirSync(path: string | URL): void;
|
||||
export function mkdirSync(
|
||||
path: string | URL,
|
||||
@@ -2223,7 +2231,7 @@ declare module "fs/promises" {
|
||||
stat(): Promise<import("node:fs").Stats>;
|
||||
[Symbol.asyncDispose](): Promise<void>;
|
||||
}
|
||||
export function open(path: string, flags?: string, mode?: number): Promise<FileHandle>;
|
||||
export function open(path: string | URL, flags?: string, mode?: number): Promise<FileHandle>;
|
||||
export function link(
|
||||
existingPath: string | Uint8Array | URL,
|
||||
newPath: string | Uint8Array | URL,
|
||||
@@ -2260,14 +2268,14 @@ declare module "fs/promises" {
|
||||
atime: number | string | Date,
|
||||
mtime: number | string | Date,
|
||||
): Promise<void>;
|
||||
export function readFile(path: string, encoding: "utf8" | "utf-8"): Promise<string>;
|
||||
export function readFile(path: string | URL, encoding: "utf8" | "utf-8"): Promise<string>;
|
||||
export function readFile(
|
||||
path: string,
|
||||
path: string | URL,
|
||||
options: BufferEncoding | { encoding: BufferEncoding },
|
||||
): Promise<string>;
|
||||
export function readFile(path: string): Promise<Buffer>;
|
||||
export function readFile(path: string | URL): Promise<Buffer>;
|
||||
export function writeFile(
|
||||
path: string,
|
||||
path: string | URL,
|
||||
data: string,
|
||||
options?: {
|
||||
mode?: number;
|
||||
@@ -2276,14 +2284,18 @@ declare module "fs/promises" {
|
||||
[option: string]: unknown;
|
||||
},
|
||||
): Promise<void>;
|
||||
export function writeFile(path: string, data: string, encoding: "utf8" | "utf-8"): Promise<void>;
|
||||
export function writeFile(
|
||||
path: string | URL,
|
||||
data: string,
|
||||
encoding: "utf8" | "utf-8",
|
||||
): Promise<void>;
|
||||
export function mkdir(
|
||||
path: string,
|
||||
path: string | URL,
|
||||
options?: { recursive?: boolean; mode?: number },
|
||||
): Promise<void>;
|
||||
export function readdir(path: string): Promise<string[]>;
|
||||
export function readdir(path: string | URL): Promise<string[]>;
|
||||
export function readdir(
|
||||
path: string,
|
||||
path: string | URL,
|
||||
options: {
|
||||
encoding?: "utf8" | "utf-8";
|
||||
withFileTypes: true;
|
||||
@@ -2300,12 +2312,12 @@ declare module "fs/promises" {
|
||||
pattern: string | readonly string[],
|
||||
options: import("node:fs").GlobOptionsWithFileTypes,
|
||||
): AsyncIterableIterator<import("node:fs").Dirent>;
|
||||
export function rm(path: string): Promise<void>;
|
||||
export function rm(path: string | URL): Promise<void>;
|
||||
export function rm(
|
||||
path: string,
|
||||
path: string | URL,
|
||||
options: { recursive?: boolean; force?: boolean; maxRetries?: number; retryDelay?: number },
|
||||
): Promise<void>;
|
||||
export function stat(path: string): Promise<import("node:fs").Stats>;
|
||||
export function stat(path: string | URL): Promise<import("node:fs").Stats>;
|
||||
export function statfs(
|
||||
path: string | Uint8Array | URL,
|
||||
options?: import("node:fs").StatFsOptions & { bigint?: false },
|
||||
@@ -2318,21 +2330,21 @@ declare module "fs/promises" {
|
||||
path: string | Uint8Array | URL,
|
||||
options?: import("node:fs").StatFsOptions,
|
||||
): Promise<import("node:fs").StatsFs | import("node:fs").BigIntStatsFs>;
|
||||
export function realpath(path: string): Promise<string>;
|
||||
export function lstat(path: string): Promise<import("node:fs").Stats>;
|
||||
export function unlink(path: string): Promise<void>;
|
||||
export function chmod(path: string, mode: number): Promise<void>;
|
||||
export function chown(path: string, uid: number, gid: number): Promise<void>;
|
||||
export function lchown(path: string, uid: number, gid: number): Promise<void>;
|
||||
export function access(path: string, mode?: number): Promise<void>;
|
||||
export function truncate(path: string, len?: number): Promise<void>;
|
||||
export function appendFile(path: string, data: string | Uint8Array): Promise<void>;
|
||||
export function copyFile(src: string, dest: string): Promise<void>;
|
||||
export function mkdtemp(prefix: string): Promise<string>;
|
||||
export function rmdir(path: string): Promise<void>;
|
||||
export function rename(oldPath: string, newPath: string): Promise<void>;
|
||||
export function realpath(path: string | URL): Promise<string>;
|
||||
export function lstat(path: string | URL): Promise<import("node:fs").Stats>;
|
||||
export function unlink(path: string | URL): Promise<void>;
|
||||
export function chmod(path: string | URL, mode: number): Promise<void>;
|
||||
export function chown(path: string | URL, uid: number, gid: number): Promise<void>;
|
||||
export function lchown(path: string | URL, uid: number, gid: number): Promise<void>;
|
||||
export function access(path: string | URL, mode?: number): Promise<void>;
|
||||
export function truncate(path: string | URL, len?: number): Promise<void>;
|
||||
export function appendFile(path: string | URL, data: string | Uint8Array): Promise<void>;
|
||||
export function copyFile(src: string | URL, dest: string | URL): Promise<void>;
|
||||
export function mkdtemp(prefix: string | URL): Promise<string>;
|
||||
export function rmdir(path: string | URL): Promise<void>;
|
||||
export function rename(oldPath: string | URL, newPath: string | URL): Promise<void>;
|
||||
export function opendir(
|
||||
path: string,
|
||||
path: string | URL,
|
||||
options?: import("node:fs").OpenDirOptions,
|
||||
): Promise<import("node:fs").Dir>;
|
||||
}
|
||||
@@ -2992,6 +3004,24 @@ declare module "crypto" {
|
||||
options: ED25519KeyPairOptions<"der", "der">,
|
||||
): KeyPairSyncResult<Buffer, Buffer>;
|
||||
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 };
|
||||
}
|
||||
export function generateKeyPairSync(
|
||||
type: "x25519",
|
||||
options: X25519KeyPairOptions<"pem", "pem">,
|
||||
): KeyPairSyncResult<string, string>;
|
||||
export function generateKeyPairSync(
|
||||
type: "x25519",
|
||||
options: X25519KeyPairOptions<"der", "der">,
|
||||
): KeyPairSyncResult<Buffer, Buffer>;
|
||||
export function generateKeyPairSync(type: "x25519", options?: {}): KeyPairKeyObjectResult;
|
||||
export function generateKeyPair(
|
||||
type: "x25519",
|
||||
options: {} | undefined,
|
||||
callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void,
|
||||
): void;
|
||||
export function generateKeyPair(
|
||||
type: "ed25519",
|
||||
options: {} | undefined,
|
||||
@@ -3020,6 +3050,11 @@ declare module "crypto" {
|
||||
export interface VerifyKeyObjectInput extends SigningOptions {
|
||||
key: KeyObject;
|
||||
}
|
||||
export function diffieHellman(options: { privateKey: KeyObject; publicKey: KeyObject }): Buffer;
|
||||
export function diffieHellman(
|
||||
options: { privateKey: KeyObject; publicKey: KeyObject },
|
||||
callback: (err: Error | null, secret: Buffer) => void,
|
||||
): void;
|
||||
export function sign(
|
||||
algorithm: string | null | undefined,
|
||||
data: ArrayBufferView,
|
||||
@@ -7235,9 +7270,18 @@ declare module "node:tty" {
|
||||
declare module "async_hooks" {
|
||||
/* AsyncLocalStorage: fiber-carried context snapshots — run/getStore/
|
||||
* exit/enterWith/disable lower onto the runtime's active-slot machinery
|
||||
* (als.* libCalls); values cross as dyn values. */
|
||||
* (als.* libCalls); values cross as dyn values. bind and snapshot bind
|
||||
* functions to the active context; the options carry the store's
|
||||
* default value and name. */
|
||||
export interface AsyncLocalStorageOptions {
|
||||
defaultValue?: any;
|
||||
name?: string | undefined;
|
||||
}
|
||||
export class AsyncLocalStorage<T = any> {
|
||||
constructor();
|
||||
constructor(options?: AsyncLocalStorageOptions);
|
||||
static bind<Func extends (...args: any[]) => any>(fn: Func): Func;
|
||||
static snapshot(): <R, TArgs extends any[]>(fn: (...args: TArgs) => R, ...args: TArgs) => R;
|
||||
readonly name: string;
|
||||
run<R>(store: T, callback: (...args: any[]) => R, ...args: any[]): R;
|
||||
exit<R>(callback: (...args: any[]) => R, ...args: any[]): R;
|
||||
getStore(): T | undefined;
|
||||
|
||||
@@ -50,6 +50,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
dgram: features.dgram,
|
||||
dnsPromises: features.dnsPromises,
|
||||
dgramDyn: features.dgramDyn,
|
||||
textCodecDyn: features.textCodecDyn,
|
||||
watch: features.fsWatch,
|
||||
foreignFfi: hasForeignFfiCallback(mod.ffiImports ?? []),
|
||||
nodeTest: features.nodeTest,
|
||||
|
||||
@@ -54,6 +54,7 @@ export async function compilePackedLibrary(
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -139,7 +139,6 @@ export class LlvmDebugInfo {
|
||||
let id: string;
|
||||
switch (type.kind) {
|
||||
case "f64":
|
||||
case "date":
|
||||
case "procStream":
|
||||
id = this.add(
|
||||
`!DIBasicType(name: ${quoted(type.kind === "f64" ? "number" : type.kind)}, size: 64, encoding: DW_ATE_float)`,
|
||||
@@ -190,7 +189,7 @@ export class LlvmDebugInfo {
|
||||
? []
|
||||
: [
|
||||
this.add(
|
||||
`!DIDerivedType(tag: DW_TAG_member, name: "arm${i}", baseType: ${this.type(arm)}, size: ${arm.kind === "bool" ? 8 : arm.kind === "f64" || arm.kind === "date" ? 64 : this.pointerBits})`,
|
||||
`!DIDerivedType(tag: DW_TAG_member, name: "arm${i}", baseType: ${this.type(arm)}, size: ${arm.kind === "bool" ? 8 : arm.kind === "f64" ? 64 : this.pointerBits})`,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -239,9 +238,7 @@ export class LlvmDebugInfo {
|
||||
// Scalar forward captures use an array-backed TDZ box. Expose that
|
||||
// exceptional representation opaquely rather than reporting a bad value.
|
||||
const opaqueBox =
|
||||
local.boxed &&
|
||||
local.tdz &&
|
||||
(local.type.kind === "f64" || local.type.kind === "bool" || local.type.kind === "date");
|
||||
local.boxed && local.tdz && (local.type.kind === "f64" || local.type.kind === "bool");
|
||||
const type = opaqueBox
|
||||
? this.pointer(
|
||||
this.add(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { expect, test } from "vitest";
|
||||
import { DYN_HANDLE_KINDS } from "../../ir/ir.js";
|
||||
import { DYN_HANDLE_TAG_NUM } from "./dyn.js";
|
||||
import { DYN_HANDLE_TAG_NUM, TEXT_CODEC_TAG_NUM } from "./dyn.js";
|
||||
|
||||
// SCR_DYNH_TAGS in scr_dyn.h lists the ScrDynHandleTag values by name, each
|
||||
// with an explicit value, so changes that add tags edit different lines. The
|
||||
@@ -39,3 +39,9 @@ test("the backend's handle tag numbers match scr_dyn.h", () => {
|
||||
expect(values.get(tag!), `${kind} (${tag})`).toBe(num);
|
||||
}
|
||||
});
|
||||
|
||||
test("the backend's codec handle tag numbers match scr_dyn.h", () => {
|
||||
const values = new Map(tags.map((tag) => [tag.name, tag.value]));
|
||||
expect(values.get("SCR_DYNH_TEXT_DECODER")).toBe(TEXT_CODEC_TAG_NUM.TextDecoder);
|
||||
expect(values.get("SCR_DYNH_TEXT_ENCODER")).toBe(TEXT_CODEC_TAG_NUM.TextEncoder);
|
||||
});
|
||||
|
||||
@@ -43,6 +43,8 @@ import {
|
||||
isDynTypedRefType,
|
||||
isRefCounted,
|
||||
isUnitType,
|
||||
recordTextCodecClass,
|
||||
textCodecRecordClass,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
import { dynDesc, streamTypedRefEligible } from "../../ir/analysis.js";
|
||||
@@ -94,6 +96,17 @@ export const DYN_HANDLE_TAG_NUM: Record<string, number> = {
|
||||
cryptoHmac: 53,
|
||||
};
|
||||
|
||||
/** The handle tags of TextEncoder and TextDecoder records boxed as dyn
|
||||
* (scr_dyn.h; scr_text_codec_dyn.c boxes and unboxes them). */
|
||||
export const TEXT_CODEC_TAG_NUM: Record<"TextEncoder" | "TextDecoder", number> = {
|
||||
TextDecoder: 76,
|
||||
TextEncoder: 77,
|
||||
};
|
||||
|
||||
/** The decoder record fields scr_text_decoder_to_dyn reads, in its
|
||||
* parameter order. */
|
||||
const TEXT_DECODER_FIELDS = ["%TextDecoder", "%fatal", "%ignoreBOM", "%state"] as const;
|
||||
|
||||
/** Handle kinds whose runtime unit boxes them through its own function,
|
||||
* which installs the tag's ops on first use (scr_crypto.h). Programs that
|
||||
* never box one of these values do not link its dynamic members. */
|
||||
@@ -456,7 +469,7 @@ export class LlDyn {
|
||||
|
||||
/** The value LLVM type of a dynCheck result / toDyn operand for `t`. */
|
||||
private valTy(t: IrType): string {
|
||||
return t.kind === "f64" || t.kind === "date" || t.kind === "procStream"
|
||||
return t.kind === "f64" || t.kind === "procStream"
|
||||
? "double"
|
||||
: t.kind === "bool"
|
||||
? "i1"
|
||||
@@ -927,6 +940,21 @@ export class LlDyn {
|
||||
const name = `sc_dm_${this.dynMatchers.size}`;
|
||||
this.dynMatchers.set(matcherKey, name);
|
||||
this.host.journalAllocated?.(name, "dyn.dynMatchHelper", [t, preserveRefs]);
|
||||
const codec = textCodecRecordClass(t, (id) => this.host.recordsById.get(id));
|
||||
if (codec !== null) {
|
||||
// A codec matches only its own handle box.
|
||||
const B = new BlockBuilder();
|
||||
const matched = this.handleTagMatches(B, TEXT_CODEC_TAG_NUM[codec]);
|
||||
B.terminate(`ret i1 ${matched}`);
|
||||
this.defs.push(
|
||||
`define internal zeroext i1 @${name}(ptr %d) ${FN_ATTRS} { ; matches ${key}`,
|
||||
B.render(),
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
const recursive = this.isRecursiveShape(t);
|
||||
const wrapped = recursive || this.holdsRecursiveShape(t);
|
||||
const B = new BlockBuilder();
|
||||
@@ -1535,6 +1563,19 @@ export class LlDyn {
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
/** `record.sourceSpread(built, value)`: the built record keeps value's
|
||||
* kept-source order over its own declared names. */
|
||||
emitRecordSourceSpread(B: BlockBuilder, built: string, value: string, shapeId: string): void {
|
||||
const host = this.host;
|
||||
const shape = host.recordsById.get(shapeId)!;
|
||||
this.keepsRecordSources = true;
|
||||
const names = this.recordNameTable(shapeId, shape);
|
||||
host.declare(`declare void @scr_record_source_spread(ptr, ptr, ptr, ptr, ${host.sizeType})`);
|
||||
B.line(
|
||||
`call void @scr_record_source_spread(ptr ${built}, ptr ${value}, ptr ${names.names}, ptr ${names.lens}, ${host.sizeType} ${shape.fields.length})`,
|
||||
);
|
||||
}
|
||||
|
||||
/** The declared-name tables scr_record_source_note scans: parallel
|
||||
* constant arrays of field-name bytes and byte lengths in declaration
|
||||
* order (the order the record serializes), interned per shape. Internal
|
||||
@@ -1724,6 +1765,24 @@ export class LlDyn {
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
const codec = textCodecRecordClass(t, (id) => host.recordsById.get(id));
|
||||
if (codec !== null) {
|
||||
// The record the codec's handle box holds (+1), or the path TypeError.
|
||||
host.declare(`declare ptr @scr_text_codec_from_dyn(ptr, i1 zeroext, ptr, ptr)`);
|
||||
const checked = B.tmp();
|
||||
B.line(
|
||||
`${checked} = call ptr @scr_text_codec_from_dyn(ptr %d, i1 ${codec === "TextDecoder"}, ptr %path, ptr ${want})`,
|
||||
);
|
||||
B.terminate(`ret ptr ${checked}`);
|
||||
this.defs.push(
|
||||
`define internal ptr @${name}(ptr %d, ptr %path) ${FN_ATTRS} {`,
|
||||
B.render(),
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
if (isRefCounted(t) && t.kind !== "dyn") {
|
||||
host.declare(`declare ptr @scr_dyn_typed_ref_unbox(ptr)`);
|
||||
if (t.kind === "object" && t.className === "%Error") {
|
||||
@@ -1976,10 +2035,10 @@ export class LlDyn {
|
||||
};
|
||||
switch (t.kind) {
|
||||
case "date": {
|
||||
host.declare(`declare double @scr_dyn_native_date_value(ptr)`);
|
||||
host.declare(`declare ptr @scr_dyn_native_date_check(ptr, ptr)`);
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call double @scr_dyn_native_date_value(ptr %d)`);
|
||||
B.terminate(`ret double ${r}`);
|
||||
B.line(`${r} = call ptr @scr_dyn_native_date_check(ptr %d, ptr %path)`);
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
break;
|
||||
}
|
||||
case "url":
|
||||
@@ -2752,6 +2811,77 @@ export class LlDyn {
|
||||
return name;
|
||||
}
|
||||
|
||||
/** `i1`: %d is a handle box with tag `tag`. */
|
||||
private handleTagMatches(B: BlockBuilder, tag: number): string {
|
||||
const kd = this.kindOf(B, "%d");
|
||||
const isHandle = B.tmp();
|
||||
B.line(`${isHandle} = icmp eq i32 ${kd}, ${DYN_KIND.HANDLE}`);
|
||||
const lHandle = B.newLabel("dm.handle");
|
||||
const lDone = B.newLabel("dm.handle.done");
|
||||
const entry = B.currentLabel();
|
||||
B.condBr(isHandle, lHandle, lDone);
|
||||
B.startBlock(lHandle);
|
||||
const tagPtr = B.tmp();
|
||||
const loaded = B.tmp();
|
||||
B.line(
|
||||
`${tagPtr} = getelementptr inbounds i8, ptr %d, i64 ${this.abiOffset(24, 20)} ; ->v.handle.tag`,
|
||||
);
|
||||
B.line(`${loaded} = load i32, ptr ${tagPtr}`);
|
||||
const same = B.tmp();
|
||||
B.line(`${same} = icmp eq i32 ${loaded}, ${tag}`);
|
||||
B.br(lDone);
|
||||
B.startBlock(lDone);
|
||||
const matched = B.tmp();
|
||||
B.line(`${matched} = phi i1 [ false, %${entry} ], [ ${same}, %${lHandle} ]`);
|
||||
return matched;
|
||||
}
|
||||
|
||||
/** A TextEncoder or TextDecoder record boxes as its handle (+1), which
|
||||
* holds the record by reference. A decoder passes the byte offsets of the
|
||||
* fields its boxed decode reads. */
|
||||
private textCodecToDyn(
|
||||
B: BlockBuilder,
|
||||
t: IrType & { kind: "record" },
|
||||
shape: IrRecordShape,
|
||||
codec: "TextEncoder" | "TextDecoder",
|
||||
): void {
|
||||
const host = this.host;
|
||||
const rc = vAdapters(host, t);
|
||||
const r = B.tmp();
|
||||
if (codec === "TextEncoder") {
|
||||
host.declare(`declare ptr @scr_text_encoder_to_dyn(ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`${r} = call ptr @scr_text_encoder_to_dyn(ptr %v, ptr ${rc.retain}, ptr ${rc.release})`,
|
||||
);
|
||||
} else {
|
||||
const struct = mangleRecordStruct(t.shapeId);
|
||||
const base = host.nullableUnions.fieldBase(t.shapeId);
|
||||
const start = B.tmp();
|
||||
B.line(`${start} = ptrtoint ptr %v to ${this.S}`);
|
||||
const offsets = TEXT_DECODER_FIELDS.map((field) => {
|
||||
const index = shape.fields.findIndex((f) => f.name === field);
|
||||
if (index < 0)
|
||||
throw new InternalCompilerError(`llvm emitter bug: TextDecoder record lacks ${field}`);
|
||||
const p = B.tmp();
|
||||
const at = B.tmp();
|
||||
const offset = B.tmp();
|
||||
B.line(
|
||||
`${p} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 ${index + base} ; .${llvmCommentText(field)}`,
|
||||
);
|
||||
B.line(`${at} = ptrtoint ptr ${p} to ${this.S}`);
|
||||
B.line(`${offset} = sub ${this.S} ${at}, ${start}`);
|
||||
return `${this.S} ${offset}`;
|
||||
});
|
||||
host.declare(
|
||||
`declare ptr @scr_text_decoder_to_dyn(ptr, ptr, ptr, ${this.S}, ${this.S}, ${this.S}, ${this.S})`,
|
||||
);
|
||||
B.line(
|
||||
`${r} = call ptr @scr_text_decoder_to_dyn(ptr %v, ptr ${rc.retain}, ptr ${rc.release}, ${offsets.join(", ")})`,
|
||||
);
|
||||
}
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
}
|
||||
|
||||
/* ── toDynHelper (walkers.ts, ported) ─────────────────────────── */
|
||||
|
||||
/** `sc_td_<n>(<T> v) -> ptr` — build a fresh dyn value from a
|
||||
@@ -2769,11 +2899,10 @@ export class LlDyn {
|
||||
let sourceAccessor: { name: string; release: string } | null = null;
|
||||
switch (t.kind) {
|
||||
case "date": {
|
||||
// A Date handle holding the same time value; native Dates are
|
||||
// immutable values, and so is the handle.
|
||||
host.declare(`declare ptr @scr_dyn_native_date_from_ms(double)`);
|
||||
// A Date handle holding the same Date object.
|
||||
host.declare(`declare ptr @scr_dyn_native_date(ptr)`);
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @scr_dyn_native_date_from_ms(double %v)`);
|
||||
B.line(`${r} = call ptr @scr_dyn_native_date(ptr %v)`);
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
break;
|
||||
}
|
||||
@@ -2927,6 +3056,11 @@ export class LlDyn {
|
||||
const shape = host.recordsById.get(t.shapeId);
|
||||
if (!shape)
|
||||
throw new InternalCompilerError(`llvm emitter bug: to-dyn of unknown shape ${t.shapeId}`);
|
||||
const codec = recordTextCodecClass(shape);
|
||||
if (codec !== null) {
|
||||
this.textCodecToDyn(B, t, shape, codec);
|
||||
break;
|
||||
}
|
||||
const struct = mangleRecordStruct(t.shapeId);
|
||||
const base = host.nullableUnions.fieldBase(t.shapeId);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + base]));
|
||||
|
||||
@@ -304,11 +304,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
// ScrGen owns the frame), so its OUT slot address is fixed.
|
||||
let out: string | null = null;
|
||||
const yieldKind = fn.generator?.yieldT.kind;
|
||||
if (
|
||||
!emitter.wasi &&
|
||||
fn.async !== true &&
|
||||
(yieldKind === "f64" || yieldKind === "date" || yieldKind === "bool")
|
||||
) {
|
||||
if (!emitter.wasi && fn.async !== true && (yieldKind === "f64" || yieldKind === "bool")) {
|
||||
emitter.declare(`declare ptr @scr_gen_of_fiber(ptr)`);
|
||||
const gen = B.tmp();
|
||||
B.line(`${gen} = call ptr @scr_gen_of_fiber(ptr ${self})`);
|
||||
@@ -704,7 +700,6 @@ export function emitGenScaffolding(emitter: LlEmitter): string[] {
|
||||
tr.push(` br label %done ; void body: the done value is undefined (NONE)`);
|
||||
break;
|
||||
case "f64":
|
||||
case "date":
|
||||
emitter.declare(`declare void @scr_gen_out_f64(ptr, double)`);
|
||||
tr.push(` call void @scr_gen_out_f64(ptr %g, double %r)`, ` br label %done`);
|
||||
break;
|
||||
@@ -1053,7 +1048,6 @@ export function emitAsyncAwaitCall(
|
||||
emitter.declare(`declare void @scr_await_void(ptr)`);
|
||||
return ` call void @scr_await_void(ptr ${promise})`;
|
||||
case "f64":
|
||||
case "date":
|
||||
emitter.declare(`declare double @scr_await_f64(ptr)`);
|
||||
return ` ${dest} = call double @scr_await_f64(ptr ${promise})`;
|
||||
case "bool":
|
||||
@@ -1108,7 +1102,7 @@ export function emitAsyncAwaitCall(
|
||||
const kind =
|
||||
ret.kind === "void"
|
||||
? "void"
|
||||
: ret.kind === "f64" || ret.kind === "date"
|
||||
: ret.kind === "f64"
|
||||
? "f64"
|
||||
: ret.kind === "bool"
|
||||
? "bool"
|
||||
@@ -1187,7 +1181,6 @@ export function emitWasiFulfill(emitter: LlEmitter, v: LlValue | null): void {
|
||||
case "void":
|
||||
return;
|
||||
case "f64":
|
||||
case "date":
|
||||
emitter.declare(`declare void @scr_gen_out_f64(ptr, double)`);
|
||||
emitter.B.line(`call void @scr_gen_out_f64(ptr ${gen}, double ${v!.name})`);
|
||||
return;
|
||||
@@ -1212,7 +1205,6 @@ export function emitWasiFulfill(emitter: LlEmitter, v: LlValue | null): void {
|
||||
emitter.B.line(`call void @scr_promise_fulfill_void(ptr ${pr})`);
|
||||
break;
|
||||
case "f64":
|
||||
case "date":
|
||||
emitter.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`);
|
||||
emitter.B.line(`call void @scr_promise_fulfill_f64(ptr ${pr}, double ${v!.name})`);
|
||||
break;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
BOOL,
|
||||
DATE_T,
|
||||
F64,
|
||||
PROCSTREAM_T,
|
||||
STRING,
|
||||
@@ -287,7 +286,7 @@ test("shared exceptional exits preserve the function's scalar return ABI", () =>
|
||||
});
|
||||
|
||||
test("exceptional exits use each scalar kind's LLVM return type", () => {
|
||||
for (const type of [F64, DATE_T, BOOL, PROCSTREAM_T]) {
|
||||
for (const type of [F64, BOOL, PROCSTREAM_T]) {
|
||||
for (const ownedLocals of [false, true]) {
|
||||
const module = moduleFor([call(), call(), { kind: "throw", value: literal("failed"), loc }]);
|
||||
const work = module.functions[1]!;
|
||||
|
||||
@@ -2049,7 +2049,6 @@ export class LlEmitter {
|
||||
if (POINTER_KINDS.has(t.kind)) return "ptr";
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
case "date":
|
||||
return "double";
|
||||
case "bool":
|
||||
return "i1";
|
||||
@@ -2182,7 +2181,6 @@ export class LlEmitter {
|
||||
this.declare(`declare void @scr_promise_fulfill_void(ptr)`);
|
||||
return [` call void @scr_promise_fulfill_void(ptr ${promise})`];
|
||||
case "f64":
|
||||
case "date":
|
||||
this.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`);
|
||||
return [` call void @scr_promise_fulfill_f64(ptr ${promise}, double %r)`];
|
||||
case "bool":
|
||||
@@ -2412,7 +2410,7 @@ export class LlEmitter {
|
||||
return { name: "", type };
|
||||
}
|
||||
const t = B.tmp();
|
||||
if (type.kind === "f64" || type.kind === "date") {
|
||||
if (type.kind === "f64") {
|
||||
this.declare(`declare double @scr_coro_direct_f64(ptr)`);
|
||||
B.line(`${t} = call double @scr_coro_direct_f64(ptr ${record})`);
|
||||
} else if (type.kind === "bool") {
|
||||
|
||||
@@ -184,7 +184,7 @@ export function emitIntrinsicExpr(host: LlvmEmitterContext, e: ExprOf<"intrinsic
|
||||
}
|
||||
const v = host.emitExpr(e.args[0]!);
|
||||
const t = e.args[0]!.type;
|
||||
if (t.kind === "f64" || t.kind === "date") {
|
||||
if (t.kind === "f64") {
|
||||
host.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`);
|
||||
B.line(`call void @scr_promise_fulfill_f64(ptr ${p}, double ${v.name})`);
|
||||
} else if (t.kind === "bool") {
|
||||
@@ -500,7 +500,7 @@ export function emitAsyncExpr(
|
||||
host.declare(`declare void @scr_async_gen_hop_done()`);
|
||||
B.line(`call void @scr_async_gen_hop_done()`);
|
||||
}
|
||||
if (yt.kind === "f64" || yt.kind === "date") {
|
||||
if (yt.kind === "f64") {
|
||||
if (host.currentCoro !== null) {
|
||||
const coro = host.currentCoro;
|
||||
if (coro.out !== null) host.emitGenSlotStore(coro.out, "f64", v.name);
|
||||
@@ -582,7 +582,7 @@ export function emitAsyncExpr(
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
const t = B.tmp();
|
||||
if (e.type.kind === "f64" || e.type.kind === "date") {
|
||||
if (e.type.kind === "f64") {
|
||||
host.declare(`declare double @scr_gen_take_in_f64()`);
|
||||
B.line(`${t} = call double @scr_gen_take_in_f64()`);
|
||||
return { name: t, type: e.type };
|
||||
@@ -710,12 +710,12 @@ export function emitAsyncExpr(
|
||||
throw new InternalCompilerError(`llvm emitter bug: genResume ${e.mode} on async`);
|
||||
const gb = emitBorrowedInput(host, e.gen);
|
||||
if (e.mode === "value") {
|
||||
if (!host.wasi && (e.type.kind === "f64" || e.type.kind === "date"))
|
||||
if (!host.wasi && e.type.kind === "f64")
|
||||
return { name: host.emitGenSlotTake(host.genOutSlot(gb.name), "f64"), type: e.type };
|
||||
if (!host.wasi && e.type.kind === "bool")
|
||||
return { name: host.emitGenSlotTake(host.genOutSlot(gb.name), "bool"), type: e.type };
|
||||
const t = B.tmp();
|
||||
if (e.type.kind === "f64" || e.type.kind === "date") {
|
||||
if (e.type.kind === "f64") {
|
||||
host.declare(`declare double @scr_gen_take_out_f64(ptr)`);
|
||||
B.line(`${t} = call double @scr_gen_take_out_f64(ptr ${gb.name})`);
|
||||
return { name: t, type: e.type };
|
||||
@@ -752,7 +752,7 @@ export function emitAsyncExpr(
|
||||
store(a.name, e.arg!.type);
|
||||
};
|
||||
const parkIn = (name: string, t: IrType): void => {
|
||||
if (t.kind === "f64" || t.kind === "date") {
|
||||
if (t.kind === "f64") {
|
||||
host.declare(`declare void @scr_gen_in_f64(ptr, double)`);
|
||||
B.line(`call void @scr_gen_in_f64(ptr ${g.name}, double ${name})`);
|
||||
} else if (t.kind === "bool") {
|
||||
@@ -787,7 +787,7 @@ export function emitAsyncExpr(
|
||||
const a = host.emitExpr(e.arg);
|
||||
const t = e.arg.type;
|
||||
if (isRefCounted(t)) host.toHeap(a);
|
||||
if (t.kind === "f64" || t.kind === "date") {
|
||||
if (t.kind === "f64") {
|
||||
host.declare(`declare ptr @scr_async_gen_next_f64(ptr, double)`);
|
||||
call = `call ptr @scr_async_gen_next_f64(ptr ${g.name}, double ${a.name})`;
|
||||
} else if (t.kind === "bool") {
|
||||
@@ -806,7 +806,7 @@ export function emitAsyncExpr(
|
||||
const a = host.emitExpr(e.arg);
|
||||
const t = e.arg.type;
|
||||
if (isRefCounted(t)) host.toHeap(a);
|
||||
if (t.kind === "f64" || t.kind === "date") {
|
||||
if (t.kind === "f64") {
|
||||
host.declare(`declare ptr @scr_async_gen_return_f64(ptr, double)`);
|
||||
call = `call ptr @scr_async_gen_return_f64(ptr ${g.name}, double ${a.name})`;
|
||||
} else if (t.kind === "bool") {
|
||||
@@ -849,7 +849,7 @@ export function emitAsyncExpr(
|
||||
B.line(`call void @scr_gen_ret_none(ptr ${g.name})`);
|
||||
} else {
|
||||
sendArg((name, t) => {
|
||||
if (t.kind === "f64" || t.kind === "date") {
|
||||
if (t.kind === "f64") {
|
||||
host.declare(`declare void @scr_gen_ret_f64(ptr, double)`);
|
||||
B.line(`call void @scr_gen_ret_f64(ptr ${g.name}, double ${name})`);
|
||||
} else if (t.kind === "bool") {
|
||||
@@ -960,7 +960,7 @@ export function emitAsyncExpr(
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
const t = B.tmp();
|
||||
if (e.type.kind === "f64" || e.type.kind === "date") {
|
||||
if (e.type.kind === "f64") {
|
||||
const f64Await = host.awaitResultFn("f64");
|
||||
host.declare(`declare double @${f64Await}(ptr)`);
|
||||
B.line(`${t} = call double @${f64Await}(ptr ${pr.name})`);
|
||||
@@ -1050,7 +1050,7 @@ export function emitAsyncExpr(
|
||||
const peek = host.unionPeek(u.name, def.id);
|
||||
if (host.coroAwait) host.emitCoroAwait(peek);
|
||||
let awaited: LlValue;
|
||||
if (inner.kind === "f64" || inner.kind === "date") {
|
||||
if (inner.kind === "f64") {
|
||||
const f64Await = host.awaitResultFn("f64");
|
||||
host.declare(`declare double @${f64Await}(ptr)`);
|
||||
const x = B.tmp();
|
||||
@@ -1136,7 +1136,6 @@ export function emitAsyncExpr(
|
||||
let resolve: string;
|
||||
const kindNums: Partial<Record<IrType["kind"], number>> = {
|
||||
f64: 0,
|
||||
date: 0,
|
||||
bool: 1,
|
||||
string: 2,
|
||||
void: 3,
|
||||
@@ -1191,7 +1190,6 @@ export function emitAsyncExpr(
|
||||
B.line(`${p} = call ptr @scr_promise_new()`);
|
||||
const kindNums: Partial<Record<IrType["kind"], number>> = {
|
||||
f64: 0,
|
||||
date: 0,
|
||||
bool: 1,
|
||||
string: 2,
|
||||
void: 3,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { BYTES_ELEMENT_SIZE, F64, type IrBytesElem, type IrExpr } from "../../ir/ir.js";
|
||||
import type { LlvmEmitterContext, LlValue } from "./expr-context.js";
|
||||
import { F64_INF, exactIndexLines, f64Lit, sizeToDouble } from "./common.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
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";
|
||||
@@ -86,17 +86,18 @@ export function emitBytesIndex(
|
||||
): string {
|
||||
const B = host.B;
|
||||
const index = value.name;
|
||||
const integer = exactInteger(host, value, expr);
|
||||
if (inBounds && integer) {
|
||||
const wide = widenInteger(host, integer);
|
||||
if (host.sizeType === "i64") return wide;
|
||||
// The exact integer, or a wide view that always equals the double (such
|
||||
// as `i + 1` of a speculated counter).
|
||||
const exact = alwaysExactInteger(host, value, exactInteger(host, value, expr));
|
||||
if (inBounds && exact !== null) {
|
||||
if (host.sizeType === "i64") return exact;
|
||||
const narrow = B.tmp();
|
||||
B.line(`${narrow} = trunc i64 ${wide} to ${host.sizeType}`);
|
||||
B.line(`${narrow} = trunc i64 ${exact} to ${host.sizeType}`);
|
||||
return narrow;
|
||||
}
|
||||
const len = emitBytesHeaderLength(host, receiver);
|
||||
if (integer) {
|
||||
const wideIndex = widenInteger(host, integer);
|
||||
if (exact !== null) {
|
||||
const wideIndex = exact;
|
||||
const wideLength = host.sizeType === "i64" ? len : B.tmp();
|
||||
if (host.sizeType !== "i64") B.line(`${wideLength} = zext ${host.sizeType} ${len} to i64`);
|
||||
const inRange = B.tmp();
|
||||
|
||||
@@ -936,6 +936,12 @@ export function emitArrIntrinsic(
|
||||
B.line(`${t} = call ptr @${helper}(ptr ${r.name}, ptr ${sep.name})`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
if (elem.kind === "date") {
|
||||
host.declare(`declare ptr @scr_date_arr_join(ptr, ptr)`);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call ptr @scr_date_arr_join(ptr ${r.name}, ptr ${sep.name})`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
host.declare(`declare ptr @scr_arr_join(ptr, ptr)`);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call ptr @scr_arr_join(ptr ${r.name}, ptr ${sep.name})`);
|
||||
@@ -960,6 +966,16 @@ export function emitArrIntrinsic(
|
||||
case "sortPrimitive":
|
||||
case "toSortedPrimitive": {
|
||||
if (e.method === "sortPrimitive") host.emitPublishedArrayGuard(r.name, e.receiver.type, 7);
|
||||
if (elem.kind === "date") {
|
||||
// Dates order by their toString() text.
|
||||
host.declare(`declare ptr @scr_arr_sort_by_text(ptr, i1 zeroext, ptr)`);
|
||||
host.declare(`declare ptr @scr_date_object_to_string(ptr)`);
|
||||
const sorted = B.tmp();
|
||||
B.line(
|
||||
`${sorted} = call ptr @scr_arr_sort_by_text(ptr ${r.name}, i1 ${e.method === "toSortedPrimitive" ? 1 : 0}, ptr @scr_date_object_to_string)`,
|
||||
);
|
||||
return host.own({ name: sorted, type: e.type });
|
||||
}
|
||||
host.declare(`declare ptr @scr_arr_sort_primitive(ptr, i1 zeroext)`);
|
||||
const result = B.tmp();
|
||||
B.line(
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
type IrExpr,
|
||||
type IrType,
|
||||
isDynTypedRefType,
|
||||
textCodecRecordClass,
|
||||
isRefCounted,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
@@ -86,7 +87,10 @@ export function emitDynamicExpr(
|
||||
// Bytes already box their shared mutable storage directly. A second
|
||||
// capsule would split identity between checked and native views.
|
||||
const errorRef = v.type.kind === "object" && host.shapeHost.isErrorClass(v.type.className);
|
||||
if (!errorRef && ((e.liveRef && v.type.kind !== "bytes") || identityRef)) {
|
||||
// A TextEncoder or TextDecoder record boxes as its own handle, which
|
||||
// keeps the record's identity (toDynHelper).
|
||||
const codecRef = textCodecRecordClass(v.type, (id) => host.recordsById.get(id)) !== null;
|
||||
if (!errorRef && !codecRef && ((e.liveRef && v.type.kind !== "bytes") || identityRef)) {
|
||||
if (v.type.kind === "union") {
|
||||
const adapter = host.liveDynUnionRefAdapter(v.type);
|
||||
const boxed = B.tmp();
|
||||
|
||||
@@ -13,6 +13,7 @@ import { emitStringInputs } from "./string-lifetimes.js";
|
||||
import { emitStringParts, numberPart, stringParts } from "./string-construction.js";
|
||||
import { emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
import {
|
||||
alwaysExactInteger,
|
||||
exactInteger,
|
||||
int32Number,
|
||||
integerNumber,
|
||||
@@ -46,6 +47,16 @@ import {
|
||||
} from "./dense-array-access.js";
|
||||
import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js";
|
||||
|
||||
/** The signed integer predicates of the number compares. */
|
||||
const INTEGER_COMPARES: Record<string, string> = {
|
||||
"<": "slt",
|
||||
"<=": "sle",
|
||||
">": "sgt",
|
||||
">=": "sge",
|
||||
"===": "eq",
|
||||
"!==": "ne",
|
||||
};
|
||||
|
||||
export function emitLiteralExpr(
|
||||
host: LlvmEmitterContext,
|
||||
e: ExprOf<"numLit" | "boolLit" | "strLit" | "moduleNsRef" | "unitLit" | "varRef">,
|
||||
@@ -318,15 +329,7 @@ export function emitOperatorExpr(
|
||||
if (integer) return integer;
|
||||
}
|
||||
if (cmp[e.op] !== undefined) {
|
||||
const integerCmp: Record<string, string> = {
|
||||
"<": "slt",
|
||||
"<=": "sle",
|
||||
">": "sgt",
|
||||
">=": "sge",
|
||||
"===": "eq",
|
||||
"!==": "ne",
|
||||
};
|
||||
B.line(`${t} = icmp ${integerCmp[e.op]} i64 ${left}, ${right}`);
|
||||
B.line(`${t} = icmp ${INTEGER_COMPARES[e.op]} i64 ${left}, ${right}`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
}
|
||||
@@ -356,7 +359,17 @@ export function emitOperatorExpr(
|
||||
if (wideInteger) value.wideInteger = wideInteger;
|
||||
return value;
|
||||
}
|
||||
} else B.line(`${t} = fcmp ${cmp[e.op]} double ${l.name}, ${r.name}`);
|
||||
} else {
|
||||
// An operand with only a wide view, such as `i + 1` of a speculated
|
||||
// counter: compare the integers when both always equal the doubles.
|
||||
const left = alwaysExactInteger(host, l, li),
|
||||
right = left === null ? null : alwaysExactInteger(host, r, ri);
|
||||
if (left !== null && right !== null) {
|
||||
B.line(`${t} = icmp ${INTEGER_COMPARES[e.op]} i64 ${left}, ${right}`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
B.line(`${t} = fcmp ${cmp[e.op]} double ${l.name}, ${r.name}`);
|
||||
}
|
||||
} else if (bit[e.op] !== undefined) {
|
||||
if (e.left.type.kind !== "f64" || e.right.type.kind !== "f64") {
|
||||
throw new LlvmUnsupportedError(
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
KEY_ORDER_FIELD,
|
||||
shapeKeyOrders,
|
||||
isDynTypedRefType,
|
||||
textCodecRecordClass,
|
||||
isRefCounted,
|
||||
shapeHasAccessorSlots,
|
||||
typeKey,
|
||||
@@ -121,12 +122,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri
|
||||
if (inner.kind === "void") {
|
||||
host.declare(`declare void @scr_promise_fulfill_void(ptr)`);
|
||||
B.line(`call void @scr_promise_fulfill_void(ptr %dst)`);
|
||||
} else if (
|
||||
inner.kind === "f64" ||
|
||||
inner.kind === "date" ||
|
||||
inner.kind === "procStream" ||
|
||||
inner.kind === "bool"
|
||||
) {
|
||||
} else if (inner.kind === "f64" || inner.kind === "procStream" || inner.kind === "bool") {
|
||||
const fn = inner.kind !== "bool" ? "scr_promise_fulfill_f64" : "scr_promise_fulfill_bool";
|
||||
host.declare(
|
||||
`declare void @${fn}(ptr, ${host.llType(inner)}${inner.kind === "bool" ? " zeroext" : ""})`,
|
||||
@@ -601,12 +597,21 @@ export function liveDynUnionRefAdapter(
|
||||
if (!union) {
|
||||
throw new InternalCompilerError(`llvm emitter bug: live dyn ref of unknown union ${t.unionId}`);
|
||||
}
|
||||
// Codec records box as their own handles, by reference, through toDynHelper.
|
||||
const codecArm = (arm: IrType): boolean =>
|
||||
textCodecRecordClass(arm, (id) => host.recordsById.get(id)) !== null;
|
||||
const mutableArms = union.arms
|
||||
.map((arm, tag) => ({ arm, tag }))
|
||||
.filter(
|
||||
({ arm }) => arm.kind !== "bytes" && (streamTypedRefEligible(arm) || isDynTypedRefType(arm)),
|
||||
({ arm }) =>
|
||||
arm.kind !== "bytes" &&
|
||||
!codecArm(arm) &&
|
||||
(streamTypedRefEligible(arm) || isDynTypedRefType(arm)),
|
||||
);
|
||||
if (mutableArms.length === 0 && !union.arms.some((arm) => arm.kind === "bytes")) {
|
||||
if (
|
||||
mutableArms.length === 0 &&
|
||||
!union.arms.some((arm) => arm.kind === "bytes" || codecArm(arm))
|
||||
) {
|
||||
throw new InternalCompilerError(`llvm emitter bug: live dyn ref of immutable union ${key}`);
|
||||
}
|
||||
|
||||
@@ -716,7 +721,13 @@ export function streamTypedRefBoxValue(
|
||||
B.line(`${boxed} = call ptr @${host.liveDynUnionRefAdapter(t)}(ptr ${value})`);
|
||||
return boxed;
|
||||
}
|
||||
if (t.kind === "bytes" || (!streamTypedRefEligible(t) && !isDynTypedRefType(t))) {
|
||||
// A codec record boxes as its own handle, by reference (toDynHelper), as
|
||||
// it does outside a live view.
|
||||
if (
|
||||
t.kind === "bytes" ||
|
||||
textCodecRecordClass(t, (id) => host.recordsById.get(id)) !== null ||
|
||||
(!streamTypedRefEligible(t) && !isDynTypedRefType(t))
|
||||
) {
|
||||
const valueTy = host.llType(t);
|
||||
B.line(`${boxed} = call ptr @${host.dyn.toDynHelper(t)}(${valueTy} ${value})`);
|
||||
return boxed;
|
||||
|
||||
@@ -65,6 +65,22 @@ function magnitude(range: IntegerRange): number {
|
||||
return Math.max(Math.abs(range.min), Math.abs(range.max));
|
||||
}
|
||||
|
||||
/** The bounds of a sum and a product of two magnitude bounds, as doubles.
|
||||
* Below 2^53 the double result is the integer; above it the result is past
|
||||
* every exactness limit either way, and rounding moves it far less than the
|
||||
* margin from WIDE_BOUND to the i64 range. A result of exactly 2^53 may
|
||||
* stand for 2^53 + 1 (2^53 - 1 + 2 rounds to 2^53), so it moves to the next
|
||||
* double up unless the sum is exactly 2^53. */
|
||||
function sumBound(a: number, b: number): number {
|
||||
const sum = a + b;
|
||||
return sum === EXACT_DOUBLES && EXACT_DOUBLES - a !== b ? sum + 2 : sum;
|
||||
}
|
||||
|
||||
function productBound(a: number, b: number): number {
|
||||
const product = a * b;
|
||||
return product === EXACT_DOUBLES ? product + 2 : product;
|
||||
}
|
||||
|
||||
/** The wide view of `l op r` for `+`, `-` and `*` when the exact operands
|
||||
* have no proven result range (their product or sum may pass 2^53).
|
||||
*
|
||||
@@ -98,14 +114,14 @@ export function wideArithmetic(
|
||||
if (op === "*") {
|
||||
if (lv.below !== Infinity || rv.below !== Infinity) return null;
|
||||
below = EXACT_DOUBLES;
|
||||
bound = lv.bound * rv.bound;
|
||||
bound = productBound(lv.bound, rv.bound);
|
||||
} else if (lv.below === Infinity && rv.below === Infinity) {
|
||||
below = EXACT_DOUBLES;
|
||||
bound = lv.bound + rv.bound;
|
||||
bound = sumBound(lv.bound, rv.bound);
|
||||
} else if (lv.below === Infinity || rv.below === Infinity) {
|
||||
const [exact, wide] = lv.below === Infinity ? [lv, rv] : [rv, lv];
|
||||
below = Math.min(wide.below - exact.bound, EXACT_DOUBLES);
|
||||
bound = exact.bound + wide.bound;
|
||||
bound = sumBound(exact.bound, wide.bound);
|
||||
} else return null;
|
||||
if (!(bound <= WIDE_BOUND) || below < INDEX_LIMIT) return null;
|
||||
const operand = (value: LlValue, integer: LlInteger | null): string =>
|
||||
@@ -134,6 +150,20 @@ export function indexInteger(
|
||||
return wide && wide.below >= INDEX_LIMIT ? wide.name : null;
|
||||
}
|
||||
|
||||
/** An i64 equal to a value's double wherever the value can be: its exact
|
||||
* integer, or a wide view whose magnitude bound is within the magnitude below
|
||||
* which the view is exact. Every integer of magnitude at most 2^53 is a
|
||||
* double, so a bound equal to that limit is exact too. Null otherwise. */
|
||||
export function alwaysExactInteger(
|
||||
host: LlvmEmitterContext,
|
||||
value: LlValue,
|
||||
integer: LlInteger | null,
|
||||
): string | null {
|
||||
if (integer) return widenInteger(host, integer);
|
||||
const wide = value.wideInteger;
|
||||
return wide && wide.bound <= wide.below ? wide.name : null;
|
||||
}
|
||||
|
||||
export function widenInteger(host: LlvmEmitterContext, value: LlInteger): string {
|
||||
if (value.type === "i64") return value.name;
|
||||
const wide = host.B.tmp();
|
||||
|
||||
@@ -5,11 +5,12 @@ import { emitTlsLibCall } from "./lib-tls.js";
|
||||
import { emitHttp2LibCall } from "./lib-http2.js";
|
||||
import { emitDatagramLibCall } from "./lib-datagram.js";
|
||||
import { emitTestLibCall } from "./lib-test.js";
|
||||
import { FN_ATTRS, arrNewCall, traceArg, vAdapters } from "./shapes.js";
|
||||
/* Focused LLVM library-call emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
|
||||
import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
|
||||
import type { IrExpr, IrType } from "../../ir/ir.js";
|
||||
import { F64, type IrExpr, type IrType } from "../../ir/ir.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import { emitJsLibCall } from "./jsval.js";
|
||||
import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js";
|
||||
@@ -239,6 +240,42 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
host.emitPendingCheck();
|
||||
return output;
|
||||
}
|
||||
if (e.fn === "promise.allTuple") return emitPromiseAllTuple(host, e);
|
||||
if (e.fn === "promise.fulfilledValue") {
|
||||
// The payload of a fulfilled promise (borrowed operand): the scalar
|
||||
// kinds that lower to a double (numbers, Dates, process streams) and
|
||||
// booleans by value, strings and references +1.
|
||||
const promise = host.emitExpr(e.args[0]!);
|
||||
const out = B.tmp();
|
||||
const ty = host.llType(e.type);
|
||||
if (ty === "double") {
|
||||
host.declare(`declare double @scr_promise_payload_f64(ptr)`);
|
||||
B.line(`${out} = call double @scr_promise_payload_f64(ptr ${promise.name})`);
|
||||
return { name: out, type: e.type };
|
||||
}
|
||||
if (ty === "i1") {
|
||||
host.declare(`declare zeroext i1 @scr_promise_payload_bool(ptr)`);
|
||||
B.line(`${out} = call zeroext i1 @scr_promise_payload_bool(ptr ${promise.name})`);
|
||||
return { name: out, type: e.type };
|
||||
}
|
||||
if (e.type.kind === "string") {
|
||||
host.declare(`declare ptr @scr_promise_payload_str(ptr)`);
|
||||
B.line(`${out} = call ptr @scr_promise_payload_str(ptr ${promise.name}) ; +1`);
|
||||
return host.own({ name: out, type: e.type });
|
||||
}
|
||||
host.declare(`declare ptr @scr_promise_payload_ref(ptr)`);
|
||||
B.line(`${out} = call ptr @scr_promise_payload_ref(ptr ${promise.name}) ; +1`);
|
||||
return host.own({ name: out, type: e.type });
|
||||
}
|
||||
if (e.fn === "record.sourceSpread") {
|
||||
const built = host.emitReadReceiver(e.args[0]!);
|
||||
const value = host.emitReadReceiver(e.args[1]!);
|
||||
const type = e.args[0]!.type;
|
||||
if (type.kind !== "record")
|
||||
throw new InternalCompilerError("llvm emitter bug: record.sourceSpread of a non-record");
|
||||
host.dyn.emitRecordSourceSpread(B, built.name, value.name, type.shapeId);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "dyn.typedRefIs" || e.fn === "dyn.typedRefKeyIs") {
|
||||
const literal = e.args[1]!.kind === "strLit" ? e.args[1]!.value : null;
|
||||
if (e.fn === "dyn.typedRefKeyIs" && literal === null)
|
||||
@@ -629,16 +666,66 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
? "scr_stream_dyn_async_iterator_open"
|
||||
: LIB_FN_SYMS[e.fn]);
|
||||
if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
|
||||
// The runtime's Date functions take and answer time values: a Date
|
||||
// operand passes its time value, read at once from the borrowed object,
|
||||
// and a Date result is a new Date object.
|
||||
const args = READ_ONLY_STRING_LIB_FNS.has(e.fn)
|
||||
? emitBorrowedInputs(host, e.args)
|
||||
: e.args.map((a) =>
|
||||
borrowsJsonInputs(e.fn) || borrowsIteratorInputs(e.fn)
|
||||
? emitBorrowedInput(host, a)
|
||||
: host.emitExpr(a),
|
||||
a.type.kind === "date"
|
||||
? { name: emitDateTimeArg(host, a), type: F64 }
|
||||
: borrowsJsonInputs(e.fn) || borrowsIteratorInputs(e.fn)
|
||||
? emitBorrowedInput(host, a)
|
||||
: host.emitExpr(a),
|
||||
);
|
||||
// getTime and valueOf answer the time value the argument already is.
|
||||
if ((e.fn === "date.getTime" || e.fn === "date.valueOf") && e.args[0]?.type.kind === "date")
|
||||
return { name: args[0]!.name, type: e.type };
|
||||
// The deep comparisons' number test: computed here, with no runtime call.
|
||||
if (e.fn === "assert.sameValue")
|
||||
return { name: emitSameValueF64(host, args[0]!.name, args[1]!.name), type: e.type };
|
||||
const callArgs = args;
|
||||
if (e.type.kind === "date") {
|
||||
const ms = emitSymbolLibCall(host, { ...e, type: F64 }, sym, callArgs);
|
||||
host.declare(`declare ptr @scr_date_new(double)`);
|
||||
const date = B.tmp();
|
||||
B.line(`${date} = call ptr @scr_date_new(double ${ms.name})`);
|
||||
return host.own({ name: date, type: e.type });
|
||||
}
|
||||
return emitSymbolLibCall(host, e, sym, callArgs);
|
||||
}
|
||||
|
||||
/** The time value of a Date argument. A Date that a runtime Date function
|
||||
* produces for this argument alone (`new Date(ms).getTime()`, a
|
||||
* time-value local's read) passes the function's time value and creates
|
||||
* no Date object. */
|
||||
function emitDateTimeArg(host: LlvmEmitterContext, date: IrExpr): string {
|
||||
// A time-value local's read is its time value itself.
|
||||
if (date.kind === "libCall" && date.fn === "date.fromTime")
|
||||
return host.emitExpr(date.args[0]!).name;
|
||||
if (date.kind === "libCall" && date.fn.startsWith("date."))
|
||||
return emitGenericLibCall(host, { ...date, type: F64 }).name;
|
||||
return emitDateTime(host, emitBorrowedInput(host, date).name);
|
||||
}
|
||||
|
||||
/** A Date object's time value (ScrDate.ms). */
|
||||
function emitDateTime(host: LlvmEmitterContext, date: string): string {
|
||||
const B = host.B;
|
||||
const field = B.tmp();
|
||||
const ms = B.tmp();
|
||||
B.line(`${field} = getelementptr inbounds i8, ptr ${date}, i64 8 ; ->ms`);
|
||||
B.line(`${ms} = load double, ptr ${field}`);
|
||||
return ms;
|
||||
}
|
||||
|
||||
/** The call of runtime function `sym` with evaluated arguments. */
|
||||
function emitSymbolLibCall(
|
||||
host: LlvmEmitterContext,
|
||||
e: LibCallExpr,
|
||||
sym: string,
|
||||
args: readonly LlValue[],
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const argDecls = args.map((a) => {
|
||||
const ty = host.llType(a.type);
|
||||
return ty === "i1" ? "i1 zeroext" : ty;
|
||||
@@ -968,6 +1055,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
case "disposable":
|
||||
case "punycode":
|
||||
case "constants":
|
||||
case "promise":
|
||||
return host.emitGenericLibCall(e);
|
||||
case "test":
|
||||
return emitTestLibCall(host, e);
|
||||
@@ -1005,3 +1093,63 @@ function emitArrayForeignThrow(host: LlvmEmitterContext, e: LibCallExpr): LlValu
|
||||
host.emitAlwaysPendingUnwind();
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 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
|
||||
* the tuple inside the combinator's settlement; a rejection settles the
|
||||
* result the same way. The combinator itself counts as handled. */
|
||||
function emitPromiseAllTuple(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
const build = e.args[1]!;
|
||||
if (build.kind !== "closure" || e.type.kind !== "promise")
|
||||
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`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
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()`);
|
||||
host.declare(`declare void @scr_promise_race_add(ptr, ptr, ptr)`);
|
||||
host.declare(`declare void @scr_promise_mark_handled(ptr)`);
|
||||
host.declare(`declare void @scr_promise_release(ptr)`);
|
||||
host.declare(`declare double @scr_arr_len(ptr)`);
|
||||
if (entriesT.kind !== "array")
|
||||
throw new InternalCompilerError("llvm emitter bug: promise.allTuple entries");
|
||||
const len = B.tmp();
|
||||
const cap = B.tmp();
|
||||
B.line(`${len} = call double @scr_arr_len(ptr ${entries.name})`);
|
||||
B.line(`${cap} = fptoui double ${len} to ${host.sizeType}`);
|
||||
const values = B.tmp();
|
||||
B.line(`${values} = ${arrNewCall(host.shapeHost, entriesT.elem, cap)}`);
|
||||
host.own({ name: values, type: entriesT });
|
||||
const all = B.tmp();
|
||||
B.line(
|
||||
`${all} = call ptr @scr_promise_all(ptr ${entries.name}, ptr ${values}, ptr @scr_promise_all_store_promise)`,
|
||||
);
|
||||
B.line(`call void @scr_promise_mark_handled(ptr ${all})`);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = call ptr @scr_promise_new()`);
|
||||
B.line(`call void @scr_promise_race_add(ptr ${result}, ptr ${all}, ptr @${adapter})`);
|
||||
B.line(`call void @scr_promise_release(ptr ${all})`);
|
||||
return host.own({ name: result, type: e.type });
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/* Focused LLVM library-call emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { F64, RUNTIME_ERROR_CLASSES, STRING } from "../../ir/ir.js";
|
||||
import { F64, type IrType, RUNTIME_ERROR_CLASSES, STRING, typeKey } from "../../ir/ir.js";
|
||||
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";
|
||||
|
||||
export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
@@ -460,9 +461,58 @@ export function emitAsyncContextLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
for (const a of e.args) host.emitExpr(a);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "als.bindCallback") {
|
||||
// The closure moves into the bound closure, whose entry is the
|
||||
// per-signature thunk; with nothing to bind the runtime answers the
|
||||
// closure itself.
|
||||
if (e.type.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: als.bindCallback on non-func");
|
||||
const cb = host.emitExpr(e.args[0]!);
|
||||
const always = host.emitExpr(e.args[1]!);
|
||||
host.toHeap(cb);
|
||||
const thunk = boundCallbackThunk(host, e.type);
|
||||
host.declare("declare ptr @scr_als_bind_closure(ptr, ptr, i1 zeroext)");
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call ptr @scr_als_bind_closure(ptr ${cb.name}, ptr @${thunk}, i1 ${always.name})`,
|
||||
);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
return host.emitGenericLibCall(e);
|
||||
}
|
||||
|
||||
/** The entry of a callback bound to an AsyncLocalStorage context, one per
|
||||
* callback signature: enters the captured context, calls the target with
|
||||
* the same arguments (the callValue ABI) and restores the caller's
|
||||
* context, also when the target leaves an exception pending. */
|
||||
function boundCallbackThunk(host: LlvmEmitterContext, cbT: IrType & { kind: "func" }): string {
|
||||
const key = `alsbound:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = `sc_alsbound_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
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)");
|
||||
const ret = host.llType(cbT.ret);
|
||||
const params = cbT.params.map((p, i) => `${host.llType(p)} %a${i}`);
|
||||
const call = `call ${ret} %fn(${["ptr %cb", ...params].join(", ")})`;
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal ${ret} @${sym}(${["ptr %self", ...params].join(", ")}) ${FN_ATTRS} { ; AsyncLocalStorage-bound callback ${typeKey(cbT)}`,
|
||||
`entry:`,
|
||||
` %cb = call ptr @scr_als_bound_target(ptr %self)`,
|
||||
` %prev = call ptr @scr_als_bound_enter(ptr %self)`,
|
||||
` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`,
|
||||
` %fn = load ptr, ptr %fnp`,
|
||||
ret === "void" ? ` ${call}` : ` %r = ${call}`,
|
||||
` call void @scr_als_bound_leave(ptr %prev, ptr %cb)`,
|
||||
ret === "void" ? ` ret void` : ` ret ${ret} %r`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
return sym;
|
||||
}
|
||||
|
||||
export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
if (e.fn === "process.forkTarget") {
|
||||
|
||||
@@ -333,6 +333,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"date.parse": "scr_date_parse",
|
||||
"date.getTime": "scr_date_get_time",
|
||||
"date.valueOf": "scr_date_get_time",
|
||||
"date.fromTime": "scr_date_get_time",
|
||||
"date.parseGetTime": "scr_date_parse",
|
||||
"date.utc": "scr_date_utc",
|
||||
"date.newLocal": "scr_date_new_local",
|
||||
@@ -454,6 +455,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"buffer.poolFromDyn": "scr_buffer_from_dyn_pooled",
|
||||
"buffer.poolFromDynRef": "scr_buffer_from_dyn_ref_pooled",
|
||||
"buffer.poolFromDynOffset": "scr_buffer_from_dyn_offset_pooled",
|
||||
"buffer.poolFromDynRange": "scr_buffer_from_dyn_range_pooled",
|
||||
"buffer.poolSize": "scr_buffer_pool_size",
|
||||
"buffer.setPoolSize": "scr_buffer_set_pool_size",
|
||||
"buffer.byteLenDyn": "scr_buffer_byte_length_dyn",
|
||||
@@ -479,6 +481,12 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"bytes.equalsChk": "scr_bytes_equals_chk",
|
||||
"bytes.compareChk": "scr_bytes_compare_chk",
|
||||
"buffer.newStringFail": "scr_buffer_new_string_fail",
|
||||
"buffer.deprecation": "scr_buffer_deprecation",
|
||||
"buffer.deprecationCount": "scr_buffer_deprecation_count",
|
||||
"buffer.constructor": "scr_buffer_constructor_value",
|
||||
"buffer.installValue": "scr_buffer_install_value",
|
||||
"buffer.staticValue": "scr_buffer_static_value",
|
||||
"buffer.prototypeMember": "scr_buffer_prototype_member",
|
||||
"fs.toUnixTimestamp": "scr_fs_to_unix_timestamp",
|
||||
"fs.existsChk": "scr_fs_exists_async",
|
||||
"fs.mkdtempSyncChk": "scr_fs_mkdtemp_sync_chk",
|
||||
@@ -560,6 +568,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"text.decoderEncoding": "scr_text_decoder_encoding",
|
||||
"text.decoderName": "scr_text_decoder_name",
|
||||
"text.encodeInto": "scr_text_encode_into",
|
||||
"text.codecIs": "scr_text_codec_is",
|
||||
"buffer.isAscii": "scr_buffer_is_ascii",
|
||||
"buffer.isUtf8": "scr_buffer_is_utf8",
|
||||
"buffer.transcode": "scr_buffer_transcode",
|
||||
@@ -677,6 +686,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"insp.circular": "scr_insp_circular",
|
||||
// Undeclared source properties kept beside a checked record (scr_json.c).
|
||||
"record.sourceHas": "scr_record_source_has",
|
||||
"record.sourceSpread": "scr_record_source_spread",
|
||||
// WHATWG URL + URLSearchParams (scr_url.c / scr_url_params.c):
|
||||
// constructions +1; url.new, the fileURLToPath pair, the win32
|
||||
// pathToFileURL flavor, and sp.fromPairs throw catchably (may-throw).
|
||||
@@ -701,6 +711,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"url.hash": "scr_url_hash",
|
||||
"url.searchParams": "scr_url_search_params",
|
||||
"url.fileURLToPathUrl": "scr_url_to_path",
|
||||
"url.fileURLToPathValue": "scr_url_to_path_value",
|
||||
"fs.checkPath": "scr_fs_check_path",
|
||||
"fs.checkReadEncoding": "scr_fs_check_read_encoding",
|
||||
"url.fileURLToPathChecked": "scr_url_checked_to_path",
|
||||
"url.fileURLToPathOptions": "scr_url_checked_to_path_options",
|
||||
"url.fileURLToPathBuffer": "scr_url_checked_to_path_buffer",
|
||||
@@ -1068,6 +1081,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"crypto.generateKeyPairSync": "scr_crypto_generate_key_pair_sync",
|
||||
"crypto.generateKeyPairCb": "scr_crypto_generate_key_pair_async",
|
||||
"crypto.signOneshot": "scr_crypto_sign_oneshot",
|
||||
"crypto.diffieHellman": "scr_crypto_diffie_hellman",
|
||||
"crypto.verifyOneshot": "scr_crypto_verify_oneshot",
|
||||
"crypto.createSign": "scr_crypto_create_sign",
|
||||
"crypto.createVerify": "scr_crypto_create_verify",
|
||||
@@ -1485,6 +1499,10 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"als.exitRun": "scr_als_exit_run",
|
||||
"als.enterWith": "scr_als_enter_with",
|
||||
"als.disable": "scr_als_disable",
|
||||
"als.newOptions": "scr_als_new_options",
|
||||
"als.name": "scr_als_name",
|
||||
"als.bindCallbackDyn": "scr_als_bind_dyn",
|
||||
"als.snapshot": "scr_als_snapshot",
|
||||
"dc.chanBindStore": "scr_dc_chan_bind_store",
|
||||
"dc.chanUnbindStore": "scr_dc_chan_unbind_store",
|
||||
"dc.chanRunStores": "scr_dc_chan_run_stores",
|
||||
@@ -1649,6 +1667,7 @@ export function mockTimerSym(
|
||||
|
||||
export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"crypto.generateKeyPairCb",
|
||||
"crypto.diffieHellman",
|
||||
"crypto.checkPrimeCb",
|
||||
"crypto.generatePrimeCb",
|
||||
"process.emitWarning",
|
||||
@@ -1674,6 +1693,8 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"url.legacyParse",
|
||||
"url.legacyFormat",
|
||||
"url.legacyResolve",
|
||||
// The deprecated Buffer constructor reports DEP0005 on the next tick.
|
||||
"buffer.deprecation",
|
||||
// The wrappers schedule warnings and callbacks on the tick queue.
|
||||
"util.deprecate",
|
||||
"util.callbackify",
|
||||
|
||||
@@ -596,10 +596,10 @@ export class PublishWalkers {
|
||||
}
|
||||
}
|
||||
|
||||
/** Unpublishable types stored as scalars (dates are time values, process
|
||||
* streams file descriptors): no pointer to push, and always a value. */
|
||||
/** Unpublishable types stored as scalars (process streams are file
|
||||
* descriptors): no pointer to push, and always a value. */
|
||||
function scalarRefusal(t: IrType): boolean {
|
||||
return t.kind === "date" || t.kind === "procStream";
|
||||
return t.kind === "procStream";
|
||||
}
|
||||
|
||||
/** The Node.js implementation's refusal for a value of type `t`. */
|
||||
|
||||
@@ -1100,6 +1100,7 @@ export function arrNewCall(
|
||||
elem.kind === "stats" || // fs.Stats snapshots: scr_stats_* adapters, no trace
|
||||
elem.kind === "dyn" || // native collection seeds/drains: traced REF
|
||||
elem.kind === "regex" || // RegExp values: scr_regex_* adapters, no trace (no refs inside)
|
||||
elem.kind === "date" || // Date objects: scr_date_* adapters, no trace (no refs inside)
|
||||
(elem.kind === "array" && traceAdapter(host, elem) !== null);
|
||||
if (!useRef) {
|
||||
if (small) {
|
||||
@@ -1163,7 +1164,6 @@ const SCR_ARR_OPS_RELEASE_NOTHING = 2;
|
||||
export function boxNewCall(host: ShapeHost, t: IrType): string {
|
||||
const plain: Partial<Record<IrType["kind"], number>> = {
|
||||
f64: 0,
|
||||
date: 0,
|
||||
procStream: 0,
|
||||
bool: 1,
|
||||
string: 2,
|
||||
@@ -1188,11 +1188,7 @@ export function boxNewCall(host: ShapeHost, t: IrType): string {
|
||||
|
||||
/** Box accessor suffix (boxAccess): scalars unboxed, ref kinds pointers. */
|
||||
export function boxAccess(t: IrType): "f64" | "bool" | "ref" {
|
||||
return t.kind === "f64" || t.kind === "date" || t.kind === "procStream"
|
||||
? "f64"
|
||||
: t.kind === "bool"
|
||||
? "bool"
|
||||
: "ref";
|
||||
return t.kind === "f64" || t.kind === "procStream" ? "f64" : t.kind === "bool" ? "bool" : "ref";
|
||||
}
|
||||
|
||||
/* ── record shapes ────────────────────────────────────────────────────── */
|
||||
@@ -1208,7 +1204,6 @@ export function llFieldType(
|
||||
if (POINTER_KINDS.has(t.kind)) return "ptr";
|
||||
switch (t.kind) {
|
||||
case "f64":
|
||||
case "date":
|
||||
case "procStream":
|
||||
return "double";
|
||||
case "bool":
|
||||
|
||||
@@ -105,13 +105,7 @@ export class StackCaptures {
|
||||
// sites, including duplicated finally bodies and versioned loops.
|
||||
if (!this.locals.has(id) || this.remaining < 48) return null;
|
||||
const ref = isRefCounted(type);
|
||||
if (
|
||||
!ref &&
|
||||
type.kind !== "f64" &&
|
||||
type.kind !== "date" &&
|
||||
type.kind !== "procStream" &&
|
||||
type.kind !== "bool"
|
||||
)
|
||||
if (!ref && type.kind !== "f64" && type.kind !== "procStream" && type.kind !== "bool")
|
||||
return null;
|
||||
this.remaining -= 48;
|
||||
const B = host.B;
|
||||
|
||||
@@ -237,6 +237,10 @@ export class LlWalkers {
|
||||
this.host.declare(`declare void @scr_jb_put_json_str(ptr, ptr)`);
|
||||
B.line(`call void @scr_jb_put_json_str(ptr %b, ptr %v)`);
|
||||
break;
|
||||
case "date":
|
||||
this.host.declare(`declare void @scr_jb_put_date(ptr, ptr)`);
|
||||
B.line(`call void @scr_jb_put_date(ptr %b, ptr %v) ; toJSON(): ISO text, null if invalid`);
|
||||
break;
|
||||
case "dyn":
|
||||
// A checked-dynamic value serializes itself (runtime walker).
|
||||
// Bare stringify of dyn stays frontend-fenced, and overflow
|
||||
@@ -1006,6 +1010,17 @@ export class LlWalkers {
|
||||
case "string":
|
||||
this.putScrStr(B, buf, this.unionPeek(B, u, unionId));
|
||||
break;
|
||||
case "date": {
|
||||
host.declare(`declare ptr @scr_date_object_to_string(ptr)`);
|
||||
host.declare(`declare void @scr_str_release(ptr)`);
|
||||
const text = B.tmp();
|
||||
B.line(
|
||||
`${text} = call ptr @scr_date_object_to_string(ptr ${this.unionPeek(B, u, unionId)})`,
|
||||
);
|
||||
this.putScrStr(B, buf, text);
|
||||
B.line(`call void @scr_str_release(ptr ${text})`);
|
||||
break;
|
||||
}
|
||||
case "f64": {
|
||||
host.declare(`declare double @scr_union_get_f64(ptr)`);
|
||||
host.declare(`declare void @scr_jb_put_number(ptr, double)`);
|
||||
|
||||
@@ -6,19 +6,29 @@ export const NATIVE_CODEGEN_LLVM_VERSION = "22.1.8";
|
||||
|
||||
export type NativeCodegenOutputKind = "asm" | "obj";
|
||||
|
||||
/** LLVM text per partition, and the most partitions one executable uses. */
|
||||
/** LLVM text that stays one module, LLVM text per partition, the fewest
|
||||
* partitions (MIN_PROGRAM_PARTITIONS, of at least MIN_PARTITION_BYTES each)
|
||||
* a divided module gets, and the most one executable uses. */
|
||||
const SINGLE_PARTITION_BYTES = 1024 * 1024;
|
||||
const PROGRAM_PARTITION_BYTES = 4 * 1024 * 1024;
|
||||
const MIN_PARTITION_BYTES = 512 * 1024;
|
||||
const MIN_PROGRAM_PARTITIONS = 8;
|
||||
const MAX_PROGRAM_PARTITIONS = 64;
|
||||
|
||||
/** Executable objects this large are divided into partitions that the helper
|
||||
* simplifies, optimizes and compiles concurrently, importing small functions
|
||||
* across partitions for inlining, and reuses individually across builds. The
|
||||
/** Executable objects larger than SINGLE_PARTITION_BYTES are divided into
|
||||
* partitions that the helper simplifies, optimizes and compiles concurrently,
|
||||
* importing small functions across partitions for inlining, and reuses
|
||||
* individually across builds. A divided module gets at least
|
||||
* MIN_PROGRAM_PARTITIONS so that mid-size programs use several cores. The
|
||||
* count is a power of two that depends only on the module's size, on every
|
||||
* target, so artifacts never vary with the host and ordinary edits keep every
|
||||
* function in its partition. */
|
||||
export function nativeProgramPartitions(llvmBytes: number): number {
|
||||
const required = Math.ceil(llvmBytes / PROGRAM_PARTITION_BYTES);
|
||||
if (required < 2) return 1;
|
||||
if (llvmBytes <= SINGLE_PARTITION_BYTES) return 1;
|
||||
const required = Math.max(
|
||||
Math.ceil(llvmBytes / PROGRAM_PARTITION_BYTES),
|
||||
Math.min(MIN_PROGRAM_PARTITIONS, Math.ceil(llvmBytes / MIN_PARTITION_BYTES)),
|
||||
);
|
||||
return Math.min(MAX_PROGRAM_PARTITIONS, 2 ** Math.ceil(Math.log2(required)));
|
||||
}
|
||||
|
||||
|
||||
@@ -352,11 +352,13 @@ test("sanitized native artifacts fail before helper resolution", async () => {
|
||||
|
||||
test("program partitions depend only on module size", () => {
|
||||
const megabyte = 1024 * 1024;
|
||||
expect(nativeProgramPartitions(4 * megabyte)).toBe(1);
|
||||
expect(nativeProgramPartitions(4 * megabyte + 1)).toBe(2);
|
||||
expect(nativeProgramPartitions(megabyte)).toBe(1);
|
||||
expect(nativeProgramPartitions(megabyte + 1)).toBe(4);
|
||||
expect(nativeProgramPartitions(2 * megabyte)).toBe(4);
|
||||
expect(nativeProgramPartitions(2 * megabyte + 1)).toBe(8);
|
||||
expect(nativeProgramPartitions(32 * megabyte)).toBe(8);
|
||||
// Powers of two keep every function in its partition across small edits.
|
||||
expect(nativeProgramPartitions(13 * megabyte)).toBe(4);
|
||||
expect(nativeProgramPartitions(16 * megabyte)).toBe(4);
|
||||
expect(nativeProgramPartitions(32 * megabyte + 1)).toBe(16);
|
||||
expect(nativeProgramPartitions(100 * megabyte)).toBe(32);
|
||||
expect(nativeProgramPartitions(1024 * megabyte)).toBe(64);
|
||||
expect(nativePartitionPaths("/build.d/program.o", 3)).toEqual([
|
||||
|
||||
@@ -81,6 +81,9 @@ export interface NativeLinkFeatures {
|
||||
dnsPromises: boolean;
|
||||
/** dgram sockets as untyped values: scr_dgram_dyn.c. */
|
||||
dgramDyn: boolean;
|
||||
/** TextEncoder and TextDecoder records as untyped values:
|
||||
* scr_text_codec_dyn.c. */
|
||||
textCodecDyn: boolean;
|
||||
watch: boolean;
|
||||
foreignFfi: boolean;
|
||||
nodeTest: boolean;
|
||||
|
||||
@@ -220,6 +220,10 @@ export interface CcOptions {
|
||||
* scr_dgram_dyn.c over the dgram unit, so typed dgram binaries keep
|
||||
* their exact link line. */
|
||||
dgramDyn?: boolean;
|
||||
/** A TextEncoder or TextDecoder record converts to or from an untyped
|
||||
* value: compiles scr_text_codec_dyn.c, so typed codec binaries keep
|
||||
* their exact link line. */
|
||||
textCodecDyn?: boolean;
|
||||
|
||||
/** The program uses fs.watch (moduleUsesFsWatch on the IR): compiles
|
||||
* scr_watch.c into the binary — the net gating precedent, so watch-free
|
||||
|
||||
@@ -302,6 +302,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unit((u) => u.opts.dgram, "scr_dgram.c"),
|
||||
unit((u) => u.opts.dgram && u.opts.dnsPromises, "scr_dns_promises.c"),
|
||||
unit((u) => u.opts.dgram && u.opts.dgramDyn, "scr_dgram_dyn.c"),
|
||||
unit((u) => u.opts.textCodecDyn, "scr_text_codec_dyn.c"),
|
||||
unit((u) => u.opts.watch, "scr_watch.c"),
|
||||
unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"),
|
||||
unit((u) => u.opts.nodeTest, "scr_test.c"),
|
||||
|
||||
@@ -118,6 +118,7 @@ const features = {
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -54,6 +54,7 @@ const BASE: NativeLinkFeatures = {
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -61,6 +61,7 @@ export async function warmNativeCaches(
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -656,7 +656,7 @@ export function genericSignatureTypeDiag(typeText: string, loc: SrcLoc): ScrDiag
|
||||
export function indexSignatureTypeDiag(typeText: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2006",
|
||||
message: `values of type '${typeText}' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Promises, or 'unknown')`,
|
||||
message: `values of type '${typeText}' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Dates, Promises, or 'unknown')`,
|
||||
loc,
|
||||
milestone: "M2",
|
||||
hint: "string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering",
|
||||
|
||||
@@ -58,6 +58,7 @@ const native: EarlyExecutableNativeFeatures = {
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -92,6 +92,7 @@ export interface EarlyExecutableNativeFeatures {
|
||||
dgram: boolean;
|
||||
dnsPromises: boolean;
|
||||
dgramDyn: boolean;
|
||||
textCodecDyn: boolean;
|
||||
watch: boolean;
|
||||
foreignFfi: boolean;
|
||||
nodeTest: boolean;
|
||||
@@ -235,6 +236,7 @@ const BOOLEAN_NATIVE_KEYS = [
|
||||
"dgram",
|
||||
"dnsPromises",
|
||||
"dgramDyn",
|
||||
"textCodecDyn",
|
||||
"watch",
|
||||
"foreignFfi",
|
||||
"nodeTest",
|
||||
|
||||
@@ -185,8 +185,24 @@ class TypePrinter {
|
||||
readonly declarations: string[] = [];
|
||||
private readonly inProgress = new Map<number, { name: string | null }>();
|
||||
|
||||
/** While set, printing registers no recursive alias (projectable()). */
|
||||
private dry = false;
|
||||
|
||||
constructor(private readonly checker: ts.TypeChecker) {}
|
||||
|
||||
/** Whether a type projects, without naming it in the generated file. */
|
||||
projectable(type: ts.Type): boolean {
|
||||
if (type.flags & ANYISH) return false;
|
||||
const was = this.dry;
|
||||
this.dry = true;
|
||||
try {
|
||||
const text = this.print(type, 0);
|
||||
return text !== null && text !== "unknown";
|
||||
} finally {
|
||||
this.dry = was;
|
||||
}
|
||||
}
|
||||
|
||||
/** The alias naming a projectable type, or null. Top-level any/unknown
|
||||
* never projects. */
|
||||
project(type: ts.Type): string | null {
|
||||
@@ -212,7 +228,12 @@ class TypePrinter {
|
||||
if (f & ts.TypeFlags.Null) return "null";
|
||||
if (f & ts.TypeFlags.Undefined) return "undefined";
|
||||
if (f & ts.TypeFlags.EnumLiteral || f & ts.TypeFlags.Enum) return null;
|
||||
if (f & ts.TypeFlags.BooleanLiteral) return type.intrinsicName === "true" ? "true" : "false";
|
||||
// A boolean literal type carries its value (`intrinsicName` is not
|
||||
// set for it by every checker facade).
|
||||
if (f & ts.TypeFlags.BooleanLiteral)
|
||||
return (type as ts.BooleanLiteralType).value === true || type.intrinsicName === "true"
|
||||
? "true"
|
||||
: "false";
|
||||
if (f & ts.TypeFlags.StringLiteral) return JSON.stringify(String(type.value));
|
||||
if (f & ts.TypeFlags.NumberLiteral) {
|
||||
const v = Number(type.value);
|
||||
@@ -341,7 +362,7 @@ class TypePrinter {
|
||||
if (props.length > 0 && readonlyCount === props.length) return null;
|
||||
const text = `{ ${members.join("; ")} }`;
|
||||
if (frame.name !== null) {
|
||||
this.declarations.push(`type ${frame.name} = ${text};`);
|
||||
if (!this.dry) this.declarations.push(`type ${frame.name} = ${text};`);
|
||||
return frame.name;
|
||||
}
|
||||
return text;
|
||||
@@ -517,9 +538,17 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
|
||||
const declSf = input.declProgram.getSourceFile(file);
|
||||
if (jsSf === undefined || declSf === undefined || jsSf.text !== declSf.text) continue;
|
||||
const fileEdits: Edit[] = [];
|
||||
/** Candidate expressions (by js node) with their alias. */
|
||||
const exprCandidates = new Map<ts.Node, string>();
|
||||
/** Candidate expressions (by js node) with their declared type, named
|
||||
* only for the casts that survive suppression. */
|
||||
const exprCandidates = new Map<ts.Node, ts.Type>();
|
||||
const exprOrder: ts.Node[] = [];
|
||||
/** Reads whose projectable receiver the JavaScript world types `any`:
|
||||
* kept only when nothing types the receiver. */
|
||||
const deferred = new Set<ts.Node>();
|
||||
/** Package calls whose declared type does not project. */
|
||||
const unprojected = new Set<ts.Node>();
|
||||
/** The declaration-world twin of every walked node. */
|
||||
const twins = new Map<ts.Node, ts.Node>();
|
||||
|
||||
const considerType = (jsNode: ts.Node, declNode: ts.Node): boolean => {
|
||||
if (
|
||||
@@ -603,8 +632,18 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
|
||||
} else if (ts.isPropertyAccessExpression(d) || ts.isElementAccessExpression(d)) {
|
||||
const recvDecl = typeAt(declChecker, d.expression);
|
||||
const recvJs = typeAt(jsChecker, (jsNode as ts.PropertyAccessExpression).expression);
|
||||
const recvProjectable = recvDecl !== null && printer.project(recvDecl) !== null;
|
||||
const recvProjectable = recvDecl !== null && printer.projectable(recvDecl);
|
||||
produced = !recvProjectable || (recvJs !== null && !isAnyish(recvJs));
|
||||
// A projectable receiver the JavaScript world types `any` is
|
||||
// usually typed by another candidate. One that is a package value
|
||||
// nothing projects stays `any`, such as a safeParse result narrowed
|
||||
// to its success arm (the whole result is not data: its error arm
|
||||
// is a class): the read crosses here instead, if the suppression
|
||||
// below confirms the receiver stays untyped.
|
||||
if (!produced) {
|
||||
produced = true;
|
||||
deferred.add(jsNode);
|
||||
}
|
||||
} else if (ts.isAwaitExpression(d)) {
|
||||
const inner = unwrapTransparent(d.expression);
|
||||
produced =
|
||||
@@ -633,6 +672,12 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
|
||||
declaredInPackage(resolveAlias(raw));
|
||||
}
|
||||
if (!produced) return;
|
||||
if (isCall && !deferred.has(jsNode)) {
|
||||
// Recorded before the contextual-type test below: a package value
|
||||
// whose type does not project stays `any` wherever it flows.
|
||||
const t = typeAt(declChecker, d);
|
||||
if (t === null || !printer.projectable(t)) unprojected.add(jsNode);
|
||||
}
|
||||
// A consumer that takes anything needs no conversion.
|
||||
try {
|
||||
const ctx = declChecker.getContextualType(d);
|
||||
@@ -641,10 +686,8 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
|
||||
// no contextual type
|
||||
}
|
||||
const declType = typeAt(declChecker, d);
|
||||
if (declType === null) return;
|
||||
const alias = printer.project(declType);
|
||||
if (alias === null) return;
|
||||
exprCandidates.set(jsNode, alias);
|
||||
if (declType === null || !printer.projectable(declType)) return;
|
||||
exprCandidates.set(jsNode, declType);
|
||||
exprOrder.push(jsNode);
|
||||
};
|
||||
|
||||
@@ -703,6 +746,7 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
|
||||
|
||||
const walk = (jsNode: ts.Node, declNode: ts.Node): void => {
|
||||
if (jsNode.kind !== declNode.kind) return;
|
||||
twins.set(jsNode, declNode);
|
||||
if (ts.isTypeNode(jsNode) || ts.isExpressionWithTypeArguments(jsNode)) {
|
||||
if (considerType(jsNode, declNode)) return;
|
||||
} else {
|
||||
@@ -749,21 +793,67 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
|
||||
result = willBeTyped(decl.initializer, depth + 1);
|
||||
else if (ts.isParameter(decl) && annotated.has(decl)) result = true;
|
||||
else if (ts.isBindingElement(decl)) result = false;
|
||||
else if (
|
||||
(ts.isVariableDeclaration(decl) || ts.isParameter(decl)) &&
|
||||
decl.type !== undefined
|
||||
)
|
||||
result = annotationWillBeTyped(decl.type);
|
||||
}
|
||||
symbolTyped.set(sym, result);
|
||||
return result;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/** An annotation in this file whose declaration-world type projects:
|
||||
* after the rewrite it names the projected structure (a package type
|
||||
* is replaced by its alias, a user alias of one has its right-hand
|
||||
* side replaced). */
|
||||
const annotationWillBeTyped = (type: ts.TypeNode): boolean => {
|
||||
const twin = twins.get(type);
|
||||
if (twin === undefined) return false;
|
||||
let declType: ts.Type | null;
|
||||
try {
|
||||
declType = declChecker.getTypeFromTypeNode(twin as ts.TypeNode);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return declType !== null && printer.projectable(declType);
|
||||
};
|
||||
const receiverOf = (e: ts.Node): ts.Node | null => {
|
||||
if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) return e.expression;
|
||||
if (ts.isIdentifier(e)) return null;
|
||||
return null;
|
||||
};
|
||||
const kept: ts.Node[] = [];
|
||||
/** A receiver that stays `any` after the rewrite: a package call whose
|
||||
* type does not project, or a user const bound to one. */
|
||||
const staysUntyped = (e: ts.Node): boolean => {
|
||||
const u = unwrapTransparent(e as ts.Expression);
|
||||
if (unprojected.has(u)) return true;
|
||||
if (!ts.isIdentifier(u)) return false;
|
||||
let sym: ts.Symbol | undefined;
|
||||
try {
|
||||
sym = jsChecker.getSymbolAtLocation(u);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const decls = sym?.declarations ?? [];
|
||||
if (decls.length !== 1) return false;
|
||||
const decl = decls[0]!;
|
||||
return (
|
||||
ts.isVariableDeclaration(decl) &&
|
||||
decl.type === undefined &&
|
||||
decl.initializer !== undefined &&
|
||||
decl.parent !== undefined &&
|
||||
ts.isVariableDeclarationList(decl.parent) &&
|
||||
(decl.parent.flags & ts.NodeFlags.Const) !== 0 &&
|
||||
unprojected.has(unwrapTransparent(decl.initializer))
|
||||
);
|
||||
};
|
||||
for (const e of exprOrder) {
|
||||
const recv = receiverOf(e);
|
||||
if (recv !== null && willBeTyped(recv)) continue;
|
||||
if (deferred.has(e) && (recv === null || !staysUntyped(recv))) continue;
|
||||
// An identifier candidate that is a user binding never gets here
|
||||
// (only package imports are candidates).
|
||||
kept.push(e);
|
||||
@@ -800,7 +890,8 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
|
||||
)
|
||||
continue;
|
||||
}
|
||||
const alias = exprCandidates.get(e)!;
|
||||
const alias = printer.project(exprCandidates.get(e)!);
|
||||
if (alias === null) continue;
|
||||
const start = ts.tokenStartOfNode(e);
|
||||
const stmt = e.parent;
|
||||
let prefix = "(";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { isBuiltinClassInstance } from "./receiver-types.js";
|
||||
import { dynUndefinedExpr } from "../../../ir/build.js";
|
||||
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 {
|
||||
BOOL,
|
||||
DYN,
|
||||
@@ -184,16 +184,89 @@ export function lowerDiagnosticsChannelMethodCall(
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether `node` names the AsyncLocalStorage class itself. */
|
||||
function isAsyncLocalStorageClass(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
if (!ts.isIdentifier(node)) return false;
|
||||
const symbol = lowerer.resolveValueSymbol(node);
|
||||
return (
|
||||
symbol !== null &&
|
||||
lowerer.isStdlibSymbol(symbol) &&
|
||||
ts.symbolName(symbol) === "AsyncLocalStorage"
|
||||
);
|
||||
}
|
||||
|
||||
/** The static AsyncLocalStorage members: bind(fn) answers fn bound to the
|
||||
* active context (its own type for a typed function; a dyn function in
|
||||
* JavaScript files, where the bound function's name and receiver are
|
||||
* observable, and for dyn values); snapshot() answers a dyn function
|
||||
* (cb, ...args) that runs cb in the context active at the snapshot. */
|
||||
function lowerAsyncLocalStorageStaticCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr {
|
||||
const name = access.name.text;
|
||||
const loc = locOf(call);
|
||||
const always: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc };
|
||||
if (name === "bind" && call.arguments.nodes.length === 1) {
|
||||
const arg = call.arguments.nodes[0]!;
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(call))) {
|
||||
const fn = lowerer.lowerExpr(arg);
|
||||
if (fn.type.kind === "func")
|
||||
return { kind: "libCall", fn: "als.bindCallback", args: [fn, always], type: fn.type, loc };
|
||||
}
|
||||
return asyncLocalStorageResult(lowerer, call, {
|
||||
kind: "libCall",
|
||||
fn: "als.bindCallbackDyn",
|
||||
args: [lowerDiagnosticsMessage(lowerer, arg), always],
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
if (name === "snapshot" && call.arguments.nodes.length === 0)
|
||||
return { kind: "libCall", fn: "als.snapshot", args: [], type: DYN, loc };
|
||||
lowerer.noLowering(
|
||||
`AsyncLocalStorage.${name}`,
|
||||
call,
|
||||
"AsyncLocalStorage.bind(fn) and AsyncLocalStorage.snapshot() are the supported static members",
|
||||
lowerer.checker.getSymbolAtLocation(access.name),
|
||||
);
|
||||
}
|
||||
|
||||
/** new AsyncLocalStorage(options): the options object as a dyn value
|
||||
* (plain property literals build it member by member). */
|
||||
export function lowerAsyncLocalStorageOptions(lowerer: Lowerer, node: ts.Expression): IrExpr {
|
||||
return lowerTracingHandlers(lowerer, node);
|
||||
}
|
||||
|
||||
/** `.name` on AsyncLocalStorage receivers: the constructor's name option
|
||||
* (als.name), converted to the property's type. Null otherwise. */
|
||||
export function lowerAsyncLocalStorageProperty(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (access.questionDotToken || access.name.text !== "name") return null;
|
||||
if (!isBuiltinClassInstance(lowerer, access.expression, "AsyncLocalStorage")) return null;
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
const loc = locOf(access);
|
||||
const receiver = lowerer.lowerExprExpecting(access.expression, F64);
|
||||
const value: IrExpr = { kind: "libCall", fn: "als.name", args: [receiver], type: DYN, loc };
|
||||
const expected = lowerer.mapTypeOf(lowerer.typeOf(access));
|
||||
return !expected || expected.kind === "dyn" ? value : lowerer.coerceInto(access, value, expected);
|
||||
}
|
||||
|
||||
/** Method calls on AsyncLocalStorage receivers: run(store, fn, ...args),
|
||||
* exit(fn, ...args), getStore(), enterWith(store), disable() — over the
|
||||
* f64 store handle (als.* libCalls; values cross as dyn values). Null
|
||||
* for non-ALS receivers. */
|
||||
* f64 store handle (als.* libCalls; values cross as dyn values) — and the
|
||||
* static bind/snapshot members. Null for other receivers. */
|
||||
export function lowerAsyncLocalStorageMethodCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (call.questionDotToken || access.questionDotToken) return null;
|
||||
if (isAsyncLocalStorageClass(lowerer, access.expression))
|
||||
return lowerAsyncLocalStorageStaticCall(lowerer, call, access);
|
||||
if (!isBuiltinClassInstance(lowerer, access.expression, "AsyncLocalStorage")) return null;
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
const name = access.name.text;
|
||||
|
||||
@@ -1454,6 +1454,7 @@ function lowerPkiCall(
|
||||
],
|
||||
sign: ["crypto.signOneshot", 3, 3, "sign(algorithm, data, key)"],
|
||||
verify: ["crypto.verifyOneshot", 4, 4, "verify(algorithm, data, key, signature)"],
|
||||
diffieHellman: ["crypto.diffieHellman", 1, 2, "diffieHellman(options[, callback])"],
|
||||
createSign: ["crypto.createSign", 1, 1, "createSign(algorithm)"],
|
||||
createVerify: ["crypto.createVerify", 1, 1, "createVerify(algorithm)"],
|
||||
};
|
||||
@@ -1474,6 +1475,7 @@ function lowerPkiCall(
|
||||
return { kind: "libCall", fn, args: values, type: VOID, loc };
|
||||
const call: IrExpr = { kind: "libCall", fn, args: values, type: DYN, loc };
|
||||
const result = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (result === null || result.kind === "dyn") return call;
|
||||
// diffieHellman's callback form answers undefined.
|
||||
if (result === null || result.kind === "dyn" || result.kind === "void") return call;
|
||||
return lowerer.coerceInto(expr, call, result);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,13 @@ export function lowerDateCall(
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (call.questionDotToken || access.questionDotToken) return null;
|
||||
// An optional chain lowers the call again once it has narrowed the
|
||||
// receiver (Lowerer.chainHandled).
|
||||
if (
|
||||
(call.questionDotToken && !lowerer.chainHandled.has(call)) ||
|
||||
(access.questionDotToken && !lowerer.chainHandled.has(access))
|
||||
)
|
||||
return null;
|
||||
const loc = locOf(call);
|
||||
const member = lowerer.stdlibGlobalMember(access, "Date");
|
||||
if (member === "now" || member === "parse" || member === "UTC")
|
||||
|
||||
@@ -265,7 +265,7 @@ export function lowerFsPromisesOpenCall(
|
||||
"use open(path[, stringFlags[, numericMode]])",
|
||||
);
|
||||
}
|
||||
const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
|
||||
const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!);
|
||||
const defaultFlags: IrExpr = { kind: "strLit", value: "r", type: STRING, loc };
|
||||
const defaultMode: IrExpr = { kind: "numLit", value: 0o666, type: F64, loc };
|
||||
const flags = expr.arguments.nodes[1]
|
||||
@@ -705,7 +705,7 @@ export function lowerFsPromisesEncodedRead(
|
||||
)
|
||||
return null;
|
||||
}
|
||||
const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
|
||||
const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!);
|
||||
const encoding: IrExpr =
|
||||
optionsType.kind === "string"
|
||||
? lowerer.lowerExprExpecting(options, STRING)
|
||||
@@ -853,7 +853,7 @@ export function lowerFsPromisesMkdirCall(
|
||||
);
|
||||
}
|
||||
const { recursive, modeNode } = options;
|
||||
const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
|
||||
const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!);
|
||||
const mode: IrExpr | null = modeNode ? lowerer.lowerExprExpecting(modeNode, F64) : null;
|
||||
const type: IrType = { kind: "promise", inner: VOID };
|
||||
if (!recursive) {
|
||||
|
||||
@@ -5,30 +5,40 @@
|
||||
* conversion (url.fileURLToPathUrl). The later arguments through the last
|
||||
* one with side effects are evaluated into hidden locals before the
|
||||
* conversion, in source order, so they run even when it throws and each
|
||||
* reads what it reads in Node. The conversion keeps Node's error order when every
|
||||
* earlier path is a URL too and each argument Node validates before the
|
||||
* path is absent or a literal that passes that validation; other URL
|
||||
* forms fence with SC2020, as do the fs.promises functions, whose
|
||||
* conversion errors are rejections rather than throws, and existsSync,
|
||||
* which answers false for a URL it cannot convert. */
|
||||
* reads what it reads in Node. Before the conversion, an earlier string
|
||||
* path gets its null-byte check (copyFileSync, renameSync) and a runtime
|
||||
* readFileSync encoding its name check, which Node runs first. existsSync
|
||||
* and the fs.promises functions convert to a value instead
|
||||
* (url.fileURLToPathValue) and answer false or a rejected promise for a
|
||||
* conversion error. An argument Node validates before the path must
|
||||
* otherwise be absent or a literal that passes that validation; other
|
||||
* forms fence with SC2020. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { sideEffectFreeOptionValue } from "../surfaces.js";
|
||||
import { stripTypeWrappers } from "../typed-arguments.js";
|
||||
import { type IrExpr, type IrStmt, STRING, URL_T } from "../../../ir/ir.js";
|
||||
import { boolLit, varRef } from "../../../ir/build.js";
|
||||
import { BOOL, DYN, type IrExpr, type IrStmt, STRING, URL_T, VOID } from "../../../ir/ir.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
|
||||
/** The fs functions whose path parameters accept a URL: the path
|
||||
* parameter indexes, and whether Node validates the paths before every
|
||||
* other argument (otherwise the options come first). */
|
||||
const FS_URL_PATHS: Readonly<
|
||||
Record<string, { readonly paths: readonly number[]; readonly pathsFirst: boolean }>
|
||||
> = {
|
||||
interface FsPathRow {
|
||||
/** The path parameter indexes. */
|
||||
readonly paths: readonly number[];
|
||||
/** Whether Node validates the paths before every other argument
|
||||
* (otherwise the options come first). */
|
||||
readonly pathsFirst: boolean;
|
||||
/** The path parameters' names in Node's errors, when there are two. */
|
||||
readonly names?: readonly string[];
|
||||
}
|
||||
|
||||
/** The fs functions whose path parameters accept a URL. */
|
||||
const FS_URL_PATHS: Readonly<Record<string, FsPathRow>> = {
|
||||
accessSync: { paths: [0], pathsFirst: true },
|
||||
appendFileSync: { paths: [0], pathsFirst: false },
|
||||
chmodSync: { paths: [0], pathsFirst: true },
|
||||
chownSync: { paths: [0], pathsFirst: true },
|
||||
copyFileSync: { paths: [0, 1], pathsFirst: true },
|
||||
copyFileSync: { paths: [0, 1], pathsFirst: true, names: ["src", "dest"] },
|
||||
lchownSync: { paths: [0], pathsFirst: true },
|
||||
lstatSync: { paths: [0], pathsFirst: true },
|
||||
mkdirSync: { paths: [0], pathsFirst: false },
|
||||
@@ -39,7 +49,7 @@ const FS_URL_PATHS: Readonly<
|
||||
readdirSync: { paths: [0], pathsFirst: false },
|
||||
realpathSync: { paths: [0], pathsFirst: false },
|
||||
"realpathSync.native": { paths: [0], pathsFirst: false },
|
||||
renameSync: { paths: [0, 1], pathsFirst: true },
|
||||
renameSync: { paths: [0, 1], pathsFirst: true, names: ["oldPath", "newPath"] },
|
||||
rmSync: { paths: [0], pathsFirst: false },
|
||||
rmdirSync: { paths: [0], pathsFirst: true },
|
||||
statSync: { paths: [0], pathsFirst: true },
|
||||
@@ -48,43 +58,66 @@ const FS_URL_PATHS: Readonly<
|
||||
writeFileSync: { paths: [0], pathsFirst: false },
|
||||
};
|
||||
|
||||
/** The fs.promises functions with path parameters, by path indexes. */
|
||||
const FSP_PATHS: Readonly<Record<string, readonly number[]>> = {
|
||||
access: [0],
|
||||
appendFile: [0],
|
||||
chmod: [0],
|
||||
chown: [0],
|
||||
copyFile: [0, 1],
|
||||
lchown: [0],
|
||||
lstat: [0],
|
||||
mkdir: [0],
|
||||
mkdtemp: [0],
|
||||
open: [0],
|
||||
opendir: [0],
|
||||
readFile: [0],
|
||||
readdir: [0],
|
||||
realpath: [0],
|
||||
rename: [0, 1],
|
||||
rm: [0],
|
||||
rmdir: [0],
|
||||
stat: [0],
|
||||
truncate: [0],
|
||||
unlink: [0],
|
||||
writeFile: [0],
|
||||
/** The fs.promises functions with path parameters. */
|
||||
const FSP_PATHS: Readonly<Record<string, FsPathRow>> = {
|
||||
access: { paths: [0], pathsFirst: true },
|
||||
appendFile: { paths: [0], pathsFirst: false },
|
||||
chmod: { paths: [0], pathsFirst: true },
|
||||
chown: { paths: [0], pathsFirst: true },
|
||||
copyFile: { paths: [0, 1], pathsFirst: true },
|
||||
lchown: { paths: [0], pathsFirst: true },
|
||||
lstat: { paths: [0], pathsFirst: true },
|
||||
mkdir: { paths: [0], pathsFirst: false },
|
||||
mkdtemp: { paths: [0], pathsFirst: false },
|
||||
open: { paths: [0], pathsFirst: true },
|
||||
opendir: { paths: [0], pathsFirst: true },
|
||||
readFile: { paths: [0], pathsFirst: false },
|
||||
readdir: { paths: [0], pathsFirst: false },
|
||||
realpath: { paths: [0], pathsFirst: false },
|
||||
rename: { paths: [0, 1], pathsFirst: true },
|
||||
rm: { paths: [0], pathsFirst: true },
|
||||
rmdir: { paths: [0], pathsFirst: true },
|
||||
stat: { paths: [0], pathsFirst: true },
|
||||
truncate: { paths: [0], pathsFirst: true },
|
||||
unlink: { paths: [0], pathsFirst: true },
|
||||
writeFile: { paths: [0], pathsFirst: false },
|
||||
};
|
||||
|
||||
/** What a path argument of an fs call lowers to (Lowerer.fsPathPlans). */
|
||||
export type FsPathPlan =
|
||||
/** The first URL path: the later arguments with effects, the earlier
|
||||
* string paths to check, and a runtime encoding to check, all before the
|
||||
* conversion. */
|
||||
| {
|
||||
kind: "convert";
|
||||
staged: ts.Expression[];
|
||||
checks: { path: ts.Expression; name: string }[];
|
||||
encoding: ts.Expression | null;
|
||||
}
|
||||
/** A string path that a later URL path checks: the local that holds it,
|
||||
* once lowered. */
|
||||
| { kind: "save"; local: string | null }
|
||||
/** A URL path whose conversion error becomes false or a rejection: the
|
||||
* local that holds the conversion's value. */
|
||||
| { kind: "value"; local: string };
|
||||
|
||||
function isUrlArgument(lowerer: Lowerer, node: ts.Expression | undefined): boolean {
|
||||
return node !== undefined && lowerer.mapTypeOf(lowerer.typeOf(node))?.kind === "url";
|
||||
}
|
||||
|
||||
function stripParens(node: ts.Expression): ts.Expression {
|
||||
let e = node;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
return e;
|
||||
}
|
||||
|
||||
/** A literal that Node's option validation accepts before it reads the
|
||||
* path: a string (the checker admits only the declared encodings, flags
|
||||
* and type names), a boolean, an integer in [0, 2^31 - 1] (every numeric
|
||||
* option of these functions accepts that range), undefined or null, or an
|
||||
* object literal of those. */
|
||||
function preValidatedLiteral(node: ts.Expression): boolean {
|
||||
let e = node;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
const e = stripParens(node);
|
||||
if (ts.isStringLiteralLike(e)) return true;
|
||||
if (
|
||||
e.kind === ts.SyntaxKind.TrueKeyword ||
|
||||
@@ -108,39 +141,59 @@ function preValidatedLiteral(node: ts.Expression): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** SC2020 when the call passes a URL path in a form whose conversion
|
||||
* order would differ from Node's (see the file comment). */
|
||||
function checkUrlPaths(
|
||||
const HINT = "convert the URL first: fileURLToPath(url) from node:url";
|
||||
|
||||
/** SC2020 for an argument Node validates before the paths that is neither
|
||||
* absent nor a literal passing that validation, other than a string
|
||||
* readFileSync encoding (returned: it is checked before the conversion).
|
||||
* writeFileSync/appendFileSync's data is checked after the path. */
|
||||
function optionsBeforePaths(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
module: string,
|
||||
args: readonly ts.Expression[],
|
||||
member: string,
|
||||
): void {
|
||||
const args = call.arguments.nodes;
|
||||
const name = module === "fs/promises" ? `fs.promises.${member}` : member;
|
||||
const hint = "convert the URL first: fileURLToPath(url) from node:url";
|
||||
if (module === "fs/promises") {
|
||||
if ((FSP_PATHS[member] ?? []).some((i) => isUrlArgument(lowerer, args[i])))
|
||||
lowerer.noLowering(`${name} with a URL path`, call, hint);
|
||||
return;
|
||||
row: FsPathRow,
|
||||
name: string,
|
||||
): ts.Expression | null {
|
||||
if (row.pathsFirst) return null;
|
||||
const data = /^(writeFile|appendFile)(Sync)?$/.test(member) ? 1 : -1;
|
||||
let encoding: ts.Expression | null = null;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i]!;
|
||||
if (row.paths.includes(i) || i === data || preValidatedLiteral(arg)) continue;
|
||||
if (
|
||||
member === "readFileSync" &&
|
||||
i === 1 &&
|
||||
lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "string"
|
||||
) {
|
||||
encoding = stripTypeWrappers(arg);
|
||||
continue;
|
||||
}
|
||||
lowerer.noLowering(`${name} with a URL path and options that are not literals`, arg, HINT);
|
||||
}
|
||||
if (member === "existsSync" && isUrlArgument(lowerer, args[0]))
|
||||
lowerer.noLowering(`${name} with a URL path`, call, hint);
|
||||
const row = FS_URL_PATHS[member];
|
||||
if (!row) return;
|
||||
const first = row.paths.find((i) => isUrlArgument(lowerer, args[i]));
|
||||
if (first === undefined) return;
|
||||
if (args.some(ts.isSpreadElement))
|
||||
lowerer.noLowering(`${name} with spread arguments and a URL path`, call);
|
||||
// The runtime checks a string path after the conversion, so a string
|
||||
// path Node checks first cannot precede the URL.
|
||||
if (row.paths.some((i) => i < first && args[i] !== undefined))
|
||||
lowerer.noLowering(`${name} with a string path before a URL path`, call, hint);
|
||||
// Every argument from the URL through the last one with effects is
|
||||
// evaluated ahead of the conversion, in source order: a later argument's
|
||||
// effect (an assignment, say) must not change what an earlier one read.
|
||||
return encoding;
|
||||
}
|
||||
|
||||
/** The arguments after the URL path that are evaluated ahead of the
|
||||
* conversion: every one through the last with effects or that `force`
|
||||
* names, in source order. An object or array literal among them is
|
||||
* SC2020: its members are lowered one by one, so it cannot be evaluated
|
||||
* ahead of the conversion as one value. */
|
||||
function stagedAfter(
|
||||
lowerer: Lowerer,
|
||||
args: readonly ts.Expression[],
|
||||
first: number,
|
||||
name: string,
|
||||
force: (e: ts.Expression, i: number) => boolean,
|
||||
): ts.Expression[] {
|
||||
// Every argument from the URL through the last one with effects (or that
|
||||
// the caller forces) is evaluated ahead of the conversion, in source
|
||||
// order: a later argument's effect (an assignment, say) must not change
|
||||
// what an earlier one read.
|
||||
let last = first;
|
||||
for (let i = first + 1; i < args.length; i++) if (!sideEffectFreeOptionValue(args[i]!)) last = i;
|
||||
for (let i = first + 1; i < args.length; i++) {
|
||||
const e = stripTypeWrappers(args[i]!);
|
||||
if (!sideEffectFreeOptionValue(e) || force(e, i)) last = i;
|
||||
}
|
||||
const staged: ts.Expression[] = [];
|
||||
for (let i = first + 1; i <= last; i++) {
|
||||
// Keyed without type-only wrappers (`satisfies`, `as`, `!`), so a
|
||||
@@ -154,56 +207,190 @@ function checkUrlPaths(
|
||||
? `${name} with a URL path and a literal before a later argument that has effects`
|
||||
: `${name} with a URL path and a later literal that has effects`,
|
||||
e,
|
||||
hint,
|
||||
HINT,
|
||||
);
|
||||
}
|
||||
staged.push(e);
|
||||
}
|
||||
if (staged.length > 0) {
|
||||
lowerer.stagedAfterPath.set(args[first]!, staged);
|
||||
for (const e of staged) lowerer.stagedArguments.set(e, null);
|
||||
}
|
||||
if (row.pathsFirst) return;
|
||||
// writeFileSync/appendFileSync's data is checked after the path.
|
||||
const data = member === "writeFileSync" || member === "appendFileSync" ? 1 : -1;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (row.paths.includes(i) || i === data || preValidatedLiteral(args[i]!)) continue;
|
||||
lowerer.noLowering(`${name} with a URL path and options that are not literals`, args[i]!, hint);
|
||||
}
|
||||
return staged;
|
||||
}
|
||||
|
||||
/** Fences URL paths that cannot convert in Node's order: the check every
|
||||
* fs and fs.promises call runs before lowering its arguments. */
|
||||
/** Plans the URL paths of a synchronous fs call (see the file comment):
|
||||
* the check every fs call runs before lowering its arguments. */
|
||||
export function checkFsUrlPaths(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
bi: { module: string; member: string },
|
||||
): void {
|
||||
if (bi.module === "fs" || bi.module === "fs/promises")
|
||||
checkUrlPaths(lowerer, call, bi.module, bi.member);
|
||||
if (bi.module !== "fs") return;
|
||||
const row = FS_URL_PATHS[bi.member];
|
||||
if (!row) return;
|
||||
const args = call.arguments.nodes;
|
||||
const first = row.paths.find((i) => isUrlArgument(lowerer, args[i]));
|
||||
if (first === undefined || lowerer.fsPathPlans.has(stripParens(args[first]!))) return;
|
||||
const name = bi.member;
|
||||
if (args.some(ts.isSpreadElement))
|
||||
lowerer.noLowering(`${name} with spread arguments and a URL path`, call);
|
||||
const checks: { path: ts.Expression; name: string }[] = [];
|
||||
for (const i of row.paths) {
|
||||
if (i >= first || args[i] === undefined) continue;
|
||||
const path = stripParens(args[i]!);
|
||||
checks.push({ path, name: row.names?.[i] ?? "path" });
|
||||
lowerer.fsPathPlans.set(path, { kind: "save", local: null });
|
||||
}
|
||||
const encoding = optionsBeforePaths(lowerer, args, bi.member, row, name);
|
||||
const staged = stagedAfter(lowerer, args, first, name, (e) => e === encoding);
|
||||
for (const e of staged) lowerer.stagedArguments.set(e, null);
|
||||
lowerer.fsPathPlans.set(stripParens(args[first]!), { kind: "convert", staged, checks, encoding });
|
||||
}
|
||||
|
||||
/** existsSync and the fs.promises functions with URL paths: each URL
|
||||
* converts to a value (url.fileURLToPathValue) after the arguments are
|
||||
* evaluated, and the call runs on the converted paths only when every
|
||||
* conversion produced a string; otherwise existsSync answers false and an
|
||||
* fs.promises function a promise rejected with the first conversion's
|
||||
* error. `lowerCall` lowers the call itself, whose URL paths then read the
|
||||
* converted values. Null for every other call. */
|
||||
export function lowerFallibleFsUrlCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
bi: { module: string; member: string },
|
||||
lowerCall: () => IrExpr,
|
||||
): IrExpr | null {
|
||||
const row =
|
||||
bi.module === "fs/promises"
|
||||
? FSP_PATHS[bi.member]
|
||||
: bi.module === "fs" && bi.member === "existsSync"
|
||||
? ({ paths: [0], pathsFirst: true } satisfies FsPathRow)
|
||||
: undefined;
|
||||
if (!row) return null;
|
||||
const args = call.arguments.nodes;
|
||||
const urls = row.paths.filter((i) => isUrlArgument(lowerer, args[i]));
|
||||
if (urls.length === 0 || lowerer.fallibleFsCalls.has(call)) return null;
|
||||
const name = bi.module === "fs/promises" ? `fs.promises.${bi.member}` : bi.member;
|
||||
if (args.some(ts.isSpreadElement))
|
||||
lowerer.noLowering(`${name} with spread arguments and a URL path`, call);
|
||||
const first = urls[0]!;
|
||||
// A string path before the URL would be checked in the call itself,
|
||||
// after the conversion.
|
||||
if (row.paths.some((i) => i < first && args[i] !== undefined))
|
||||
lowerer.noLowering(`${name} with a string path before a URL path`, call, HINT);
|
||||
if (optionsBeforePaths(lowerer, args, bi.member, row, name) !== null)
|
||||
lowerer.noLowering(`${name} with a URL path and options that are not literals`, args[1]!, HINT);
|
||||
const loc = locOf(call);
|
||||
const stmts: IrStmt[] = [];
|
||||
const values = new Map<number, string>();
|
||||
const save = (value: IrExpr, label: string, at: ts.Node): string => {
|
||||
const slot = lowerer.declareHiddenLocal(label, value.type);
|
||||
stmts.push({ kind: "varDecl", localId: slot.id, init: value, loc: locOf(at) });
|
||||
return slot.id;
|
||||
};
|
||||
values.set(first, save(lowerer.lowerExprExpecting(args[first]!, URL_T), "%fsUrl", args[first]!));
|
||||
const staged = stagedAfter(lowerer, args, first, name, (_e, i) => urls.includes(i));
|
||||
for (const e of staged) {
|
||||
// A call lowered again (an array-literal element) evaluates the
|
||||
// argument anew; the slot an earlier lowering saved it in is gone.
|
||||
lowerer.stagedArguments.delete(e);
|
||||
const value = lowerer.lowerExpr(e);
|
||||
const slot = save(value, "%fsArg", e);
|
||||
lowerer.stagedArguments.set(e, { localId: slot, type: value.type });
|
||||
const index = args.findIndex((arg) => stripTypeWrappers(arg) === e);
|
||||
if (urls.includes(index)) values.set(index, slot);
|
||||
}
|
||||
const results: string[] = [];
|
||||
for (const i of urls) {
|
||||
const url = varRef(values.get(i)!, URL_T, loc);
|
||||
const converted: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "url.fileURLToPathValue",
|
||||
args: [url],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const local = save(converted, "%fsPathValue", args[i]!);
|
||||
lowerer.fsPathPlans.set(stripParens(args[i]!), { kind: "value", local });
|
||||
results.push(local);
|
||||
}
|
||||
lowerer.fallibleFsCalls.add(call);
|
||||
let inner: IrExpr;
|
||||
try {
|
||||
inner = lowerCall();
|
||||
} finally {
|
||||
lowerer.fallibleFsCalls.delete(call);
|
||||
}
|
||||
let result = inner;
|
||||
for (let k = results.length - 1; k >= 0; k--) {
|
||||
const value = varRef(results[k]!, DYN, loc);
|
||||
const failed: IrExpr =
|
||||
bi.module === "fs/promises"
|
||||
? { kind: "intrinsic", name: "promise.reject", args: [value], type: inner.type, loc }
|
||||
: boolLit(false, loc);
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: { kind: "dynTest", test: "string", value, type: BOOL, loc },
|
||||
then: result,
|
||||
else_: failed,
|
||||
type: inner.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return { kind: "seqExpr", stmts, result, type: inner.type, loc };
|
||||
}
|
||||
|
||||
/** An fs path argument as a string: a URL converts with fileURLToPath's
|
||||
* conversion (url.fileURLToPathUrl), which throws Node's errors for other
|
||||
* schemes, hosts and encoded separators. */
|
||||
* schemes, hosts and encoded separators, after the checks its plan names. */
|
||||
export function lowerFsPath(lowerer: Lowerer, node: ts.Expression): IrExpr {
|
||||
if (!isUrlArgument(lowerer, node)) return lowerer.lowerExprExpecting(node, STRING);
|
||||
const loc = locOf(node);
|
||||
const plan = lowerer.fsPathPlans.get(stripParens(node));
|
||||
if (plan?.kind === "value")
|
||||
return { kind: "dynCheck", value: varRef(plan.local, DYN, loc), type: STRING, loc };
|
||||
if (!isUrlArgument(lowerer, node)) {
|
||||
const value = lowerer.lowerExprExpecting(node, STRING);
|
||||
if (plan?.kind !== "save") return value;
|
||||
const slot = lowerer.declareHiddenLocal("%fsPath", STRING);
|
||||
plan.local = slot.id;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: slot.id, init: value, loc }],
|
||||
result: varRef(slot.id, STRING, loc),
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const url = lowerer.lowerExprExpecting(node, URL_T);
|
||||
const staged = lowerer.stagedAfterPath.get(node);
|
||||
if (staged === undefined)
|
||||
if (plan?.kind !== "convert")
|
||||
return { kind: "libCall", fn: "url.fileURLToPathUrl", args: [url], type: STRING, loc };
|
||||
// The URL and the later arguments with effects, in source order, then
|
||||
// the conversion.
|
||||
// the checks Node runs first, then the conversion.
|
||||
const saved = lowerer.declareHiddenLocal("%fsUrl", URL_T);
|
||||
const stmts: IrStmt[] = [{ kind: "varDecl", localId: saved.id, init: url, loc }];
|
||||
for (const arg of staged) {
|
||||
for (const arg of plan.staged) {
|
||||
lowerer.stagedArguments.delete(arg);
|
||||
const value = lowerer.lowerExpr(arg);
|
||||
const slot = lowerer.declareHiddenLocal("%fsArg", value.type);
|
||||
stmts.push({ kind: "varDecl", localId: slot.id, init: value, loc: locOf(arg) });
|
||||
lowerer.stagedArguments.set(arg, { localId: slot.id, type: value.type });
|
||||
}
|
||||
const check = (fn: "fs.checkPath" | "fs.checkReadEncoding", args: IrExpr[]): void => {
|
||||
stmts.push({ kind: "exprStmt", expr: { kind: "libCall", fn, args, type: VOID, loc }, loc });
|
||||
};
|
||||
for (const { path, name } of plan.checks) {
|
||||
const saved = lowerer.fsPathPlans.get(path);
|
||||
if (saved?.kind !== "save" || saved.local === null)
|
||||
throw new InternalCompilerError("an fs path checked before it was lowered");
|
||||
check("fs.checkPath", [
|
||||
varRef(saved.local, STRING, loc),
|
||||
{ kind: "strLit", value: name, type: STRING, loc },
|
||||
]);
|
||||
}
|
||||
if (plan.encoding !== null) {
|
||||
const slot = lowerer.stagedArguments.get(plan.encoding);
|
||||
if (!slot) throw new InternalCompilerError("an fs encoding checked before it was saved");
|
||||
check("fs.checkReadEncoding", [
|
||||
lowerer.coerceToExpected(varRef(slot.localId, slot.type, loc), STRING),
|
||||
]);
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts,
|
||||
@@ -219,7 +406,12 @@ export function lowerFsPath(lowerer: Lowerer, node: ts.Expression): IrExpr {
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether `index` is a path parameter of the fs function `member`. */
|
||||
export function isFsPathParam(member: string, index: number): boolean {
|
||||
return FS_URL_PATHS[member]?.paths.includes(index) === true;
|
||||
/** Whether `index` is a path parameter of the fs or fs.promises function
|
||||
* `member`. */
|
||||
export function isFsPathParam(module: string, member: string, index: number): boolean {
|
||||
if (module === "fs/promises") return FSP_PATHS[member]?.paths.includes(index) === true;
|
||||
if (module !== "fs") return false;
|
||||
return (
|
||||
(member === "existsSync" && index === 0) || FS_URL_PATHS[member]?.paths.includes(index) === true
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
lowerFsWriteOptionsCall,
|
||||
lowerFsReadPathCall,
|
||||
} from "./filesystem-calls.js";
|
||||
import { checkFsUrlPaths, isFsPathParam, lowerFsPath } from "./fs-paths.js";
|
||||
import { checkFsUrlPaths, isFsPathParam, lowerFallibleFsUrlCall, lowerFsPath } from "./fs-paths.js";
|
||||
import { lowerBufferEncodingCall } from "./text-codecs.js";
|
||||
import { lowerUtilTypeCall } from "./util-types.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
@@ -59,6 +59,10 @@ export function lowerBuiltinModuleCall(
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const name = ts.getTextOfNode(expr.expression);
|
||||
const fallible = lowerFallibleFsUrlCall(lowerer, expr, bi, () =>
|
||||
lowerBuiltinModuleCall(lowerer, expr, bi, fn, loc),
|
||||
);
|
||||
if (fallible) return fallible;
|
||||
checkFsUrlPaths(lowerer, expr, bi);
|
||||
if (bi.module === "util/types") {
|
||||
return lowerUtilTypeCall(lowerer, expr, bi, loc, name);
|
||||
@@ -258,7 +262,7 @@ export function lowerBuiltinModuleCall(
|
||||
bi.member === "readFile" &&
|
||||
expr.arguments.nodes.length === 1
|
||||
) {
|
||||
const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
|
||||
const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "fsp.readFileBytes",
|
||||
@@ -473,7 +477,7 @@ export function lowerBuiltinModuleCall(
|
||||
args = lowerer.completeArgs(expr.arguments.nodes, shapes, loc, expr);
|
||||
} else {
|
||||
args = expr.arguments.nodes.map((a, i) =>
|
||||
bi.module === "fs" && isFsPathParam(bi.member, i)
|
||||
isFsPathParam(bi.module, bi.member, i)
|
||||
? lowerFsPath(lowerer, a)
|
||||
: lowerer.lowerExprExpecting(a, fn.params[i]),
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { keyedHelperName } from "../stable-names.js";
|
||||
import { checkedPromiseAll, thenReader } from "../checked-promise-all.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { newFnCtx, type Lowerer } from "../lowerer.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
@@ -17,31 +17,23 @@ import {
|
||||
UNDEFINED_T,
|
||||
VOID,
|
||||
arrayOf,
|
||||
funcOf,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
} from "../../../ir/ir.js";
|
||||
|
||||
/** `Promise.all([p, q, r])` over DIFFERENT promise types — the checker's
|
||||
* tuple overload, `Promise<[A, B, C]>`. One interned async helper per
|
||||
* entry/result signature takes the entries as arguments, awaits the
|
||||
* runtime combinator over the same promises (each widened to
|
||||
* Promise<void>, a type-only view), so settlement is Node's exactly: the
|
||||
* first rejection in settlement order wins and every later one counts as
|
||||
* handled; then it reads each (now fulfilled) entry and returns the
|
||||
* tuple. Entries keep their identity — no value converts through the
|
||||
* checked-dynamic tree. Non-promise entries (numbers, strings, booleans,
|
||||
* unit values — never thenable) pass straight into their slot; a
|
||||
* checked-dynamic entry (a JavaScript function's result) joins through
|
||||
* PromiseResolve: the promise it holds, a promise that adopts a thenable,
|
||||
* or a promise fulfilled with its plain value. The tuple
|
||||
* fulfills one microtask turn after the combinator does. Null when the
|
||||
* result type or an entry has no typed home here. */
|
||||
function lowerTuplePromiseAll(
|
||||
* tuple overload, `Promise<[A, B, C]>`: the result type and tuple shape,
|
||||
* or null when the result type or an entry has no typed home here.
|
||||
* Entries are promises, checked-dynamic values (a JavaScript function's
|
||||
* result) and plain numbers, strings, booleans and unit values, which are
|
||||
* never thenable. */
|
||||
function tuplePromiseAllShape(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
elems: IrExpr[],
|
||||
): IrExpr | null {
|
||||
elems: readonly IrExpr[],
|
||||
): { resultT: IrType & { kind: "promise" }; tuple: IrType & { kind: "record" } } | null {
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (resultT?.kind !== "promise" || resultT.inner.kind !== "record") return null;
|
||||
const shape = lowerer.shapes.get(resultT.inner.shapeId);
|
||||
@@ -53,135 +45,159 @@ function lowerTuplePromiseAll(
|
||||
const settles = (t: IrType): boolean => t.kind === "promise" || t.kind === "dyn";
|
||||
if (!elems.some((e) => settles(e.type))) return null;
|
||||
if (!elems.every((e) => settles(e.type) || plainValue(e.type))) return null;
|
||||
return { resultT, tuple: resultT.inner };
|
||||
}
|
||||
|
||||
/** `Promise.all([p, q, r])` over different promise types. Each entry
|
||||
* becomes a promise in input order (a plain value through Promise.resolve,
|
||||
* a checked-dynamic one through its PromiseResolve view: the promise it
|
||||
* holds, a promise that adopts a thenable, or a promise fulfilled with the
|
||||
* value), widened to Promise<void>, a type-only view. The runtime
|
||||
* combinator over them gives Node's settlement: the first rejection in
|
||||
* settlement order wins and every later one counts as handled. The
|
||||
* result settles inside the combinator's own settlement, so it and its
|
||||
* awaits land on Node's microtask turns; a fulfillment runs one interned
|
||||
* function per entry/result signature that reads the fulfilled entries
|
||||
* into the tuple. Entries keep their identity: no value converts through
|
||||
* the checked-dynamic tree. */
|
||||
function lowerTuplePromiseAll(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
elems: IrExpr[],
|
||||
): IrExpr | null {
|
||||
const plan = tuplePromiseAllShape(lowerer, call, elems);
|
||||
if (!plan) return null;
|
||||
const { resultT, tuple } = plan;
|
||||
const loc = locOf(call);
|
||||
const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(resultT.inner)}`;
|
||||
const voidPromise: IrType & { kind: "promise" } = { kind: "promise", inner: VOID };
|
||||
const entriesT = arrayOf(voidPromise);
|
||||
const dynPromise: IrType = { kind: "promise", inner: DYN };
|
||||
// A checked-dynamic entry's PromiseResolve can run a `then` getter, which
|
||||
// Node runs after the whole array literal evaluated. With such an entry,
|
||||
// every entry evaluates into a hidden local first.
|
||||
const staged: IrStmt[] = [];
|
||||
const values = !elems.some((e) => e.type.kind === "dyn")
|
||||
? elems
|
||||
: elems.map((e): IrExpr => {
|
||||
if (e.kind === "unitLit") return e;
|
||||
if (isUnitType(e.type)) {
|
||||
staged.push({ kind: "exprStmt", expr: e, loc });
|
||||
return {
|
||||
kind: "unitLit",
|
||||
unit: e.type.kind === "nullT" ? "null" : "undefined",
|
||||
type: e.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const local = lowerer.declareHiddenLocal("%allValue", e.type);
|
||||
staged.push({ kind: "varDecl", localId: local.id, init: e, loc });
|
||||
return varRef(local.id, e.type, loc);
|
||||
});
|
||||
const entries = values.map((e): IrExpr => {
|
||||
let promise: IrExpr;
|
||||
if (e.type.kind === "promise") promise = e;
|
||||
else if (e.type.kind === "dyn")
|
||||
promise = {
|
||||
kind: "libCall",
|
||||
fn: "dyn.promiseResolve",
|
||||
args: [e, thenReader(lowerer, loc)],
|
||||
type: dynPromise,
|
||||
loc,
|
||||
};
|
||||
else if (isUnitType(e.type)) {
|
||||
// A unit entry's value is its type's one value; the promise only
|
||||
// takes its place in the combinator.
|
||||
const resolved: IrExpr = {
|
||||
kind: "intrinsic",
|
||||
name: "promise.resolve",
|
||||
args: [],
|
||||
type: voidPromise,
|
||||
loc,
|
||||
};
|
||||
promise =
|
||||
e.kind === "unitLit"
|
||||
? resolved
|
||||
: {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: e, loc }],
|
||||
result: resolved,
|
||||
type: voidPromise,
|
||||
loc,
|
||||
};
|
||||
} else
|
||||
promise = {
|
||||
kind: "intrinsic",
|
||||
name: "promise.resolve",
|
||||
args: [e],
|
||||
type: { kind: "promise", inner: e.type },
|
||||
loc,
|
||||
};
|
||||
return promise.type.kind === "promise" && promise.type.inner.kind === "void"
|
||||
? promise
|
||||
: { kind: "promiseVoidWiden", value: promise, type: voidPromise, loc };
|
||||
});
|
||||
const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(tuple)}`;
|
||||
let name = lowerer.arrHofHelpers.get(`promise.allTuple:${key}`);
|
||||
if (name === undefined) {
|
||||
name = keyedHelperName(lowerer, "%promise.allTuple", `promise.allTuple:${key}`);
|
||||
lowerer.arrHofHelpers.set(`promise.allTuple:${key}`, name);
|
||||
const ctx = newFnCtx(false, null, null, resultT.inner);
|
||||
ctx.isAsync = true;
|
||||
lowerer.fnStack.push(ctx);
|
||||
try {
|
||||
const params = elems.map((e, i) => ({
|
||||
localId: `entry.${i}`,
|
||||
name: `entry${i}`,
|
||||
type: e.type,
|
||||
}));
|
||||
const ref = (i: number): IrExpr => varRef(`entry.${i}`, elems[i]!.type, loc);
|
||||
const dynPromise: IrType = { kind: "promise", inner: DYN };
|
||||
const body: IrStmt[] = [];
|
||||
// Each dyn entry's promise view, in entry order: the promise it
|
||||
// holds, a promise that adopts a thenable's settlement, or a promise
|
||||
// fulfilled with the plain value (PromiseResolve's rule).
|
||||
const views = new Map<number, IrExpr>();
|
||||
elems.forEach((e, i) => {
|
||||
if (e.type.kind !== "dyn") return;
|
||||
const view = lowerer.declareHiddenLocal("%allEntry", dynPromise);
|
||||
body.push({
|
||||
kind: "varDecl",
|
||||
localId: view.id,
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.promiseResolve",
|
||||
args: [ref(i), thenReader(lowerer, loc)],
|
||||
type: dynPromise,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
views.set(i, varRef(view.id, dynPromise, loc));
|
||||
});
|
||||
const widened: IrExpr[] = [];
|
||||
elems.forEach((e, i) => {
|
||||
const view = views.get(i);
|
||||
if (view !== undefined) {
|
||||
widened.push({
|
||||
kind: "promiseVoidWiden",
|
||||
value: view,
|
||||
type: { kind: "promise", inner: VOID },
|
||||
loc,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (e.type.kind !== "promise") return;
|
||||
widened.push(
|
||||
e.type.inner.kind === "void"
|
||||
? ref(i)
|
||||
: {
|
||||
kind: "promiseVoidWiden",
|
||||
value: ref(i),
|
||||
type: { kind: "promise", inner: VOID },
|
||||
const shape = lowerer.shapes.get(tuple.shapeId)!;
|
||||
const entriesRef = varRef("entries.0", entriesT, loc);
|
||||
// Shape fields are sorted by name ("10" before "2"); a tuple field's
|
||||
// name is its position.
|
||||
const fields = shape.fields.map((field) => {
|
||||
const i = Number(field.name);
|
||||
const type = elems[i]!.type;
|
||||
const inner = type.kind === "promise" ? type.inner : type;
|
||||
const value: IrExpr =
|
||||
inner.kind === "void"
|
||||
? { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }
|
||||
: isUnitType(inner)
|
||||
? {
|
||||
kind: "unitLit",
|
||||
unit: inner.kind === "nullT" ? "null" : "undefined",
|
||||
type: inner,
|
||||
loc,
|
||||
},
|
||||
);
|
||||
});
|
||||
const settled: IrExpr = {
|
||||
kind: "awaitExpr",
|
||||
value: {
|
||||
kind: "intrinsic",
|
||||
name: "promise.all",
|
||||
args: [
|
||||
{
|
||||
kind: "arrayLit",
|
||||
elems: widened,
|
||||
type: arrayOf({ kind: "promise", inner: VOID }),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: { kind: "promise", inner: VOID },
|
||||
loc,
|
||||
},
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
// Shape fields are sorted by name ("10" before "2"); a tuple field's
|
||||
// name is its position.
|
||||
const fields = shape.fields.map((field) => {
|
||||
const i = Number(field.name);
|
||||
const entry = elems[i]!;
|
||||
let value: IrExpr;
|
||||
const view = views.get(i);
|
||||
if (view !== undefined) {
|
||||
value = { kind: "awaitExpr", value: view, type: DYN, loc };
|
||||
} else if (entry.type.kind === "promise") {
|
||||
value =
|
||||
entry.type.inner.kind === "void"
|
||||
? { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }
|
||||
: { kind: "awaitExpr", value: ref(i), type: entry.type.inner, loc };
|
||||
} else {
|
||||
value = ref(i);
|
||||
}
|
||||
return { name: field.name, value: lowerer.coerceToExpected(value, field.type) };
|
||||
});
|
||||
body.push(
|
||||
{ kind: "exprStmt", expr: settled, loc },
|
||||
{
|
||||
kind: "return",
|
||||
value: { kind: "recordLit", fields, type: resultT.inner, loc },
|
||||
loc,
|
||||
},
|
||||
);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params,
|
||||
returnType: resultT.inner,
|
||||
locals: [
|
||||
...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
|
||||
...lowerer.ctx.locals,
|
||||
],
|
||||
body,
|
||||
async: true,
|
||||
loc,
|
||||
});
|
||||
} catch (e) {
|
||||
lowerer.arrHofHelpers.delete(`promise.allTuple:${key}`);
|
||||
throw e;
|
||||
} finally {
|
||||
lowerer.fnStack.pop();
|
||||
}
|
||||
}
|
||||
: {
|
||||
kind: "libCall",
|
||||
fn: "promise.fulfilledValue",
|
||||
args: [
|
||||
{
|
||||
kind: "arrayGet",
|
||||
arr: entriesRef,
|
||||
index: { kind: "numLit", value: i, type: F64, loc },
|
||||
type: voidPromise,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: inner,
|
||||
loc,
|
||||
};
|
||||
return { name: field.name, value: lowerer.coerceToExpected(value, field.type) };
|
||||
});
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "entries.0", name: "entries", type: entriesT }],
|
||||
returnType: tuple,
|
||||
locals: [{ id: "entries.0", name: "entries", type: entriesT, mutable: false }],
|
||||
body: [{ kind: "return", value: { kind: "recordLit", fields, type: tuple, loc }, loc }],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: elems, type: resultT, loc };
|
||||
const all: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "promise.allTuple",
|
||||
args: [
|
||||
{ kind: "arrayLit", elems: entries, type: entriesT, loc },
|
||||
{ kind: "closure", fnName: name, captures: [], type: funcOf([entriesT], tuple), loc },
|
||||
],
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
return staged.length === 0
|
||||
? all
|
||||
: { kind: "seqExpr", stmts: staged, result: all, type: resultT, loc };
|
||||
}
|
||||
|
||||
/** `Promise.race([...])` on THE Promise global: the entries lower
|
||||
|
||||
@@ -611,7 +611,6 @@ export function checkDefaultParamBodyType(
|
||||
if (
|
||||
bodyType.kind === "void" ||
|
||||
bodyType.kind === "regex" ||
|
||||
bodyType.kind === "date" ||
|
||||
bodyType.kind === "dyn" ||
|
||||
isUnitType(bodyType)
|
||||
) {
|
||||
|
||||
@@ -381,7 +381,7 @@ function esModuleExportIgnoresThis(
|
||||
depth: number,
|
||||
): boolean {
|
||||
const moduleSym = lowerer.checker.getSymbolAtLocation(sf);
|
||||
const exported = moduleSym?.getExports().get(name as ts.__String);
|
||||
const exported = moduleSym?.getExports().get(ts.escapedSymbolName(name));
|
||||
if (exported !== undefined) {
|
||||
const target =
|
||||
exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported;
|
||||
|
||||
@@ -156,6 +156,13 @@ export function classCallbackValue(
|
||||
};
|
||||
}
|
||||
|
||||
/** Notes a checked conversion of a replaceable method's result for the
|
||||
* key-order witness: the result is a value the method or its replacement
|
||||
* returns, not a record built from a dynamic value. */
|
||||
function methodResult(lowerer: Lowerer, checked: IrExpr, name: string): IrExpr {
|
||||
return checked.kind === "dynCheck" ? lowerer.keyOrder.methodResult(checked, name) : checked;
|
||||
}
|
||||
|
||||
export function classCallbackCall(
|
||||
lowerer: Lowerer,
|
||||
receiver: IrExpr,
|
||||
@@ -197,7 +204,7 @@ export function classCallbackCall(
|
||||
// generator arm does not box) keeps its native type: the unchanged
|
||||
// method's result stays as it is, and a replacement's dynamic result
|
||||
// takes the checked conversion into that type.
|
||||
const converted = lowerer.coerceToExpected(call, fallback.type);
|
||||
const converted = methodResult(lowerer, lowerer.coerceToExpected(call, fallback.type), name);
|
||||
if (!typeEquals(converted.type, fallback.type)) {
|
||||
lowerer.pushDiag(
|
||||
unsupportedDiag(
|
||||
@@ -234,7 +241,9 @@ export function classCallbackCall(
|
||||
loc,
|
||||
};
|
||||
answer =
|
||||
fallback.type.kind === "void" ? result : lowerer.coerceToExpected(result, fallback.type);
|
||||
fallback.type.kind === "void"
|
||||
? result
|
||||
: methodResult(lowerer, lowerer.coerceToExpected(result, fallback.type), name);
|
||||
}
|
||||
// Read after observedSlot built the prototype data and its originals.
|
||||
const originalId = observed ? original?.originalId() : undefined;
|
||||
|
||||
@@ -84,7 +84,7 @@ export function collectClassMethodMutations(
|
||||
for (const name of literalValues(lowerer.typeOf(key)) ?? []) methods.add(String(name));
|
||||
const markName = (name: string, site: ts.Node, arrow = false): void => {
|
||||
if (name === "prototype" || name === "__proto__") unknownKey = site;
|
||||
if (methods.has(name)) lowerer.prototypeMethodAccesses.set(name, site);
|
||||
if (methods.has(name)) lowerer.noteMethodAccess(name, site);
|
||||
if (!arrow) receiverFields.add(name);
|
||||
};
|
||||
const unwrap = (value: ts.Expression): ts.Expression => {
|
||||
@@ -125,11 +125,13 @@ export function collectClassMethodMutations(
|
||||
};
|
||||
// Whether no function value can inhabit `type`. Primitives qualify, and so
|
||||
// do non-callable object types a function is not assignable to: one that
|
||||
// requires a property functions lack, or a weak (all-optional) type that
|
||||
// requires a property functions lack, a weak (all-optional) type that
|
||||
// shares no property with functions (TypeScript's weak-type rule rejects
|
||||
// callable sources too).
|
||||
const functionFree = (type: ts.Type): boolean => {
|
||||
if (type.isUnionType()) return ts.constituentTypes(type).every(functionFree);
|
||||
// callable sources too), or a dictionary with no properties whose index
|
||||
// signatures hold function-free values (functions have no index
|
||||
// signature). `seen` stops recursive dictionary types.
|
||||
const functionFree = (type: ts.Type, seen: Set<ts.Type> = new Set()): boolean => {
|
||||
if (type.isUnionType()) return ts.constituentTypes(type).every((t) => functionFree(t, seen));
|
||||
if (
|
||||
(type.flags &
|
||||
(ts.TypeFlags.StringLike |
|
||||
@@ -149,7 +151,12 @@ export function collectClassMethodMutations(
|
||||
)
|
||||
return false;
|
||||
const properties = lowerer.checker.getPropertiesOfType(type);
|
||||
if (properties.length === 0) return false;
|
||||
if (properties.length === 0) {
|
||||
if (seen.has(type)) return true;
|
||||
seen.add(type);
|
||||
const indexes = lowerer.checker.getIndexInfosOfType(type);
|
||||
return indexes.length > 0 && indexes.every((index) => functionFree(index.type, seen));
|
||||
}
|
||||
const required = properties.filter(
|
||||
(property) => (property.flags & ts.SymbolFlags.Optional) === 0,
|
||||
);
|
||||
@@ -395,10 +402,9 @@ export function collectClassMethodMutations(
|
||||
ts.isStringLiteralLike(base.argumentExpression) &&
|
||||
base.argumentExpression.text === "prototype");
|
||||
if (prototype) {
|
||||
if (ts.isPropertyAccessExpression(node))
|
||||
lowerer.prototypeMethodAccesses.set(node.name.text, node);
|
||||
if (ts.isPropertyAccessExpression(node)) lowerer.noteMethodAccess(node.name.text, node);
|
||||
else if (ts.isStringLiteralLike(node.argumentExpression))
|
||||
lowerer.prototypeMethodAccesses.set(node.argumentExpression.text, node);
|
||||
lowerer.noteMethodAccess(node.argumentExpression.text, node);
|
||||
else markKey(node.argumentExpression, node);
|
||||
}
|
||||
}
|
||||
@@ -435,7 +441,7 @@ export function collectClassMethodMutations(
|
||||
? lowerer.checker.getSymbolAtLocation(access.name)
|
||||
: lowerer.checker.getPropertyOfType(lowerer.typeOf(access.expression), name);
|
||||
if (symbol && symbol.declarations.some(ts.isMethodDeclaration))
|
||||
lowerer.prototypeMethodAccesses.set(name, access);
|
||||
lowerer.noteMethodAccess(name, access);
|
||||
}
|
||||
lowerer.checker.prefetchSymbolRoots([...globals, ...reflection]);
|
||||
for (const node of globals)
|
||||
@@ -571,9 +577,10 @@ export function collectClassMethodMutations(
|
||||
);
|
||||
for (const { target, site } of propertyWrites) {
|
||||
const name = target.name.text;
|
||||
if (lowerer.prototypeMethodAccesses.has(name) || functionFree(lowerer.typeOf(target))) continue;
|
||||
if (!methodOwnerCanInhabit(target.expression, name)) continue;
|
||||
lowerer.prototypeMethodAccesses.set(name, site);
|
||||
if (functionFree(lowerer.typeOf(target)) || !methodOwnerCanInhabit(target.expression, name))
|
||||
continue;
|
||||
if (lowerer.prototypeMethodAccesses.has(name)) lowerer.noteMethodSite(name, site);
|
||||
else lowerer.noteMethodAccess(name, site);
|
||||
}
|
||||
for (const { key, receiver, site } of computedWrites) {
|
||||
// A class method declared with a well-known symbol key is not a write;
|
||||
@@ -620,7 +627,7 @@ export function collectClassMethodMutations(
|
||||
}
|
||||
}
|
||||
if (unknownKey) {
|
||||
for (const method of methods) lowerer.prototypeMethodAccesses.set(method, unknownKey);
|
||||
for (const method of methods) lowerer.noteMethodAccess(method, unknownKey);
|
||||
for (const method of symbolMethods) lowerer.replacedSymbolMethods.add(method);
|
||||
}
|
||||
if (!unknownKey && !unknownOwnField)
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/* Completion callbacks in programs that use AsyncLocalStorage. Node runs
|
||||
* the callback of an operation a callback-style API starts (fs.readFile,
|
||||
* crypto.randomBytes, zlib.gzip, dns.lookup, server.listen, net.connect,
|
||||
* http.get, stream.finished, stream.pipeline, ...) with the stores active
|
||||
* when the call started it. The runtime invokes these callbacks from
|
||||
* the event loop, where the context is empty, so in programs whose
|
||||
* scheduling carries contexts (RuntimeFeatures asyncContext) each such
|
||||
* argument is bound to the active context at the call: als.bindCallback
|
||||
* for typed closures, als.bindCallbackDyn for dyn functions. Other
|
||||
* programs are left unchanged. */
|
||||
import { BOOL, type IrExpr, type IrModule, type IrStmt } from "../../ir/ir.js";
|
||||
import type { IrLibFn } from "../../ir/builtin-signatures.js";
|
||||
import { mapExprChildren, mapStmtChildren } from "../../ir/traverse.js";
|
||||
import { moduleRuntimeFeatures } from "../../ir/runtime-features.js";
|
||||
|
||||
/** The argument that is the completion callback: its index; "last" for
|
||||
* calls whose callback is the final argument when one is passed; or
|
||||
* "arguments" for the fs callback calls, whose argument is the call's
|
||||
* dyn argument vector, where the runtime finds the callback. */
|
||||
type CallbackPosition = number | "last" | "arguments";
|
||||
|
||||
const COMPLETION_CALLBACKS: ReadonlyMap<IrLibFn, CallbackPosition> = new Map<
|
||||
IrLibFn,
|
||||
CallbackPosition
|
||||
>([
|
||||
["cp.execFile", 2],
|
||||
["crypto.generateKeyCb", 2],
|
||||
["crypto.hkdfCb", 5],
|
||||
["crypto.pbkdf2Cb", 5],
|
||||
["crypto.randomBytesCb", 1],
|
||||
["crypto.randomFillCb", 4],
|
||||
["crypto.randomFillCbDyn", 4],
|
||||
["crypto.randomIntCb", 3],
|
||||
["crypto.scryptCb", 4],
|
||||
["dgram.bindCb", 3],
|
||||
["dgram.closeCb", 1],
|
||||
["dgram.connectCb", 3],
|
||||
["dns.lookupDyn", 2],
|
||||
["fs.callbackCall", "arguments"],
|
||||
["fs.callbackGlob", "arguments"],
|
||||
["fs.renameCb", 2],
|
||||
["http.requestAgentCb", "last"],
|
||||
["http.requestCb", "last"],
|
||||
["http.requestConnCb", "last"],
|
||||
["http.requestDynCb", "last"],
|
||||
["http.requestUrlCb", "last"],
|
||||
["net.connectCb", 2],
|
||||
["net.listenCb", 2],
|
||||
["net.listenOptsCb", 4],
|
||||
["net.listenOptsReusePortCb", 5],
|
||||
["net.listenPathCb", 2],
|
||||
["process.stderrWriteBytesCb", 2],
|
||||
["process.stdoutWriteBytesCb", 2],
|
||||
// stream.finished and stream.pipeline callbacks, which Node binds to
|
||||
// the context of the call (eos' and pipeline's AsyncResource.bind).
|
||||
["stream.finished", 1],
|
||||
["stream.finishedDyn", 1],
|
||||
["stream.finishedOpts", "last"],
|
||||
["stream.finishedOptsSignal", "last"],
|
||||
["stream.pipeline", "last"],
|
||||
["stream.pipelineDyn", "last"],
|
||||
["stream.pipelineStages", "last"],
|
||||
["stream.pipelineStagesDyn", "last"],
|
||||
["zlib.cbOpts", 3],
|
||||
["zlib.cbOptsDyn", 3],
|
||||
["zlib.deflateCb", 1],
|
||||
["zlib.deflateRawCb", 1],
|
||||
["zlib.gunzipCb", 1],
|
||||
["zlib.gzipCb", 1],
|
||||
["zlib.inflateCb", 1],
|
||||
["zlib.inflateRawCb", 1],
|
||||
["zlib.unzipCb", 1],
|
||||
]);
|
||||
|
||||
/** Completion callbacks that fire from other contexts' code (a stream event
|
||||
* that another context's call emits), so an empty context at the call
|
||||
* binds too: the callback then runs with no store, as in Node. */
|
||||
const BIND_EMPTY_CONTEXT: ReadonlySet<IrLibFn> = new Set<IrLibFn>([
|
||||
"stream.finished",
|
||||
"stream.finishedDyn",
|
||||
"stream.finishedOpts",
|
||||
"stream.finishedOptsSignal",
|
||||
"stream.pipeline",
|
||||
"stream.pipelineDyn",
|
||||
"stream.pipelineStages",
|
||||
"stream.pipelineStagesDyn",
|
||||
]);
|
||||
|
||||
/** `cb` bound to the context active where it is evaluated. Unless
|
||||
* `bindEmpty`, an empty context leaves it unbound (`always` false): the
|
||||
* loop already runs those callbacks with an empty context. */
|
||||
function bindToActiveContext(cb: IrExpr, bindEmpty = false): IrExpr {
|
||||
const always: IrExpr = { kind: "boolLit", value: bindEmpty, type: BOOL, loc: cb.loc };
|
||||
if (cb.type.kind === "func")
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "als.bindCallback",
|
||||
args: [cb, always],
|
||||
type: cb.type,
|
||||
loc: cb.loc,
|
||||
};
|
||||
if (cb.type.kind === "dyn")
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "als.bindCallbackDyn",
|
||||
args: [cb, always],
|
||||
type: cb.type,
|
||||
loc: cb.loc,
|
||||
};
|
||||
return cb;
|
||||
}
|
||||
|
||||
/** An element of a dyn argument vector that can be the callback: a boxed
|
||||
* closure binds as a typed closure; any other dyn value that is not a
|
||||
* boxed primitive binds as a dyn function (non-functions pass through). */
|
||||
function bindArgumentElement(e: IrExpr): IrExpr {
|
||||
if (e.kind === "dynFrom") {
|
||||
return e.value.type.kind === "func" ? { ...e, value: bindToActiveContext(e.value) } : e;
|
||||
}
|
||||
return e.type.kind === "dyn" ? bindToActiveContext(e) : e;
|
||||
}
|
||||
|
||||
function bindCall(e: IrExpr): IrExpr {
|
||||
if (e.kind !== "libCall") return e;
|
||||
const position = COMPLETION_CALLBACKS.get(e.fn);
|
||||
if (position === undefined) return e;
|
||||
if (position === "arguments") {
|
||||
const index = e.fn === "fs.callbackCall" ? 1 : 0;
|
||||
const vector = e.args[index];
|
||||
if (vector === undefined || vector.kind !== "dynArrLit") return e;
|
||||
const args = e.args.slice();
|
||||
args[index] = {
|
||||
kind: "dynArrLit",
|
||||
elems: vector.elems.map(bindArgumentElement),
|
||||
type: vector.type,
|
||||
loc: vector.loc,
|
||||
};
|
||||
return { ...e, args };
|
||||
}
|
||||
const index = position === "last" ? e.args.length - 1 : position;
|
||||
const cb = e.args[index];
|
||||
if (cb === undefined || (position === "last" && cb.type.kind !== "func")) return e;
|
||||
const args = e.args.slice();
|
||||
args[index] = bindToActiveContext(cb, BIND_EMPTY_CONTEXT.has(e.fn));
|
||||
return { ...e, args };
|
||||
}
|
||||
|
||||
/** Binds the completion callbacks of `mod` when its scheduling carries
|
||||
* AsyncLocalStorage contexts. */
|
||||
export function bindCompletionCallbacks(mod: IrModule): void {
|
||||
if (!moduleRuntimeFeatures(mod).asyncContext) return;
|
||||
const rewriteExpr = (e: IrExpr): IrExpr => bindCall(mapExprChildren(e, rewriteExpr, rewriteStmt));
|
||||
const rewriteStmt = (s: IrStmt): IrStmt => mapStmtChildren(s, rewriteExpr, rewriteStmt);
|
||||
for (const fn of mod.functions) fn.body = fn.body.map(rewriteStmt);
|
||||
}
|
||||
@@ -2360,10 +2360,11 @@ function reduceHelper(
|
||||
* the left element first. Undefined values sink without reaching the user
|
||||
* comparator. The callback sequence differs from V8's TimSort, so exact
|
||||
* order and count parity are not claimed; results agree for consistent
|
||||
* comparators. Without a comparator, string, number and boolean arrays use
|
||||
* native stable ordering: their String conversions cannot call user code.
|
||||
* Strings compare UTF-16 units; numbers compare their decimal spellings
|
||||
* ([10, 9, 1] → [1, 10, 9]). Other element types still need a comparator. */
|
||||
* comparators. Without a comparator, string, number, boolean and Date
|
||||
* arrays use native stable ordering: their String conversions cannot call
|
||||
* user code. Strings compare UTF-16 units; numbers compare their decimal
|
||||
* spellings ([10, 9, 1] → [1, 10, 9]); Dates compare their toString() text.
|
||||
* Other element types still need a comparator. */
|
||||
export function lowerArraySortCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -2376,7 +2377,7 @@ export function lowerArraySortCall(
|
||||
const copyFirst = method === "toSorted";
|
||||
if (
|
||||
call.arguments.nodes.length === 0 &&
|
||||
(elem.kind === "string" || elem.kind === "f64" || elem.kind === "bool")
|
||||
(elem.kind === "string" || elem.kind === "f64" || elem.kind === "bool" || elem.kind === "date")
|
||||
) {
|
||||
return {
|
||||
kind: "arrIntrinsic",
|
||||
@@ -2391,7 +2392,7 @@ export function lowerArraySortCall(
|
||||
lowerer.noLowering(
|
||||
`.${method} with ${call.arguments.nodes.length} arguments`,
|
||||
call,
|
||||
"default ordering supports string[], number[] and boolean[]; other element types need a comparator",
|
||||
"default ordering supports string[], number[], boolean[] and Date[]; other element types need a comparator",
|
||||
);
|
||||
}
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
|
||||
@@ -1081,14 +1081,25 @@ export function lowerArrayMethodCall(
|
||||
(lowerer.unions
|
||||
.get(elem.unionId)
|
||||
?.arms.every(
|
||||
(a) => a.kind === "f64" || a.kind === "string" || a.kind === "bool" || isUnitType(a),
|
||||
(a) =>
|
||||
a.kind === "f64" ||
|
||||
a.kind === "string" ||
|
||||
a.kind === "bool" ||
|
||||
a.kind === "date" ||
|
||||
isUnitType(a),
|
||||
) ??
|
||||
false);
|
||||
if (elem.kind !== "f64" && elem.kind !== "string" && elem.kind !== "bool" && !joinableUnion) {
|
||||
if (
|
||||
elem.kind !== "f64" &&
|
||||
elem.kind !== "string" &&
|
||||
elem.kind !== "bool" &&
|
||||
elem.kind !== "date" &&
|
||||
!joinableUnion
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
"'.join()' on arrays of this element type (number, string, and boolean arrays join — unions of those with undefined/null arms too, the units printing empty like JS)",
|
||||
"'.join()' on arrays of this element type (number, string, boolean and Date arrays join — unions of those with undefined/null arms too, the units printing empty like JS)",
|
||||
);
|
||||
}
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import { boolLit, dynUndefinedExpr, numLit, varRef } from "../../../ir/build.js";
|
||||
import { boolLit, dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../../ir/build.js";
|
||||
import { BUF_NUM_METHODS } from "./buffer-numeric-methods.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import {
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type IrLibFn,
|
||||
type IrLocal,
|
||||
type IrParam,
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
STRING,
|
||||
@@ -30,7 +31,8 @@ import {
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { stdlibGlobalNameOf } from "../surfaces.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { newFnCtx, own } from "../lowerer.js";
|
||||
import { ENTRY_NAME, newFnCtx, own } from "../lowerer.js";
|
||||
import { everyStmt } from "../../../ir/traverse.js";
|
||||
import { buildBytesSortFn, closureKeyOrder } from "../lower-array-sort.js";
|
||||
import { isSafeToDiscard } from "../expressions/evaluation-safety.js";
|
||||
import { lowerDynObjectLiteral } from "../expressions/object-literals.js";
|
||||
@@ -903,6 +905,126 @@ function lowerDataViewNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
return lowerArrayBufferView(lowerer, args, "dv", locOf(expr));
|
||||
}
|
||||
|
||||
/** `receiver.set(src, offset?)` on a lowered typed-array receiver. */
|
||||
function bytesSetIntrinsic(
|
||||
lowerer: Lowerer,
|
||||
receiver: IrExpr,
|
||||
argNodes: readonly ts.Expression[],
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const src = lowerer.lowerExpr(argNodes[0]!);
|
||||
const method = src.type.kind === "bytes" ? "setFrom" : "setFromDyn";
|
||||
const args = [src.type.kind === "bytes" ? src : lowerer.coerceInto(argNodes[0]!, src, DYN)];
|
||||
if (argNodes.length === 2)
|
||||
args.push(
|
||||
lowerOptionalArgument(lowerer, argNodes[1]!, F64, {
|
||||
kind: "numLit",
|
||||
value: 0,
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
);
|
||||
return { kind: "bytesIntrinsic", method, receiver, args, type: VOID, loc };
|
||||
}
|
||||
|
||||
/** `Uint8Array.prototype.set.call(target, src, offset?)`, through any typed
|
||||
* array constructor's prototype (they share %TypedArray%.prototype.set). A
|
||||
* target the checker types as a typed array or Buffer lowers like
|
||||
* `target.set(src, offset)`. Any other target, a DataView included, is
|
||||
* checked at run time: a typed array calls its `set`, and
|
||||
* anything else throws Node's TypeError "this is not a typed array.",
|
||||
* after every argument is evaluated. */
|
||||
export function lowerBytesPrototypeSetCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (
|
||||
call.questionDotToken ||
|
||||
access.questionDotToken ||
|
||||
access.name.text !== "call" ||
|
||||
!ts.isPropertyAccessExpression(access.expression)
|
||||
)
|
||||
return null;
|
||||
const method = access.expression;
|
||||
const proto = method.expression;
|
||||
if (
|
||||
method.questionDotToken ||
|
||||
method.name.text !== "set" ||
|
||||
!ts.isPropertyAccessExpression(proto) ||
|
||||
proto.questionDotToken ||
|
||||
proto.name.text !== "prototype" ||
|
||||
!ts.isIdentifier(proto.expression) ||
|
||||
!lowerer.isStdlibMember(method)
|
||||
)
|
||||
return null;
|
||||
const ctor = lowerer.resolveValueSymbol(proto.expression);
|
||||
if (!ctor || !lowerer.isStdlibSymbol(ctor) || !own(BYTES_CTORS, ts.symbolName(ctor))) return null;
|
||||
const argNodes = call.arguments.nodes;
|
||||
if (argNodes.length < 2 || argNodes.length > 3 || argNodes.some(ts.isSpreadElement)) return null;
|
||||
const loc = locOf(call);
|
||||
const target = lowerer.lowerExpr(argNodes[0]!);
|
||||
// A DataView lowers to the same bytes type, so the checker type decides:
|
||||
// only a typed array (or a Buffer) takes the method's path.
|
||||
const targetSymbol = lowerer.typeOf(argNodes[0]!).getSymbol();
|
||||
if (
|
||||
target.type.kind === "bytes" &&
|
||||
targetSymbol !== undefined &&
|
||||
lowerer.isStdlibSymbol(targetSymbol) &&
|
||||
(own(BYTES_CTORS, ts.symbolName(targetSymbol)) !== undefined ||
|
||||
ts.symbolName(targetSymbol) === "Buffer")
|
||||
)
|
||||
return bytesSetIntrinsic(lowerer, target, argNodes.slice(1), loc);
|
||||
const slot = (value: IrExpr): { decl: IrStmt; read: IrExpr } => {
|
||||
const local = lowerer.declareHiddenLocal("%setArg", DYN);
|
||||
return {
|
||||
decl: { kind: "varDecl", localId: local.id, init: value, loc },
|
||||
read: varRef(local.id, DYN, loc),
|
||||
};
|
||||
};
|
||||
const slots = [
|
||||
slot(lowerer.coerceInto(argNodes[0]!, target, DYN)),
|
||||
...argNodes.slice(1).map((arg) => slot(lowerer.lowerExprExpecting(arg, DYN))),
|
||||
];
|
||||
const isTypedArray: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "util.typeIs",
|
||||
args: [slots[0]!.read, { kind: "strLit", value: "isTypedArray", type: STRING, loc }],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
...slots.map((s) => s.decl),
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "unary", op: "!", operand: isTypedArray, type: BOOL, loc },
|
||||
then: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: nodeThrowExpr(1, "", "this is not a typed array.", VOID, loc),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: {
|
||||
kind: "dynInvoke",
|
||||
recv: slots[0]!.read,
|
||||
method: "set",
|
||||
calleeName: ts.getTextOfNode(method),
|
||||
args: slots.slice(1).map((s) => s.read),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Method calls on typed-array/Buffer receivers: slice (copy), subarray
|
||||
* (shared view), toReversed, copyWithin, set(src,
|
||||
* offset?), and the u8-only Buffer surface: toString(enc?) plus the
|
||||
@@ -1106,21 +1228,7 @@ export function lowerBytesMethodCall(
|
||||
lowerer.noLowering(`.set with ${nArgs} arguments on typed arrays`, call);
|
||||
}
|
||||
const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr);
|
||||
const src = lowerer.lowerExpr(call.arguments.nodes[0]!);
|
||||
const method = src.type.kind === "bytes" ? "setFrom" : "setFromDyn";
|
||||
const args = [
|
||||
src.type.kind === "bytes" ? src : lowerer.coerceInto(call.arguments.nodes[0]!, src, DYN),
|
||||
];
|
||||
if (nArgs === 2)
|
||||
args.push(
|
||||
lowerOptionalArgument(lowerer, call.arguments.nodes[1]!, F64, {
|
||||
kind: "numLit",
|
||||
value: 0,
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
);
|
||||
return { kind: "bytesIntrinsic", method, receiver, args, type: VOID, loc };
|
||||
return bytesSetIntrinsic(lowerer, receiver, call.arguments.nodes, loc);
|
||||
}
|
||||
if (name === "toString") {
|
||||
// Buffer's toString(encoding?) — utf8 by default. A 0-arg toString
|
||||
@@ -1271,6 +1379,8 @@ export function lowerBytesMethodCall(
|
||||
"the numeric families read/write Buffer bytes",
|
||||
);
|
||||
}
|
||||
if (!numericArguments(lowerer, call))
|
||||
return dynNumericCall(lowerer, call, access, name, receiverIr, loc);
|
||||
const write = name.startsWith("write");
|
||||
const required = write ? 1 : 0; // value; offset defaults to 0
|
||||
if (nArgs < required || nArgs > required + 1) {
|
||||
@@ -1448,6 +1558,39 @@ const BUF_BIGINT_METHODS: Record<string, { sign: boolean; le: boolean } | undefi
|
||||
writeBigUint64LE: { sign: false, le: true },
|
||||
};
|
||||
|
||||
/** Whether every argument of a numeric read/write is statically a number. */
|
||||
function numericArguments(lowerer: Lowerer, call: ts.CallExpression): boolean {
|
||||
return call.arguments.nodes.every((a) => lowerer.mapTypeOf(lowerer.typeOf(a))?.kind === "f64");
|
||||
}
|
||||
|
||||
/** A numeric read/write with an argument that is not a number (a string
|
||||
* offset from untyped JavaScript): the checked-dynamic Buffer method runs
|
||||
* Node's argument validation at runtime. */
|
||||
function dynNumericCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
name: string,
|
||||
receiverIr: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr);
|
||||
return {
|
||||
kind: "dynCheck",
|
||||
value: {
|
||||
kind: "dynInvoke",
|
||||
recv: { kind: "dynFrom", value: receiver, type: DYN, loc, liveRef: true },
|
||||
method: name,
|
||||
calleeName: ts.getTextOfNode(access),
|
||||
args: call.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, DYN)),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The variable-width quartet (offset + byteLength) → sign/endian token. */
|
||||
const BUF_NUM_VAR_METHODS: Record<string, string | undefined> = {
|
||||
readUIntBE: "ube",
|
||||
@@ -1807,7 +1950,12 @@ function knownBufferProducer(
|
||||
* or empty encoding and throws ERR_UNKNOWN_ENCODING for an unknown name; an
|
||||
* ArrayBuffer takes the second argument as its byte offset; other sources
|
||||
* ignore it. */
|
||||
function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrExpr {
|
||||
function dynBufferFromParts(
|
||||
source: IrExpr,
|
||||
encoding: IrExpr,
|
||||
loc: SrcLoc,
|
||||
length?: IrExpr,
|
||||
): IrExpr {
|
||||
const utf8: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc };
|
||||
const isString = (x: IrExpr): IrExpr => ({
|
||||
kind: "dynTest",
|
||||
@@ -1848,20 +1996,48 @@ function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrEx
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
},
|
||||
// An ArrayBuffer takes the second argument as its byte offset
|
||||
// (scr_buffer_from_dyn_offset_pooled); other values convert with utf8.
|
||||
else_: {
|
||||
kind: "libCall",
|
||||
fn: "buffer.poolFromDynOffset",
|
||||
args: [source, encoding],
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
},
|
||||
// An ArrayBuffer takes the second argument as its byte offset and the
|
||||
// third as its length (scr_buffer_from_dyn_range_pooled); other values
|
||||
// convert with utf8.
|
||||
else_: length
|
||||
? {
|
||||
kind: "libCall",
|
||||
fn: "buffer.poolFromDynRange",
|
||||
args: [source, encoding, length],
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "libCall",
|
||||
fn: "buffer.poolFromDynOffset",
|
||||
args: [source, encoding],
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
},
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Buffer.from(arrayBuffer, byteOffset[, length]): each argument in order
|
||||
* as a checked-dynamic value, read by the runtime as Node's
|
||||
* fromArrayBuffer reads them. */
|
||||
function bufferFromArrayBufferRange(
|
||||
lowerer: Lowerer,
|
||||
args: readonly ts.Expression[],
|
||||
loc: SrcLoc,
|
||||
first?: IrExpr,
|
||||
): IrExpr {
|
||||
const operands = [0, 1, 2].map((i) =>
|
||||
i === 0 && first
|
||||
? first
|
||||
: args[i]
|
||||
? lowerer.lowerExprExpecting(args[i]!, DYN)
|
||||
: dynUndefinedExpr(loc),
|
||||
);
|
||||
return { kind: "libCall", fn: "buffer.poolFromDynRange", args: operands, type: BYTES_U8, loc };
|
||||
}
|
||||
|
||||
/** Lowers `source` and `encNode` (or undefined) into hidden dyn locals, in
|
||||
* source order, and builds the result from their references. */
|
||||
function withDynBufferArguments(
|
||||
@@ -1909,52 +2085,81 @@ function dynBufferFromRuntimeEncoding(
|
||||
);
|
||||
}
|
||||
|
||||
/** The deprecated `new Buffer(arg[, encoding])` in a dependency's
|
||||
* JavaScript, through the global or an alias of it (buffer-crc32 and yazl
|
||||
* keep it as the fallback for runtimes without Buffer.alloc/Buffer.from).
|
||||
* Node never emits DEP0005 for calls from node_modules, so the construction
|
||||
* is exactly Node's: a number allocates zero-filled bytes (and throws
|
||||
* ERR_INVALID_ARG_TYPE beside a string second argument), anything else
|
||||
* converts as Buffer.from does. Null elsewhere: other callers owe the
|
||||
* deprecation warning. */
|
||||
export function lowerNodeModulesBufferConstruct(
|
||||
/** The deprecated `Buffer(arg[, encodingOrOffset[, length]])`, called or
|
||||
* constructed through the global or an alias of it, as Node's Buffer
|
||||
* function runs it: the arguments evaluate in order, DEP0005 is reported
|
||||
* on the next tick (once, and never for a call made from node_modules,
|
||||
* which only counts toward Node's 10000-call check), then a number
|
||||
* allocates zero-filled bytes (and throws ERR_INVALID_ARG_TYPE beside a
|
||||
* string second argument) and anything else converts as Buffer.from does.
|
||||
* Null for spread arguments and more than three. */
|
||||
export function lowerBufferConstruct(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.NewExpression,
|
||||
expr: ts.NewExpression | ts.CallExpression,
|
||||
): IrExpr | null {
|
||||
const sf = expr.getSourceFile();
|
||||
if (!isJsSourceFile(sf) || !/[\\/]node_modules[\\/]/.test(sf.fileName)) return null;
|
||||
if (ts.isCallExpression(expr) && expr.questionDotToken) return null;
|
||||
const args = expr.arguments?.nodes ?? [];
|
||||
if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) return null;
|
||||
if (args.length > 3 || args.some(ts.isSpreadElement)) return null;
|
||||
if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Buffer") return null;
|
||||
const loc = locOf(expr);
|
||||
const value = withDynBufferArguments(
|
||||
lowerer,
|
||||
lowerer.lowerExpr(args[0]!),
|
||||
args[1],
|
||||
loc,
|
||||
(source, encoding) => ({
|
||||
const locals = [0, 1, 2].slice(0, Math.max(args.length, 2)).map((i) => {
|
||||
const local = lowerer.declareHiddenLocal("%bufferArgument", DYN);
|
||||
const node = args[i];
|
||||
const init = node === undefined ? dynUndefinedExpr(loc) : lowerer.lowerExprExpecting(node, DYN);
|
||||
return { local, init };
|
||||
});
|
||||
const [source, second, third] = locals.map(({ local }) => varRef(local.id, DYN, loc)) as [
|
||||
IrExpr,
|
||||
IrExpr,
|
||||
IrExpr | undefined,
|
||||
];
|
||||
const inNodeModules = /[\\/]node_modules[\\/]/.test(expr.getSourceFile().fileName);
|
||||
const value: IrExpr = {
|
||||
kind: "ternary",
|
||||
cond: { kind: "dynTest", test: "number", value: source, type: BOOL, loc },
|
||||
then: {
|
||||
kind: "ternary",
|
||||
cond: { kind: "dynTest", test: "number", value: source, type: BOOL, loc },
|
||||
then: {
|
||||
kind: "ternary",
|
||||
cond: { kind: "dynTest", test: "string", value: encoding, type: BOOL, loc },
|
||||
then: { kind: "libCall", fn: "buffer.newStringFail", args: [source], type: BYTES_U8, loc },
|
||||
else_: {
|
||||
kind: "libCall",
|
||||
fn: "buffer.alloc",
|
||||
args: [{ kind: "dynCheck", value: source, type: F64, loc }],
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
},
|
||||
cond: { kind: "dynTest", test: "string", value: second, type: BOOL, loc },
|
||||
then: { kind: "libCall", fn: "buffer.newStringFail", args: [source], type: BYTES_U8, loc },
|
||||
else_: {
|
||||
kind: "libCall",
|
||||
fn: "buffer.alloc",
|
||||
args: [{ kind: "dynCheck", value: source, type: F64, loc }],
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
},
|
||||
else_: dynBufferFromParts(source, encoding, loc),
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
}),
|
||||
);
|
||||
return { kind: "libCall", fn: "buffer.brand", args: [value], type: BYTES_U8, loc };
|
||||
},
|
||||
else_: dynBufferFromParts(source, second, loc, third),
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
...locals.map(({ local, init }): IrStmt => ({
|
||||
kind: "varDecl",
|
||||
localId: local.id,
|
||||
init,
|
||||
loc,
|
||||
})),
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: inNodeModules ? "buffer.deprecationCount" : "buffer.deprecation",
|
||||
args: [],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: { kind: "libCall", fn: "buffer.brand", args: [value], type: BYTES_U8, loc },
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Buffer.from(array or bytes, extra): Node copies an array-like source and
|
||||
@@ -2018,7 +2223,16 @@ function lowerBufferStaticValue(
|
||||
lowerer.typeOf(args[0]!).getSymbol(),
|
||||
)
|
||||
) {
|
||||
return lowerArrayBufferView(lowerer, args, "u8", loc);
|
||||
// A byte offset or length follows Node's fromArrayBuffer (its
|
||||
// conversions and ERR_BUFFER_OUT_OF_BOUNDS), not the typed-array view.
|
||||
if (args.length === 1) return lowerArrayBufferView(lowerer, args, "u8", loc);
|
||||
return bufferFromArrayBufferRange(lowerer, args, loc);
|
||||
}
|
||||
// Buffer.from(value, byteOffset, length) over an untyped value: an
|
||||
// ArrayBuffer's range, or the two-argument conversion of other values.
|
||||
if (args.length === 3 && !args.some(ts.isSpreadElement)) {
|
||||
const first = lowerer.lowerExpr(args[0]!);
|
||||
if (first.type.kind === "dyn") return bufferFromArrayBufferRange(lowerer, args, loc, first);
|
||||
}
|
||||
// Buffer.from(x.buffer[, byteOffset[, length]]): a u8 VIEW sharing
|
||||
// x's storage (Node shares the ArrayBuffer; length is in BYTES).
|
||||
@@ -2445,11 +2659,101 @@ function lowerBufferStaticValue(
|
||||
const s = lowerer.lowerExprExpecting(args[0]!, STRING);
|
||||
return { kind: "libCall", fn: "buffer.isEncoding", args: [s], type: BOOL, loc };
|
||||
}
|
||||
// Any other single argument: Node answers false for non-strings.
|
||||
if (args.length === 1 && !ts.isSpreadElement(args[0]!)) {
|
||||
const value = lowerer.declareHiddenLocal("%bufferEncoding", DYN);
|
||||
const ref = varRef(value.id, DYN, loc);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: value.id,
|
||||
init: lowerer.lowerExprExpecting(args[0]!, DYN),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: { kind: "dynTest", test: "string", value: ref, type: BOOL, loc },
|
||||
right: {
|
||||
kind: "libCall",
|
||||
fn: "buffer.isEncoding",
|
||||
args: [{ kind: "dynCheck", value: ref, type: STRING, loc }],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
lowerer.noLowering(
|
||||
"Buffer.isEncoding with this argument shape",
|
||||
call,
|
||||
"one string-typed argument is the lowered form",
|
||||
"one argument is the lowered form",
|
||||
);
|
||||
}
|
||||
return null; // of, copyBytesFrom, ... → the SC2020 member fence
|
||||
}
|
||||
|
||||
/** `Buffer.prototype.<name>` or `Buffer.prototype[key]` read in JavaScript
|
||||
* (smart-buffer's `typeof Buffer.prototype[method]` probe): the member as
|
||||
* scr_buffer_prototype_member answers it. Null for other expressions. */
|
||||
export function lowerBufferPrototypeMember(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression | ts.ElementAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (expr.questionDotToken || !isJsSourceFile(expr.getSourceFile())) return null;
|
||||
const receiver = expr.expression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(receiver) ||
|
||||
receiver.questionDotToken ||
|
||||
receiver.name.text !== "prototype" ||
|
||||
stdlibGlobalNameOf(lowerer, receiver.expression) !== "Buffer"
|
||||
)
|
||||
return null;
|
||||
const loc = locOf(expr);
|
||||
const key: IrExpr = ts.isPropertyAccessExpression(expr)
|
||||
? {
|
||||
kind: "dynFrom",
|
||||
value: { kind: "strLit", value: expr.name.text, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
}
|
||||
: lowerer.lowerExprExpecting(expr.argumentExpression, DYN);
|
||||
return { kind: "libCall", fn: "buffer.prototypeMember", args: [key], type: DYN, loc };
|
||||
}
|
||||
|
||||
const BUFFER_VALUE_FNS: ReadonlySet<string> = new Set([
|
||||
"buffer.constructor",
|
||||
"buffer.staticValue",
|
||||
"buffer.prototypeMember",
|
||||
]);
|
||||
|
||||
/** A program that reads the Buffer function's value installs it in %main,
|
||||
* so a Buffer's `.constructor` answers the same function. */
|
||||
export function installBufferValue(functions: IrFunction[]): void {
|
||||
let found = false;
|
||||
const visitor = {
|
||||
expr: (e: IrExpr): boolean => {
|
||||
if (e.kind === "libCall" && BUFFER_VALUE_FNS.has(e.fn)) found = true;
|
||||
return !found;
|
||||
},
|
||||
stmt: (): boolean => !found,
|
||||
};
|
||||
for (const fn of functions) {
|
||||
for (const s of fn.body) everyStmt(s, visitor);
|
||||
if (found) break;
|
||||
}
|
||||
const main = functions.find((fn) => fn.name === ENTRY_NAME);
|
||||
if (!found || !main) return;
|
||||
main.body.unshift({
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: "buffer.installValue", args: [], type: VOID, loc: main.loc },
|
||||
loc: main.loc,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
/** Date methods whose receiver is only read for its time value: a Date
|
||||
* local used as their receiver can store the time value instead of a Date
|
||||
* object (builtins/dates.ts lowers them). */
|
||||
const TIME_VALUE_METHODS = new Set([
|
||||
"getTime",
|
||||
"valueOf",
|
||||
"toISOString",
|
||||
"toJSON",
|
||||
"toString",
|
||||
"toDateString",
|
||||
"toTimeString",
|
||||
"toUTCString",
|
||||
"getFullYear",
|
||||
"getUTCFullYear",
|
||||
"getMonth",
|
||||
"getUTCMonth",
|
||||
"getDate",
|
||||
"getUTCDate",
|
||||
"getDay",
|
||||
"getUTCDay",
|
||||
"getHours",
|
||||
"getUTCHours",
|
||||
"getMinutes",
|
||||
"getUTCMinutes",
|
||||
"getSeconds",
|
||||
"getUTCSeconds",
|
||||
"getMilliseconds",
|
||||
"getUTCMilliseconds",
|
||||
"getTimezoneOffset",
|
||||
]);
|
||||
|
||||
const RELATIONAL = new Set([
|
||||
ts.SyntaxKind.LessThanToken,
|
||||
ts.SyntaxKind.LessThanEqualsToken,
|
||||
ts.SyntaxKind.GreaterThanToken,
|
||||
ts.SyntaxKind.GreaterThanEqualsToken,
|
||||
]);
|
||||
|
||||
/** True when a local `const` (or a `let` that is never assigned again)
|
||||
* initialized with `new Date(...)` can keep the Date's time value instead
|
||||
* of a Date object: no use observes the Date's identity. Every use reads the time value as
|
||||
* a Date method receiver, a String() or template operand, a relational or
|
||||
* string `+` operand, or `new Date(d)`'s argument, inside the declaring
|
||||
* function, or inside the block or loop that scopes a module-level
|
||||
* binding (no closure captures it). Top-level module bindings keep the
|
||||
* object. Other uses box the time value into a new Date at the use. */
|
||||
export function isTimeValueDateDeclaration(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
): boolean {
|
||||
const known = lowerer.timeValueDateDecisions.get(decl);
|
||||
if (known !== undefined) return known;
|
||||
const answer = decideTimeValueDate(lowerer, decl);
|
||||
lowerer.timeValueDateDecisions.set(decl, answer);
|
||||
return answer;
|
||||
}
|
||||
|
||||
function decideTimeValueDate(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
|
||||
if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return false;
|
||||
// Only a Date the declaration creates: another reference to an existing
|
||||
// Date can mutate it (`const d = a; a.setTime(0)`), and d must see that.
|
||||
if (!createsDate(lowerer, decl.initializer)) return false;
|
||||
const sf = decl.getSourceFile();
|
||||
if (sf.isDeclarationFile || /\.[cm]?jsx?$/.test(sf.fileName)) return false;
|
||||
const list = decl.parent;
|
||||
if (!ts.isVariableDeclarationList(list)) return false;
|
||||
const isConst = (list.flags & ts.NodeFlags.Const) !== 0;
|
||||
const isLet = (list.flags & ts.NodeFlags.Let) !== 0;
|
||||
if (!isConst && !isLet) return false;
|
||||
if (!ts.isVariableStatement(list.parent) && !ts.isForStatement(list.parent)) return false;
|
||||
const fn = enclosingFunction(decl);
|
||||
// The uses to check: the function body, or for a binding inside a block
|
||||
// or loop at module level (a local of the module's initializer, not a
|
||||
// module binding) the block or loop that scopes it.
|
||||
let scope: ts.Node | undefined;
|
||||
if (fn !== null) scope = fn.body;
|
||||
else if (!inClassStaticBlock(decl)) scope = moduleBlockScope(decl);
|
||||
if (scope === undefined) return false;
|
||||
// The lib Date interface by its symbol first: mapping every candidate
|
||||
// binding's type would intern its unions before the lowering does.
|
||||
const typeSymbol = lowerer.typeOf(decl.name).getSymbol();
|
||||
if (
|
||||
typeSymbol === undefined ||
|
||||
ts.symbolName(typeSymbol) !== "Date" ||
|
||||
!typeSymbol.declarations.some(
|
||||
(d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
|
||||
)
|
||||
)
|
||||
return false;
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(decl.name))?.kind !== "date") return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
|
||||
if (!symbol) return false;
|
||||
let ok = true;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (!ok) return;
|
||||
if (ts.isIdentifier(node) && node !== decl.name && node.text === decl.name.text) {
|
||||
if (lowerer.checker.getSymbolAtLocation(node) === symbol) {
|
||||
if (enclosingFunction(node) !== fn || !readsTimeValue(lowerer, node)) ok = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(scope);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/** True for `new Date(...)` of the standard Date, through parentheses and
|
||||
* type-only wrappers: a Date no other reference can reach. */
|
||||
function createsDate(lowerer: Lowerer, init: ts.Expression): boolean {
|
||||
let e = init;
|
||||
while (
|
||||
ts.isParenthesizedExpression(e) ||
|
||||
ts.isAsExpression(e) ||
|
||||
ts.isSatisfiesExpression(e) ||
|
||||
ts.isTypeAssertion(e) ||
|
||||
ts.isNonNullExpression(e)
|
||||
)
|
||||
e = e.expression;
|
||||
return ts.isNewExpression(e) && lowerer.isStdlibGlobal(e.expression, "Date");
|
||||
}
|
||||
|
||||
/** The block or for statement that scopes a module-level binding which is
|
||||
* not a module binding: a declaration in a block or loop body, or in a
|
||||
* for statement's initializer. Undefined for a top-level declaration. */
|
||||
function moduleBlockScope(decl: ts.VariableDeclaration): ts.Node | undefined {
|
||||
const list = decl.parent;
|
||||
if (!list || !ts.isVariableDeclarationList(list)) return undefined;
|
||||
if (ts.isForStatement(list.parent)) return list.parent;
|
||||
const statement = list.parent;
|
||||
if (!ts.isVariableStatement(statement) || ts.isSourceFile(statement.parent)) return undefined;
|
||||
for (let cur: ts.Node | undefined = statement.parent; cur; cur = cur.parent) {
|
||||
if (ts.isBlock(cur) || ts.isCaseBlock(cur)) return cur;
|
||||
if (ts.isSourceFile(cur)) return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function inClassStaticBlock(node: ts.Node): boolean {
|
||||
for (let cur = node.parent; cur; cur = cur.parent) {
|
||||
if (ts.isClassStaticBlockDeclaration(cur)) return true;
|
||||
if (ts.isSourceFile(cur)) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function enclosingFunction(node: ts.Node): ts.FunctionLikeDeclaration | null {
|
||||
for (let cur = node.parent; cur; cur = cur.parent) {
|
||||
if (
|
||||
ts.isFunctionDeclaration(cur) ||
|
||||
ts.isFunctionExpression(cur) ||
|
||||
ts.isArrowFunction(cur) ||
|
||||
ts.isMethodDeclaration(cur) ||
|
||||
ts.isConstructorDeclaration(cur) ||
|
||||
ts.isGetAccessorDeclaration(cur) ||
|
||||
ts.isSetAccessorDeclaration(cur)
|
||||
)
|
||||
return cur;
|
||||
if (ts.isClassStaticBlockDeclaration(cur) || ts.isSourceFile(cur)) return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A use of the binding that reads only its time value. */
|
||||
function readsTimeValue(lowerer: Lowerer, id: ts.Identifier): boolean {
|
||||
let node: ts.Node = id;
|
||||
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
const parent = node.parent;
|
||||
if (ts.isPropertyAccessExpression(parent) && parent.expression === node) {
|
||||
const call = parent.parent;
|
||||
return (
|
||||
!parent.questionDotToken &&
|
||||
ts.isCallExpression(call) &&
|
||||
call.expression === parent &&
|
||||
!call.questionDotToken &&
|
||||
TIME_VALUE_METHODS.has(parent.name.text) &&
|
||||
lowerer.isStdlibMember(parent)
|
||||
);
|
||||
}
|
||||
if (ts.isTemplateSpan(parent)) return parent.expression === node;
|
||||
if (ts.isCallExpression(parent)) {
|
||||
return (
|
||||
parent.arguments.nodes.length === 1 &&
|
||||
parent.arguments.nodes[0] === node &&
|
||||
ts.isIdentifier(parent.expression) &&
|
||||
lowerer.isStdlibGlobal(parent.expression, "String")
|
||||
);
|
||||
}
|
||||
if (ts.isNewExpression(parent)) {
|
||||
return (
|
||||
parent.arguments?.nodes.length === 1 &&
|
||||
parent.arguments.nodes[0] === node &&
|
||||
ts.isIdentifier(parent.expression) &&
|
||||
lowerer.isStdlibGlobal(parent.expression, "Date")
|
||||
);
|
||||
}
|
||||
if (ts.isBinaryExpression(parent)) {
|
||||
const other = parent.left === node ? parent.right : parent.left;
|
||||
const otherKind = lowerer.mapTypeOf(lowerer.typeOf(other))?.kind;
|
||||
if (RELATIONAL.has(parent.operatorToken.kind)) return otherKind === "date";
|
||||
if (parent.operatorToken.kind === ts.SyntaxKind.PlusToken) return otherKind === "string";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -764,10 +764,21 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr
|
||||
(lowerer.unions
|
||||
.get(elem.unionId)
|
||||
?.arms.every(
|
||||
(a) => a.kind === "f64" || a.kind === "string" || a.kind === "bool" || isUnitType(a),
|
||||
(a) =>
|
||||
a.kind === "f64" ||
|
||||
a.kind === "string" ||
|
||||
a.kind === "bool" ||
|
||||
a.kind === "date" ||
|
||||
isUnitType(a),
|
||||
) ??
|
||||
false);
|
||||
if (elem.kind === "f64" || elem.kind === "string" || elem.kind === "bool" || joinableUnion) {
|
||||
if (
|
||||
elem.kind === "f64" ||
|
||||
elem.kind === "string" ||
|
||||
elem.kind === "bool" ||
|
||||
elem.kind === "date" ||
|
||||
joinableUnion
|
||||
) {
|
||||
const sep: IrExpr = { kind: "strLit", value: ",", type: STRING, loc: e.loc };
|
||||
return {
|
||||
kind: "arrIntrinsic",
|
||||
@@ -2195,14 +2206,41 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
loc,
|
||||
};
|
||||
if (bothStr) return { kind: "strCmp", op: cmpOp, left, right, type: BOOL, loc };
|
||||
// A Date converts to a new checked-dynamic Date on each crossing, so
|
||||
// comparisons keep their Date fence instead of comparing copies.
|
||||
if (
|
||||
left.type.kind !== "date" &&
|
||||
right.type.kind !== "date" &&
|
||||
lowerer.dynConvertible(left.type) &&
|
||||
lowerer.dynConvertible(right.type)
|
||||
) {
|
||||
// Dates compare by time value, the number hint of ToPrimitive. Both
|
||||
// operands evaluate before either converts, so a right operand that
|
||||
// mutates the left Date (`a < (a.setTime(t), b)`) is seen: with such
|
||||
// a right operand, both Dates bind first and their times read after.
|
||||
if (left.type.kind === "date" && right.type.kind === "date") {
|
||||
const time = (d: IrExpr): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "date.getTime",
|
||||
args: [d],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
if (isSafeToRepeat(right))
|
||||
return { kind: "bin", op: cmpOp, left: time(left), right: time(right), type: BOOL, loc };
|
||||
const leftDate = lowerer.declareHiddenLocal("%date", left.type);
|
||||
const rightDate = lowerer.declareHiddenLocal("%date", right.type);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: leftDate.id, init: left, loc },
|
||||
{ kind: "varDecl", localId: rightDate.id, init: right, loc },
|
||||
],
|
||||
result: {
|
||||
kind: "bin",
|
||||
op: cmpOp,
|
||||
left: time(varRef(leftDate.id, left.type, loc)),
|
||||
right: time(varRef(rightDate.id, right.type, loc)),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (lowerer.dynConvertible(left.type) && lowerer.dynConvertible(right.type)) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.compare",
|
||||
@@ -2416,6 +2454,7 @@ export function lowerStrictEquality(
|
||||
idLeft.type.kind === "map" ||
|
||||
idLeft.type.kind === "set" ||
|
||||
idLeft.type.kind === "regex" ||
|
||||
idLeft.type.kind === "date" ||
|
||||
idLeft.type.kind === "url" ||
|
||||
idLeft.type.kind === "object" ||
|
||||
idLeft.type.kind === "record" ||
|
||||
@@ -2452,14 +2491,7 @@ export function lowerStrictEquality(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// A Date converts to a new checked-dynamic Date on each crossing; its
|
||||
// identity has no representation, so equality keeps the fence.
|
||||
if (
|
||||
idLeft.type.kind !== "date" &&
|
||||
idRight.type.kind !== "date" &&
|
||||
lowerer.dynConvertible(idLeft.type) &&
|
||||
lowerer.dynConvertible(idRight.type)
|
||||
) {
|
||||
if (lowerer.dynConvertible(idLeft.type) && lowerer.dynConvertible(idRight.type)) {
|
||||
return {
|
||||
kind: "dynScalarEq",
|
||||
left: lowerer.coerceToExpected(idLeft, DYN),
|
||||
|
||||
@@ -72,6 +72,7 @@ import { isClassCallback } from "../class-callbacks.js";
|
||||
import { isKeyedFieldAccess } from "./keyed-field-targets.js";
|
||||
import { lowerFieldCompoundValue } from "./updates.js";
|
||||
import { readsHeldEvolvingArray } from "../lower-stmts.js";
|
||||
import { lowerBufferPrototypeMember } from "../containers/bytes.js";
|
||||
|
||||
/** `a[i]` reads. Only f64 indices into array receivers are modeled; JS
|
||||
* string-key element access (`a["length"]`) and string indexing (`s[0]`
|
||||
@@ -105,6 +106,8 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
|
||||
const en = lowerEnumAccess(lowerer, expr);
|
||||
if (en) return en;
|
||||
}
|
||||
const bufferMember = lowerBufferPrototypeMember(lowerer, expr);
|
||||
if (bufferMember) return bufferMember;
|
||||
const cachedModule = lowerRequireCacheElement(lowerer, expr);
|
||||
if (cachedModule) return cachedModule;
|
||||
const namespaceElement = lowerModuleNamespaceElement(lowerer, expr);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { type Lowerer, PoisonError } from "../lowerer.js";
|
||||
import {
|
||||
BOOL,
|
||||
BYTES_ELEMENT_NAME,
|
||||
DATE_T,
|
||||
DYN,
|
||||
F64,
|
||||
type IrExpr,
|
||||
@@ -171,6 +172,12 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
// when the checker types this USE as a single arm, maybeNarrow
|
||||
// bridges the tagged representation with a unionNarrow.
|
||||
const local = lowerer.resolveLocal(expr);
|
||||
// A Date local holding its time value reads as that Date: only uses
|
||||
// that read the time value reach here (date-values.ts).
|
||||
if (local && lowerer.timeValueDateLocals.has(local)) {
|
||||
const time: IrExpr = { kind: "varRef", localId: local.id, type: F64, loc };
|
||||
return { kind: "libCall", fn: "date.fromTime", args: [time], type: DATE_T, loc };
|
||||
}
|
||||
if (local) {
|
||||
// A switch can dispatch past a lexical declaration. Keep this
|
||||
// previously refused codec form out until switch-scope TDZ boxes
|
||||
@@ -744,6 +751,20 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc };
|
||||
const nativeAlias = stdlibGlobalNameOf(lowerer, expr);
|
||||
if (nativeAlias === "Buffer" && expr.text !== "Buffer") {
|
||||
// An alias through a cast of globalThis carries the cast's static type,
|
||||
// which the identity token below does not have.
|
||||
const aliasSymbol = identifierReadSymbol(lowerer, expr);
|
||||
if (aliasSymbol && lowerer.castGlobalAliases.has(aliasSymbol)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"the Buffer constructor read through a cast of globalThis as a value",
|
||||
"test it in a condition, read its typeof, or call its members",
|
||||
);
|
||||
}
|
||||
// JavaScript reads the Buffer function's value (builtinGlobalValue).
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(expr)))
|
||||
return { kind: "libCall", fn: "buffer.constructor", args: [], type: DYN, loc };
|
||||
return { kind: "strLit", value: "[builtin Buffer]", type: STRING, loc };
|
||||
}
|
||||
if (lowerer.isStdlibGlobal(expr, "fetch")) {
|
||||
|
||||
@@ -134,6 +134,7 @@ import {
|
||||
import { expandoMemberRead } from "../lower-expando.js";
|
||||
import { lowerProtocolPropertyRead } from "../class-protocol-dispatch.js";
|
||||
import { lowerReadlineProperty } from "../builtins/readline.js";
|
||||
import { lowerAsyncLocalStorageProperty } from "../builtins/async-context.js";
|
||||
import { lowerEventStaticProperty } from "../builtins/numbers.js";
|
||||
import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "../lower-stream.js";
|
||||
import { lowerValueStreamProperty } from "../builtins/hash-stream.js";
|
||||
@@ -153,6 +154,7 @@ import {
|
||||
lowerStringCodesValue,
|
||||
} from "../lower-builtin-values.js";
|
||||
import { lowerArrayFromValue } from "../containers/array-construction.js";
|
||||
import { lowerBufferPrototypeMember } from "../containers/bytes.js";
|
||||
import { lowerNetAddressStaticRead } from "../builtins/net-address.js";
|
||||
import { STATS_TYPE_TESTS } from "../builtins/filesystem.js";
|
||||
import { arrayValueRead, arrayValueType } from "../array-values.js";
|
||||
@@ -310,6 +312,10 @@ export function lowerPropertyAccessExpr(
|
||||
// Cloudflare probe) folds like `navigator.userAgent`.
|
||||
const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION);
|
||||
if (navigator) return navigator;
|
||||
{
|
||||
const member = lowerBufferPrototypeMember(lowerer, expr);
|
||||
if (member) return member;
|
||||
}
|
||||
// A dgram socket's method read as a value in JavaScript (`emit:
|
||||
// socket.emit.bind(socket)`): the socket crosses as its checked-dynamic
|
||||
// handle, whose methods are function values that dispatch on their
|
||||
@@ -833,6 +839,8 @@ export function lowerPropertyAccessExpr(
|
||||
// diagnostics_channel Channel receivers — name/hasSubscribers
|
||||
// over the f64 channel handle.
|
||||
lowerer.lowerDiagnosticsChannelProperty(expr) ??
|
||||
// AsyncLocalStorage receivers — the name option.
|
||||
lowerAsyncLocalStorageProperty(lowerer, expr) ??
|
||||
// readline Interface receivers — the history and option members
|
||||
// over the f64 interface handle.
|
||||
lowerReadlineProperty(lowerer, expr) ??
|
||||
@@ -1788,6 +1796,28 @@ export function lowerIntrinsicProperty(
|
||||
return lowerNumberParserValue(lowerer, numberMember, locOf(expr));
|
||||
if (lowerer.stdlibGlobalMember(expr, "Buffer") === "poolSize")
|
||||
return { kind: "libCall", fn: "buffer.poolSize", args: [], type: F64, loc: locOf(expr) };
|
||||
{
|
||||
const member = lowerer.stdlibGlobalMember(expr, "Buffer");
|
||||
if (member === "name")
|
||||
return { kind: "strLit", value: "Buffer", type: STRING, loc: locOf(expr) };
|
||||
if (member === "length") return numLit(3, locOf(expr));
|
||||
// JavaScript reads the statics from the Buffer function's value, so
|
||||
// each has one identity and Node's name and length.
|
||||
if (
|
||||
member !== null &&
|
||||
BUFFER_VALUE_STATICS.has(member) &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(expr))
|
||||
) {
|
||||
const loc = locOf(expr);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "buffer.staticValue",
|
||||
args: [{ kind: "strLit", value: member, type: STRING, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
const netAddressStatic = lowerNetAddressStaticRead(lowerer, expr);
|
||||
if (netAddressStatic) return netAddressStatic;
|
||||
if (lowerer.stdlibGlobalMember(expr, "Buffer") === "isBuffer")
|
||||
@@ -4192,6 +4222,8 @@ export function builtinFunctionValue(name: string, loc: SrcLoc): IrExpr {
|
||||
* null when it stays an identity token (JavaScript) or fenced. */
|
||||
export function builtinGlobalValue(canonical: string, loc: SrcLoc, js = false): IrExpr | null {
|
||||
if (js && GLOBAL_FUNCTION_VALUES.has(canonical)) return builtinFunctionValue(canonical, loc);
|
||||
if (js && canonical === "Buffer")
|
||||
return { kind: "libCall", fn: "buffer.constructor", args: [], type: DYN, loc };
|
||||
if (GLOBAL_CONSTRUCTOR_VALUES.has(canonical)) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
@@ -4319,6 +4351,22 @@ function fenceBuiltinConstructorStaticRead(
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The Buffer statics the Buffer function's value holds
|
||||
* (scr_buffer_constructor_value). */
|
||||
const BUFFER_VALUE_STATICS: ReadonlySet<string> = new Set([
|
||||
"from",
|
||||
"copyBytesFrom",
|
||||
"of",
|
||||
"alloc",
|
||||
"allocUnsafe",
|
||||
"allocUnsafeSlow",
|
||||
"isBuffer",
|
||||
"compare",
|
||||
"isEncoding",
|
||||
"concat",
|
||||
"byteLength",
|
||||
]);
|
||||
|
||||
/** Object.prototype members a plain record inherits, read as values. */
|
||||
const OBJECT_PROTOTYPE_VALUE_READS: ReadonlySet<string> = new Set([
|
||||
"constructor",
|
||||
|
||||
@@ -338,6 +338,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
// A codec in untyped storage is its record's handle box.
|
||||
const codec = (["TextEncoder", "TextDecoder"] as const).find((name) =>
|
||||
lowerer.isStdlibGlobal(expr.right, name),
|
||||
);
|
||||
if (codec && lowerer.mapTypeOf(lowerer.typeOf(expr.left))?.kind === "dyn") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "text.codecIs",
|
||||
args: [
|
||||
lowerer.lowerExprExpecting(expr.left, DYN),
|
||||
{ kind: "boolLit", value: codec === "TextDecoder", type: BOOL, loc },
|
||||
],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (lowerer.isStdlibGlobal(expr.right, "ReadableStream")) {
|
||||
const value = lowerer.lowerExprExpecting(expr.left, DYN);
|
||||
return { kind: "libCall", fn: "fetch.streamIs", args: [value], type: BOOL, loc };
|
||||
|
||||
@@ -88,6 +88,12 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc
|
||||
if (builtinClassRefOf(lowerer, expr.expression) !== null) {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
// The Buffer constructor and its aliases (`const B = globalThis.Buffer`,
|
||||
// `(globalThis as { Buffer?: B }).Buffer`): a value read of an alias is an
|
||||
// identity token, so its typeof is decided here.
|
||||
if (stdlibGlobalNameOf(lowerer, expr.expression) === "Buffer") {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr.expression)) {
|
||||
const member = expr.expression;
|
||||
const prototype = member.expression;
|
||||
|
||||
@@ -191,6 +191,10 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
|
||||
operatorUnsupported(lowerer, expr, "+", [raw.type]);
|
||||
}
|
||||
case ts.SyntaxKind.ExclamationToken: {
|
||||
// `!runtimeBuffer` over an alias of the Buffer global: Node always
|
||||
// defines Buffer (the condition twin is lowerCondition's).
|
||||
if (lowerer.isStdlibGlobal(expr.operand, "Buffer"))
|
||||
return { kind: "boolLit", value: false, type: BOOL, loc };
|
||||
// `!x` is ToBoolean-then-negate: f64/string operands go through toBool.
|
||||
const operand = lowerer.ensureBool(
|
||||
deferredSlotOr(
|
||||
|
||||
@@ -28,11 +28,206 @@ export function iteratorSlotAdapter(
|
||||
expr: IrExpr,
|
||||
expected: IrType & { kind: "generator" },
|
||||
): IrExpr | null {
|
||||
if (expr.type.kind === "dyn") return dynIteratorAdapter(lowerer, expr, expected);
|
||||
if (expr.type.kind === "dyn")
|
||||
return (
|
||||
collectionIteratorSlotValue(lowerer, expr, expected) ??
|
||||
dynIteratorAdapter(lowerer, expr, expected)
|
||||
);
|
||||
if (expr.type.kind === "generator") return generatorRetypeAdapter(lowerer, expr, expected);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `m.values()` (or keys/entries, or a Set's) on a typed Map or Set, which
|
||||
* lowers to a dynamic invocation on the boxed collection, flowing into an
|
||||
* iterator-typed slot (`values(): IterableIterator<T> { return
|
||||
* this.rows.values(); }`): an interned generator over the collection that
|
||||
* reads each entry typed, instead of the checked adapter over the boxed
|
||||
* native iterator (dynIteratorAdapter), which boxes every value and checks
|
||||
* it back. It keeps that adapter's protocol: the walk is lazy and live, and
|
||||
* since the native iterator has no return(), an early exit from a
|
||||
* consumer's loop parks it on the current entry instead of completing it,
|
||||
* so a later loop continues with the next entry. Null for any other source
|
||||
* or a slot whose yield type is not the collection's entry type. */
|
||||
function collectionIteratorSlotValue(
|
||||
lowerer: Lowerer,
|
||||
expr: IrExpr,
|
||||
expected: IrType & { kind: "generator" },
|
||||
): IrExpr | null {
|
||||
if (expr.kind !== "dynInvoke" || expr.args.length > 0 || expr.calleeNameValue) return null;
|
||||
const method = expr.method;
|
||||
if (method !== "values" && method !== "keys" && method !== "entries") return null;
|
||||
if (expr.recv.kind !== "dynFrom") return null;
|
||||
const receiver = expr.recv.value;
|
||||
const collT = receiver.type;
|
||||
if (collT.kind !== "map" && collT.kind !== "set") return null;
|
||||
if (expected.async || expected.yieldT.kind === "void") return null;
|
||||
if (expected.retT.kind !== "void" && expected.retT.kind !== "dyn") return null;
|
||||
if (expected.nextT.kind !== "undefinedT" && expected.nextT.kind !== "dyn") return null;
|
||||
const resultType = genResultRecord(
|
||||
expected.yieldT,
|
||||
expected.retT,
|
||||
lowerer.shapes,
|
||||
lowerer.unions,
|
||||
);
|
||||
if (!resultType) return null;
|
||||
const loc = expr.loc;
|
||||
const key = `collIterSlot:${method}:${typeKey(collT)}:${typeKey(expected)}`;
|
||||
let name = lowerer.valueHelpers.get(key);
|
||||
if (!name) {
|
||||
const coll = varRef("coll.0", collT, loc);
|
||||
const index = varRef("index.0", F64, loc);
|
||||
const native = varRef("native.0", DYN, loc);
|
||||
const resumedFlag = varRef("resumed.0", BOOL, loc);
|
||||
const keyT = collT.kind === "map" ? collT.key : collT.elem;
|
||||
const valueT = collT.kind === "map" ? collT.value : collT.elem;
|
||||
const read = (m: "iterKey" | "iterValue", t: IrType): IrExpr =>
|
||||
collT.kind === "map"
|
||||
? { kind: "mapIntrinsic", method: m, receiver: coll, args: [index], type: t, loc }
|
||||
: { kind: "setIntrinsic", method: "iterKey", receiver: coll, args: [index], type: t, loc };
|
||||
// entries() answers [key, value] pairs: only a slot whose yield type is
|
||||
// that pair as a record of the two fields takes the typed walk; a tuple
|
||||
// or other pair type keeps the checked adapter.
|
||||
if (method === "entries") {
|
||||
const pair =
|
||||
expected.yieldT.kind === "record" ? lowerer.shapes.get(expected.yieldT.shapeId) : undefined;
|
||||
const field = (name: string): IrType | undefined =>
|
||||
pair?.fields.find((f) => f.name === name)?.type;
|
||||
const first = field("0");
|
||||
const second = field("1");
|
||||
if (
|
||||
pair === undefined ||
|
||||
pair.fields.length !== 2 ||
|
||||
first === undefined ||
|
||||
second === undefined ||
|
||||
!typeEquals(first, keyT) ||
|
||||
!typeEquals(second, valueT)
|
||||
)
|
||||
return null;
|
||||
}
|
||||
const entry = (): IrExpr =>
|
||||
method === "keys" || (collT.kind === "set" && method === "values")
|
||||
? read("iterKey", keyT)
|
||||
: method === "values"
|
||||
? read("iterValue", valueT)
|
||||
: {
|
||||
kind: "recordLit",
|
||||
fields: [
|
||||
{ name: "0", value: read("iterKey", keyT) },
|
||||
{ name: "1", value: read("iterValue", valueT) },
|
||||
],
|
||||
type: expected.yieldT,
|
||||
loc,
|
||||
};
|
||||
if (!typeEquals(entry().type, expected.yieldT)) return null;
|
||||
// The entry is read once per step and kept: the parking yield of an
|
||||
// early exit answers it again rather than rereading a slot the loop may
|
||||
// have deleted.
|
||||
const current = varRef("current.0", expected.yieldT, loc);
|
||||
const suspend = (): IrStmt => ({
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "yieldExpr", value: current, type: resumeType(expected.nextT), loc },
|
||||
loc,
|
||||
});
|
||||
const finish: IrStmt = {
|
||||
kind: "return",
|
||||
value: expected.retT.kind === "dyn" ? dynUndefinedExpr(loc) : null,
|
||||
loc,
|
||||
};
|
||||
name = keyedHelperName(lowerer, "%coll.iterSlot", key);
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "coll.0", name: "coll", type: collT }],
|
||||
returnType: expected.retT,
|
||||
locals: [
|
||||
{ id: "coll.0", name: "coll", type: collT, mutable: false },
|
||||
{ id: "native.0", name: "native", type: DYN, mutable: false },
|
||||
{ id: "index.0", name: "index", type: F64, mutable: true },
|
||||
{ id: "resumed.0", name: "resumed", type: BOOL, mutable: true },
|
||||
{ id: "current.0", name: "current", type: expected.yieldT, mutable: false },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "native.0",
|
||||
init: {
|
||||
kind: "dynInvoke",
|
||||
recv: lowerer.coerceToExpected(coll, DYN),
|
||||
method: "values",
|
||||
calleeName: collT.kind === "map" ? "map.values" : "set.values",
|
||||
args: [],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "resumed.0",
|
||||
init: { kind: "boolLit", value: false, type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "while",
|
||||
cond: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
body: [
|
||||
{
|
||||
kind: "assign",
|
||||
localId: "index.0",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.collectionIteratorIndex",
|
||||
args: [native],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "<",
|
||||
left: index,
|
||||
right: { kind: "numLit", value: 0, type: F64, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [finish],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
setResumed(false, loc),
|
||||
{ kind: "varDecl", localId: "current.0", init: entry(), loc },
|
||||
{
|
||||
kind: "tryCatch",
|
||||
tryBody: [suspend(), setResumed(true, loc)],
|
||||
catchBody: null,
|
||||
catchLocalId: null,
|
||||
// An early exit parks on the current entry; the next step
|
||||
// resumes the walk after it.
|
||||
finallyBody: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "unary", op: "!", operand: resumedFlag, type: BOOL, loc },
|
||||
then: [suspend(), { kind: "continue", loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
generator: { yieldT: expected.yieldT, nextT: expected.nextT, resultType, tag: "!" },
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [receiver], type: expected, loc };
|
||||
}
|
||||
|
||||
/** `resumed.0 = value` — the adapters' early-exit marker around a yield. */
|
||||
function setResumed(value: boolean, loc: SrcLoc): IrStmt {
|
||||
return {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type KeyOrderSegment,
|
||||
} from "./key-order-witness.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
|
||||
const loc = { file: "order.ts", start: 0, end: 1 };
|
||||
/** `number | undefined`: the optional-field slot type. */
|
||||
@@ -40,6 +41,8 @@ function fixture(): Fixture {
|
||||
keyOrder: notes,
|
||||
unions: new Map([["u0", { id: "u0", arms: [F64, { kind: "undefinedT" }] }]]),
|
||||
classes: new Map(),
|
||||
methodAccessSites: new Map(),
|
||||
dynConvertible: () => true,
|
||||
armTag: (unionId: string, arm: IrType): number =>
|
||||
unionId === "u0" && arm.kind === "undefinedT" ? 1 : -1,
|
||||
recordFieldPresent: (obj: IrExpr, shapeId: string, field: string): IrExpr => ({
|
||||
@@ -700,3 +703,263 @@ test("a spread reads its source's order right after the literal evaluates the so
|
||||
const snapshot = (prologue[2] as IrStmt & { kind: "varDecl" }).localId;
|
||||
expect(orderIndexOf(lit)).toMatchObject({ kind: "varRef", localId: snapshot });
|
||||
});
|
||||
|
||||
/** `{ a, b }` with a declared table `["a", "b"]` and a literal `{ b, a }`
|
||||
* plus `{ ...value, a }` of the same shape, beside `extra`. */
|
||||
function selfCopy(extra: (f: Fixture, ab: IrType & { kind: "record" }) => IrStmt[]): string[] {
|
||||
const f = fixture();
|
||||
const ab = f.record(
|
||||
[
|
||||
["a", F64],
|
||||
["b", F64],
|
||||
],
|
||||
["a", "b"],
|
||||
);
|
||||
const self: KeyOrderSegment = { kind: "spread", shapeId: ab.shapeId, maybeAbsent: false };
|
||||
f.run([
|
||||
decl("v", f.literal(ab, ["b", "a"])),
|
||||
observe(f.literal(ab, ["a", "b"], [self, { kind: "key", name: "a", definite: true }])),
|
||||
...extra(f, ab),
|
||||
]);
|
||||
return f.order(ab)!;
|
||||
}
|
||||
|
||||
test("a literal that starts by spreading its own shape takes the other literals' order", () => {
|
||||
expect(selfCopy(() => [])).toEqual(["b", "a"]);
|
||||
});
|
||||
|
||||
test("a self copy of a shape a checked conversion builds cannot be ordered statically", () => {
|
||||
const parsed = (ab: IrType): IrExpr => ({
|
||||
kind: "dynCheck",
|
||||
value: ref("j", { kind: "dyn" }),
|
||||
type: ab,
|
||||
loc,
|
||||
});
|
||||
expect(selfCopy((_, ab) => [observe(parsed(ab))])).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
test("a replaceable method's checked result builds no record", () => {
|
||||
expect(
|
||||
selfCopy((f, ab) => {
|
||||
const result: IrExpr = { kind: "dynCheck", value: ref("r", { kind: "dyn" }), type: ab, loc };
|
||||
return [observe(f.notes.methodResult(result, "load"))];
|
||||
}),
|
||||
).toEqual(["b", "a"]);
|
||||
});
|
||||
|
||||
test("a replaceable method's checked result builds records when a replacement may return one", () => {
|
||||
expect(
|
||||
selfCopy((f, ab) => {
|
||||
// A site the census cannot see through (a computed write of some
|
||||
// value, a reflection call) may install a replacement returning a
|
||||
// record built in any order.
|
||||
(f.lowerer.methodAccessSites as Map<string, ts.Node[]>).set("load", [
|
||||
{ kind: ts.SyntaxKind.CallExpression } as unknown as ts.Node,
|
||||
]);
|
||||
const result: IrExpr = { kind: "dynCheck", value: ref("r", { kind: "dyn" }), type: ab, loc };
|
||||
return [observe(f.notes.methodResult(result, "load"))];
|
||||
}),
|
||||
).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
test("a replaceable method's result keeps its witness through an IR rewrite", () => {
|
||||
expect(
|
||||
selfCopy((f, ab) => {
|
||||
const result = f.notes.methodResult(
|
||||
{
|
||||
kind: "dynCheck",
|
||||
value: ref("r", { kind: "dyn" }),
|
||||
type: ab,
|
||||
loc,
|
||||
},
|
||||
"load",
|
||||
);
|
||||
const identity = { expr: (e: IrExpr): IrExpr => e, stmt: (st: IrStmt): IrStmt => st };
|
||||
observeRecordLiteralRebuilds((from, to) => f.notes.follow(from, to));
|
||||
try {
|
||||
const rebuilt = transformExpr(result, identity);
|
||||
expect(rebuilt).not.toBe(result);
|
||||
return [observe(rebuilt)];
|
||||
} finally {
|
||||
observeRecordLiteralRebuilds(null);
|
||||
}
|
||||
}),
|
||||
).toEqual(["b", "a"]);
|
||||
});
|
||||
|
||||
test("a checked conversion of a boxed primitive builds no record", () => {
|
||||
expect(
|
||||
selfCopy((_, ab) => {
|
||||
const missing: IrExpr = {
|
||||
kind: "dynFrom",
|
||||
value: { kind: "unitLit", unit: "undefined", type: { kind: "undefinedT" }, loc },
|
||||
type: { kind: "dyn" },
|
||||
loc,
|
||||
};
|
||||
return [observe({ kind: "dynCheck", value: missing, type: ab, loc })];
|
||||
}),
|
||||
).toEqual(["b", "a"]);
|
||||
});
|
||||
|
||||
test("a checked conversion into a class builds none of its fields' records", () => {
|
||||
expect(
|
||||
selfCopy((f, ab) => {
|
||||
(f.lowerer.classes as Map<string, unknown>).set("Holder", {
|
||||
def: { fields: [{ name: "value", type: ab }] },
|
||||
});
|
||||
const holder: IrType = { kind: "object", className: "Holder" };
|
||||
const checked: IrExpr = {
|
||||
kind: "dynCheck",
|
||||
value: ref("h", { kind: "dyn" }),
|
||||
type: holder,
|
||||
loc,
|
||||
};
|
||||
return [{ kind: "exprStmt", expr: checked, loc }];
|
||||
}),
|
||||
).toEqual(["b", "a"]);
|
||||
});
|
||||
|
||||
test("a checked function passes its record parameters out, and its callbacks' back in", () => {
|
||||
const callable = (params: IrType[]): IrType => ({
|
||||
kind: "func",
|
||||
params,
|
||||
ret: { kind: "string" },
|
||||
});
|
||||
const checkedAs = (type: IrType): IrStmt => ({
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "dynCheck", value: ref("fn", { kind: "dyn" }), type, loc },
|
||||
loc,
|
||||
});
|
||||
expect(selfCopy((_, ab) => [checkedAs(callable([ab]))])).toEqual(["b", "a"]);
|
||||
// A callback parameter receives values the JavaScript function builds.
|
||||
expect(selfCopy((_, ab) => [checkedAs(callable([callable([ab])]))])).toEqual(["a", "b"]);
|
||||
// So does a container parameter, which the function can write into.
|
||||
expect(selfCopy((_, ab) => [checkedAs(callable([{ kind: "array", elem: ab }]))])).toEqual([
|
||||
"a",
|
||||
"b",
|
||||
]);
|
||||
});
|
||||
|
||||
test("a generator resume builds only its result record, never the value it carries", () => {
|
||||
const f = fixture();
|
||||
const ab = f.record(
|
||||
[
|
||||
["a", F64],
|
||||
["b", F64],
|
||||
],
|
||||
["a", "b"],
|
||||
);
|
||||
const result = f.record(
|
||||
[
|
||||
["done", { kind: "bool" }],
|
||||
["value", ab],
|
||||
],
|
||||
["value", "done"],
|
||||
);
|
||||
const gen: IrType = {
|
||||
kind: "generator",
|
||||
yieldT: ab,
|
||||
retT: { kind: "void" },
|
||||
nextT: { kind: "undefinedT" },
|
||||
};
|
||||
const resume = (mode: "next" | "value", type: IrType): IrStmt =>
|
||||
observe({ kind: "genResume", mode, gen: ref("g", gen), arg: null, type, loc });
|
||||
f.run([
|
||||
observe(f.literal(ab, ["b", "a"])),
|
||||
observe(f.literal(result, ["done", "value"])),
|
||||
resume("value", ab),
|
||||
resume("next", result),
|
||||
]);
|
||||
expect(f.order(ab)).toEqual(["b", "a"]);
|
||||
// The result record itself is built in its fixed layout: today's table.
|
||||
expect(f.order(result)).toEqual(["value", "done"]);
|
||||
});
|
||||
|
||||
/** `{ a, b }` built by a checked conversion and by the literal `{ b, a }`,
|
||||
* copied by `{ ...value, a }` (evaluating the value into a binding, as
|
||||
* spread literals do), observed. Answers the table and the function. */
|
||||
function checkedSelfCopy(): { order: string[]; fn: IrFunction; copy: IrExpr } {
|
||||
const f = fixture();
|
||||
const ab = f.record(
|
||||
[
|
||||
["a", F64],
|
||||
["b", F64],
|
||||
],
|
||||
["a", "b"],
|
||||
);
|
||||
const value = ref("value", ab) as IrExpr & { kind: "varRef" };
|
||||
const self: KeyOrderSegment = { kind: "spread", shapeId: ab.shapeId, maybeAbsent: false, value };
|
||||
const literal = f.literal(ab, ["a", "b"], [self, { kind: "key", name: "a", definite: true }]);
|
||||
const copy: IrExpr = {
|
||||
kind: "seqExpr",
|
||||
stmts: [decl("value", { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: ab, loc })],
|
||||
result: literal,
|
||||
type: ab,
|
||||
loc,
|
||||
};
|
||||
const fn = f.run([observe(f.literal(ab, ["b", "a"])), observe(copy)]);
|
||||
return { order: f.order(ab)!, fn, copy };
|
||||
}
|
||||
|
||||
test("a spread of a value a checked conversion built carries the value's order over", () => {
|
||||
const { order, fn, copy } = checkedSelfCopy();
|
||||
// Values the literal builds enumerate in its order; copies of converted
|
||||
// values keep the order their record source holds.
|
||||
expect(order).toEqual(["b", "a"]);
|
||||
expect(copy).toMatchObject({
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: "value" },
|
||||
{ kind: "varDecl", localId: "%orderedSpread.0", init: { kind: "recordLit" } },
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "record.sourceSpread",
|
||||
args: [
|
||||
{ kind: "varRef", localId: "%orderedSpread.0" },
|
||||
{ kind: "varRef", localId: "value" },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
result: { kind: "varRef", localId: "%orderedSpread.0" },
|
||||
});
|
||||
expect(fn.locals.map((local) => local.id)).toEqual(["%orderedSpread.0"]);
|
||||
});
|
||||
|
||||
test("a spread of a converted value into a wider shape orders the wider shape", () => {
|
||||
const f = fixture();
|
||||
const ab = f.record(
|
||||
[
|
||||
["a", F64],
|
||||
["b", F64],
|
||||
],
|
||||
["a", "b"],
|
||||
);
|
||||
const abc = f.record(
|
||||
[
|
||||
["a", F64],
|
||||
["b", F64],
|
||||
["c", F64],
|
||||
],
|
||||
["c", "a", "b"],
|
||||
);
|
||||
const value = ref("value", ab) as IrExpr & { kind: "varRef" };
|
||||
const spread: KeyOrderSegment = {
|
||||
kind: "spread",
|
||||
shapeId: ab.shapeId,
|
||||
maybeAbsent: false,
|
||||
value,
|
||||
};
|
||||
const wide: IrExpr = {
|
||||
kind: "seqExpr",
|
||||
stmts: [decl("value", { kind: "dynCheck", value: ref("j", { kind: "dyn" }), type: ab, loc })],
|
||||
result: f.literal(abc, ["a", "b", "c"], [spread, { kind: "key", name: "c", definite: true }]),
|
||||
type: abc,
|
||||
loc,
|
||||
};
|
||||
f.run([observe(f.literal(ab, ["b", "a"])), observe(wide)]);
|
||||
expect(f.order(ab)).toEqual(["b", "a"]);
|
||||
expect(f.order(abc)).toEqual(["b", "a", "c"]);
|
||||
});
|
||||
|
||||
@@ -28,7 +28,25 @@
|
||||
* build keeps today's table, since another table would make values
|
||||
* parsed in today's order keep a source view; a shape object literals
|
||||
* also build takes the literals' order. Their order is unknown, so a
|
||||
* copy out of such a shape cannot be ordered statically;
|
||||
* copy out of such a shape cannot be ordered statically. A conversion
|
||||
* builds only the records it can create from a JavaScript value: none
|
||||
* for a boxed primitive or a class instance (a native instance whose
|
||||
* fields its own writes fill), none for a checked function's record
|
||||
* parameters, and none for the checked result of a typed call of a
|
||||
* method the program may replace on its instance. Without a replacement
|
||||
* that result is the method's own native value, boxed and checked back
|
||||
* unchanged; a replacement's result keeps its own order in its record
|
||||
* source, and only copies out of it follow the chosen table, as they
|
||||
* followed today's;
|
||||
* - a literal that starts by spreading a value of its own shape copies
|
||||
* that value's order. While no checked conversion builds the shape,
|
||||
* every value has the one chosen order, so the copy adds no witness;
|
||||
* - a literal that starts by spreading a value of a shape checked
|
||||
* conversions build (an ordered spread) gives the record it builds the
|
||||
* value's record source, when the value has one (record.sourceSpread).
|
||||
* Every value of a shape enumerates in its table or in the order its
|
||||
* record source holds, so the copy has the value's order either way,
|
||||
* and the literal resolves as if the value were in its table's order;
|
||||
* - a write to an optional key that may be absent appends it in Node. A
|
||||
* key no construction creates ("late-only") goes after every key a
|
||||
* construction can create; any other moving write goes after every key.
|
||||
@@ -49,6 +67,7 @@ import {
|
||||
shapeHasAccessorSlots,
|
||||
typeEquals,
|
||||
UNDEFINED_T,
|
||||
VOID,
|
||||
} from "../../ir/ir.js";
|
||||
import {
|
||||
everyExprChild,
|
||||
@@ -57,7 +76,9 @@ import {
|
||||
transformStmtList,
|
||||
} from "../../ir/traverse.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { ALWAYS_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
|
||||
import { recordMayKeepSource } from "./record-sources.js";
|
||||
|
||||
/** The statements a key-order helper runs for record `ref`: `walks[0]`,
|
||||
* its walk over the shape's table, or, for a shape with orderVariants, a
|
||||
@@ -112,6 +133,7 @@ export type KeyOrderSegment =
|
||||
export class KeyOrderNotes {
|
||||
private readonly literals = new Map<IrExpr, KeyOrderSegment[]>();
|
||||
private readonly copies = new Map<IrExpr, string>();
|
||||
private readonly methodResults = new Map<IrExpr, string>();
|
||||
/** Shapes a frontend order surface (keys, values/entries, inspect,
|
||||
* index-signature merges) walks. */
|
||||
readonly observed = new Set<string>();
|
||||
@@ -128,13 +150,22 @@ export class KeyOrderNotes {
|
||||
return node;
|
||||
}
|
||||
|
||||
/** The checked result of a typed call of method `name`, which the program
|
||||
* may replace on its instance (class-callbacks.ts classCallbackCall). */
|
||||
methodResult(node: IrExpr, name: string): IrExpr {
|
||||
this.methodResults.set(node, name);
|
||||
return node;
|
||||
}
|
||||
|
||||
/** A pass rebuilt `from` as `to` (traverse.ts mapExprChildren): the
|
||||
* rebuilt literal is the same construction, so it keeps the witness. */
|
||||
* rebuilt node is the same construction, so it keeps the witness. */
|
||||
follow(from: IrExpr, to: IrExpr): void {
|
||||
const segments = this.literals.get(from);
|
||||
if (segments !== undefined) this.literals.set(to, segments);
|
||||
const source = this.copies.get(from);
|
||||
if (source !== undefined) this.copies.set(to, source);
|
||||
const method = this.methodResults.get(from);
|
||||
if (method !== undefined) this.methodResults.set(to, method);
|
||||
}
|
||||
|
||||
observe(type: IrType): void {
|
||||
@@ -148,6 +179,11 @@ export class KeyOrderNotes {
|
||||
copyOf(node: IrExpr): string | undefined {
|
||||
return this.copies.get(node);
|
||||
}
|
||||
|
||||
/** The method whose checked result `node` is, if it is one. */
|
||||
methodResultOf(node: IrExpr): string | undefined {
|
||||
return this.methodResults.get(node);
|
||||
}
|
||||
}
|
||||
|
||||
const isArrayIndex = (name: string): boolean => {
|
||||
@@ -190,6 +226,14 @@ interface ShapeFacts {
|
||||
|
||||
type Resolved = { order: string[] } | "unknown";
|
||||
|
||||
/** A literal that starts with `...value`: the sequence that evaluates the
|
||||
* value into its binding and then builds the literal, and its function. */
|
||||
interface LeadingSpread {
|
||||
wrapper: IrExpr & { kind: "seqExpr" };
|
||||
literal: IrExpr & { kind: "recordLit" };
|
||||
fn: IrFunction;
|
||||
}
|
||||
|
||||
/** `structuredClone(v)` checked back into v's own type: the copy is built
|
||||
* from v's dyn view, so it creates exactly v's keys in v's order. */
|
||||
function orderPreservingClone(node: IrExpr & { kind: "dynCheck" }): boolean {
|
||||
@@ -201,6 +245,71 @@ function orderPreservingClone(node: IrExpr & { kind: "dynCheck" }): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
const PRIMITIVE_KINDS = new Set([
|
||||
"f64",
|
||||
"bigint",
|
||||
"string",
|
||||
"bool",
|
||||
"undefinedT",
|
||||
"nullT",
|
||||
"symbol",
|
||||
]);
|
||||
|
||||
/** A checked conversion of a boxed primitive (the `undefined` a dynamic
|
||||
* construction passes for a missing argument) answers that primitive or
|
||||
* throws: it builds no record. */
|
||||
function primitiveSource(node: IrExpr & { kind: "dynCheck" }): boolean {
|
||||
return node.value.kind === "dynFrom" && PRIMITIVE_KINDS.has(node.value.value.type.kind);
|
||||
}
|
||||
|
||||
/** Whether a census site for a method name can only read the method or
|
||||
* install a replacement whose result holds no object: a read
|
||||
* (`C.prototype.m`, `obj.m`) outside an assignment target, or an assignment
|
||||
* of a function expression whose return type is primitive. Other sites (a
|
||||
* computed write of another value, a reflection call, a property bag) may
|
||||
* install a replacement that returns a record. */
|
||||
function replacesWithoutRecords(lowerer: Lowerer, site: ts.Node): boolean {
|
||||
const parent = site.parent;
|
||||
if (ts.isPropertyAccessExpression(site) || ts.isElementAccessExpression(site))
|
||||
return !(
|
||||
parent !== undefined &&
|
||||
ts.isBinaryExpression(parent) &&
|
||||
parent.left === site &&
|
||||
parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
||||
parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
||||
);
|
||||
if (!ts.isBinaryExpression(site) || site.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
||||
return false;
|
||||
let value = site.right;
|
||||
while (ts.isParenthesizedExpression(value)) value = value.expression;
|
||||
if (!ts.isArrowFunction(value) && !ts.isFunctionExpression(value)) return false;
|
||||
const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(value));
|
||||
return (
|
||||
signatures.length > 0 &&
|
||||
signatures.every((signature) =>
|
||||
primitiveOnly(lowerer.checker.getReturnTypeOfSignature(signature)),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** A type whose values are primitives (or void): it holds no record. */
|
||||
function primitiveOnly(type: ts.Type): boolean {
|
||||
if (type.isUnionType()) return ts.constituentTypes(type).every(primitiveOnly);
|
||||
return (
|
||||
(type.flags &
|
||||
(ts.TypeFlags.StringLike |
|
||||
ts.TypeFlags.NumberLike |
|
||||
ts.TypeFlags.BooleanLike |
|
||||
ts.TypeFlags.BigIntLike |
|
||||
ts.TypeFlags.ESSymbolLike |
|
||||
ts.TypeFlags.Null |
|
||||
ts.TypeFlags.Undefined |
|
||||
ts.TypeFlags.Void |
|
||||
ts.TypeFlags.Never)) !==
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/** Chooses each in-scope record shape's enumeration table from its
|
||||
* witnesses. Runs once retained reachability has settled the historical
|
||||
* tables and derived metadata, before the order-snapshotting helper bodies
|
||||
@@ -239,35 +348,60 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
/** Every record shape a type can hold, through fields, arms, elements,
|
||||
* callables, promises and class fields. */
|
||||
/** Each walk shares one `seen` set per result set, so every type graph
|
||||
* is walked once however many roots reach it. */
|
||||
const shapesIn = (type: IrType, out: Set<string>, seen: Set<string>): void => {
|
||||
const visit = (t: IrType): boolean => {
|
||||
* is walked once however many roots reach it.
|
||||
*
|
||||
* `produced` walks only what a checked conversion into the type can
|
||||
* build. It answers an existing native instance for a class type (a plain
|
||||
* object cannot acquire a class brand; only %Error rebuilds), so the
|
||||
* records the instance's fields hold come from the class's own writes and
|
||||
* its checked field setters, each a witness of its own. A checked
|
||||
* function's parameters receive native values: a record parameter is
|
||||
* passed out, never built, though a callback among them receives values
|
||||
* built from JavaScript again. */
|
||||
const shapesIn = (type: IrType, out: Set<string>, seen: Set<string>, produced = false): void => {
|
||||
const visit = (t: IrType, built: boolean): boolean => {
|
||||
const mark = built ? "" : "-";
|
||||
// A container JavaScript builds converts into fresh native storage, so
|
||||
// its children are built too. One passed out stays shared: JavaScript
|
||||
// writes into it (built children) and reads native values out of it,
|
||||
// such as callbacks whose parameters it then builds (children passed
|
||||
// out). Those children count in both directions.
|
||||
const child = (type: IrType): boolean => {
|
||||
visit(type, true);
|
||||
if (!built) visit(type, false);
|
||||
return true;
|
||||
};
|
||||
if (t.kind === "record") {
|
||||
if (seen.has("r:" + t.shapeId)) return true;
|
||||
seen.add("r:" + t.shapeId);
|
||||
out.add(t.shapeId);
|
||||
if (seen.has(mark + "r:" + t.shapeId)) return true;
|
||||
seen.add(mark + "r:" + t.shapeId);
|
||||
if (built) out.add(t.shapeId);
|
||||
const shape = shapes.get(t.shapeId);
|
||||
if (shape) {
|
||||
for (const f of shape.fields) visit(f.type);
|
||||
if (shape.indexValue) visit(shape.indexValue);
|
||||
for (const f of shape.fields) child(f.type);
|
||||
if (shape.indexValue) child(shape.indexValue);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (t.kind === "union") {
|
||||
if (seen.has("u:" + t.unionId)) return true;
|
||||
seen.add("u:" + t.unionId);
|
||||
for (const arm of lowerer.unions.get(t.unionId)?.arms ?? []) visit(arm);
|
||||
if (seen.has(mark + "u:" + t.unionId)) return true;
|
||||
seen.add(mark + "u:" + t.unionId);
|
||||
for (const arm of lowerer.unions.get(t.unionId)?.arms ?? []) visit(arm, built);
|
||||
return true;
|
||||
}
|
||||
if (t.kind === "object") {
|
||||
if (seen.has("c:" + t.className)) return true;
|
||||
seen.add("c:" + t.className);
|
||||
for (const f of lowerer.classes.get(t.className)?.def.fields ?? []) visit(f.type);
|
||||
if (produced && t.className !== "%Error") return true;
|
||||
if (seen.has(mark + "c:" + t.className)) return true;
|
||||
seen.add(mark + "c:" + t.className);
|
||||
for (const f of lowerer.classes.get(t.className)?.def.fields ?? []) child(f.type);
|
||||
return true;
|
||||
}
|
||||
return everyTypeChild(t, visit);
|
||||
if (produced && t.kind === "func") {
|
||||
for (const param of t.params) visit(param, !built);
|
||||
return visit(t.ret, built);
|
||||
}
|
||||
return everyTypeChild(t, child);
|
||||
};
|
||||
visit(type);
|
||||
visit(type, true);
|
||||
};
|
||||
const opaqueShapes = new Set<string>();
|
||||
const opaqueSeen = new Set<string>();
|
||||
@@ -278,14 +412,28 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
* declaration's order, the order their table holds. */
|
||||
const declaredShapes = new Set<string>();
|
||||
const declaredSeen = new Set<string>();
|
||||
/** The checked result of a replaceable method call while every site that
|
||||
* can replace the method installs functions that return no record. */
|
||||
const unchangedMethodResult = (node: IrExpr): boolean => {
|
||||
const name = notes.methodResultOf(node);
|
||||
if (name === undefined) return false;
|
||||
const sites = lowerer.methodAccessSites.get(name) ?? [];
|
||||
return sites.every((site) => replacesWithoutRecords(lowerer, site));
|
||||
};
|
||||
const producedBy = (type: IrType, opaque: boolean): void => {
|
||||
if (opaque) shapesIn(type, opaqueShapes, opaqueSeen);
|
||||
else shapesIn(type, runtimeShapes, runtimeSeen);
|
||||
else shapesIn(type, runtimeShapes, runtimeSeen, true);
|
||||
};
|
||||
|
||||
// ── collection ──────────────────────────────────────────────────────
|
||||
let pos = 0;
|
||||
let fnName = "";
|
||||
let currentFn: IrFunction | null = null;
|
||||
/** Literals that start by spreading a binding, by their witness: where
|
||||
* each one is built. A literal's notes are shared by its rebuilt copies,
|
||||
* so `literalSites` counts every literal of each witness. */
|
||||
const leadingSpreads = new Map<KeyOrderSegment[], LeadingSpread[]>();
|
||||
const literalSites = new Map<KeyOrderSegment[], number>();
|
||||
const loops: number[] = [];
|
||||
let loopIds = 0;
|
||||
const loopKey = (): string => loops.join("/");
|
||||
@@ -337,8 +485,10 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
}
|
||||
const segments = notes.literalOf(e);
|
||||
const copy = notes.copyOf(e);
|
||||
if (segments !== undefined) f.literals.push(segments);
|
||||
else if (copy !== undefined) f.copies.push({ source: copy, keys: presentKeys });
|
||||
if (segments !== undefined) {
|
||||
f.literals.push(segments);
|
||||
literalSites.set(segments, (literalSites.get(segments) ?? 0) + 1);
|
||||
} else if (copy !== undefined) f.copies.push({ source: copy, keys: presentKeys });
|
||||
else f.pinned = true;
|
||||
return;
|
||||
}
|
||||
@@ -353,6 +503,18 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
case "assignExpr":
|
||||
noteInit(e.localId, e.value);
|
||||
return;
|
||||
case "seqExpr": {
|
||||
// A spread literal evaluates its sources into bindings first.
|
||||
const literal = e.result;
|
||||
if (literal.kind !== "recordLit" || currentFn === null) return;
|
||||
const segments = notes.literalOf(literal);
|
||||
const first = segments?.[0];
|
||||
if (segments === undefined || first?.kind !== "spread" || first.value === undefined) return;
|
||||
const list = leadingSpreads.get(segments) ?? [];
|
||||
list.push({ wrapper: e, literal, fn: currentFn });
|
||||
leadingSpreads.set(segments, list);
|
||||
return;
|
||||
}
|
||||
case "jsonStringify":
|
||||
case "dynFrom":
|
||||
observedRoots.push(e.value.type);
|
||||
@@ -380,15 +542,31 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
case "strIntrinsic":
|
||||
case "regexIntrinsic":
|
||||
case "bytesIntrinsic":
|
||||
// Generator results ({ done, value }) and Promise.withResolvers
|
||||
// records are built by the backend in a fixed layout.
|
||||
case "genResume":
|
||||
// Promise.withResolvers records are built by the backend in a fixed
|
||||
// layout.
|
||||
case "promiseWithResolvers":
|
||||
producedBy(e.type, true);
|
||||
return;
|
||||
case "genResume": {
|
||||
// A resume builds at most its IteratorResult record ({ done, value }
|
||||
// in a fixed layout; an async generator's settles a promise of it).
|
||||
// The value it carries is the generator's own yielded or returned
|
||||
// value, which the generator's body constructs. The record-free
|
||||
// drives build nothing: `value` moves that value out, and step,
|
||||
// close and a record-free delegate answer a boolean or nothing.
|
||||
if (e.mode === "step" || e.mode === "value" || e.mode === "close") return;
|
||||
const result = e.type.kind === "promise" ? e.type.inner : e.type;
|
||||
if (result.kind === "record") opaqueShapes.add(result.shapeId);
|
||||
return;
|
||||
}
|
||||
case "dynCheck":
|
||||
if (e.adopt === true) shapesIn(e.type, declaredShapes, declaredSeen);
|
||||
else if (!orderPreservingClone(e)) producedBy(e.type, false);
|
||||
// Conversions that build no record: a boxed primitive, and a
|
||||
// replaceable method's result while no replacement the program can
|
||||
// install returns a record: then it is the method's own value,
|
||||
// boxed and checked back unchanged.
|
||||
else if (!orderPreservingClone(e) && !primitiveSource(e) && !unchangedMethodResult(e))
|
||||
producedBy(e.type, false);
|
||||
return;
|
||||
case "dynDestrCheck":
|
||||
case "caughtCheck":
|
||||
@@ -440,6 +618,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
};
|
||||
for (const fn of fns) {
|
||||
fnName = fn.name;
|
||||
currentFn = fn;
|
||||
for (const st of fn.body) stmt(st);
|
||||
}
|
||||
for (const id of opaqueShapes) {
|
||||
@@ -556,6 +735,61 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
return edges;
|
||||
};
|
||||
|
||||
// ── ordered spreads ─────────────────────────────────────────────────
|
||||
// A value a checked conversion builds keeps its source's key order in
|
||||
// its record source when that order is not the table's (record-
|
||||
// sources.ts), so every value of a shape enumerates in the table's order
|
||||
// or in the order its record source holds. A literal that starts with
|
||||
// `...value` creates the value's keys first, in the value's order. When
|
||||
// the value's shape is one checked conversions build, the literal
|
||||
// carries that order over: after it builds the record it gives the
|
||||
// record a source holding the value's order (record.sourceSpread), and
|
||||
// the keys the value lacks follow in the table's order, which every
|
||||
// literal sequence of the shape agrees with. Such a literal resolves as
|
||||
// if its source were in the source's table order, and its own shape
|
||||
// counts as built at run time too, since its values may now carry a
|
||||
// source. It qualifies when every value of the source has all of the
|
||||
// source's keys, both shapes can keep a source, and every later spread
|
||||
// whose values may carry a source only overwrites the source's keys
|
||||
// (its own order cannot move them).
|
||||
const orderedSpreads = new Set<readonly KeyOrderSegment[]>();
|
||||
const keepsSource = (shapeId: string): boolean =>
|
||||
recordMayKeepSource(lowerer, { kind: "record", shapeId });
|
||||
const userKeys = (shapeId: string): string[] =>
|
||||
(shapes.get(shapeId)?.fields ?? []).map((x) => x.name).filter((k) => !k.startsWith("%"));
|
||||
const ordersSpread = (segments: KeyOrderSegment[], sites: readonly LeadingSpread[]): boolean => {
|
||||
const first = segments[0];
|
||||
if (first?.kind !== "spread" || first.maybeAbsent || first.value === undefined) return false;
|
||||
const sourceId = first.shapeId;
|
||||
if (facts.get(sourceId)?.runtime !== true || sites.length !== literalSites.get(segments))
|
||||
return false;
|
||||
const target = sites[0]!.literal.type;
|
||||
if (target.kind !== "record" || !keepsSource(sourceId) || !keepsSource(target.shapeId))
|
||||
return false;
|
||||
const localId = first.value.localId;
|
||||
const bound = ({ wrapper, literal }: LeadingSpread): boolean =>
|
||||
typeEquals(literal.type, target) &&
|
||||
wrapper.stmts.some(
|
||||
(st) =>
|
||||
st.kind === "varDecl" &&
|
||||
st.localId === localId &&
|
||||
st.init !== null &&
|
||||
st.init.type.kind === "record" &&
|
||||
st.init.type.shapeId === sourceId,
|
||||
);
|
||||
if (!sites.every(bound)) return false;
|
||||
const sourceKeys = new Set(userKeys(sourceId));
|
||||
if ([...sourceKeys].some((k) => maybeAbsent(sourceId, k))) return false;
|
||||
return segments
|
||||
.slice(1)
|
||||
.every(
|
||||
(s) =>
|
||||
s.kind === "key" ||
|
||||
(s.kind === "spread" &&
|
||||
(facts.get(s.shapeId)?.runtime !== true ||
|
||||
userKeys(s.shapeId).every((k) => sourceKeys.has(k)))),
|
||||
);
|
||||
};
|
||||
// ── witness sequences ───────────────────────────────────────────────
|
||||
/** The key sequences one literal can create, or null when a spread
|
||||
* source's order is unknown. A key sits at its first occurrence that
|
||||
@@ -565,15 +799,19 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
segments: readonly KeyOrderSegment[],
|
||||
keySet: ReadonlySet<string>,
|
||||
resolved: (id: string) => Resolved,
|
||||
raw: (id: string) => Resolved,
|
||||
): string[][] | null => {
|
||||
const occurrences: { key: string; definite: boolean }[] = [];
|
||||
const ordered = orderedSpreads.has(segments);
|
||||
for (const s of segments) {
|
||||
if (s.kind === "opaque") return null;
|
||||
if (s.kind === "key") {
|
||||
if (keySet.has(s.name)) occurrences.push({ key: s.name, definite: s.definite });
|
||||
continue;
|
||||
}
|
||||
const source = resolved(s.shapeId);
|
||||
// An ordered spread's values without a source enumerate in the
|
||||
// source's table; the others carry their order over.
|
||||
const source = ordered && s === segments[0] ? raw(s.shapeId) : resolved(s.shapeId);
|
||||
if (source === "unknown") return null;
|
||||
for (const k of source.order) {
|
||||
if (keySet.has(k))
|
||||
@@ -649,8 +887,35 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
for (const c of f.copies) deps.add(c.source);
|
||||
return [...deps];
|
||||
};
|
||||
/** `{ ...value, ...rest }` that starts with a spread of a value of the
|
||||
* literal's own shape holding every key: it creates its keys in the
|
||||
* value's order, and the later segments only overwrite them. With no
|
||||
* checked conversion building the shape, every value enumerates in the
|
||||
* one table this pass chooses, so the copy agrees with any choice and
|
||||
* adds no edge. */
|
||||
const copiesItself = (
|
||||
id: string,
|
||||
f: ShapeFacts,
|
||||
table: readonly string[],
|
||||
segments: readonly KeyOrderSegment[],
|
||||
): boolean => {
|
||||
const first = segments[0];
|
||||
return (
|
||||
first !== undefined &&
|
||||
first.kind === "spread" &&
|
||||
first.shapeId === id &&
|
||||
!first.maybeAbsent &&
|
||||
(!f.runtime || orderedSpreads.has(segments)) &&
|
||||
segments.every((s) => s.kind !== "opaque") &&
|
||||
table.every((k) => !maybeAbsent(id, k))
|
||||
);
|
||||
};
|
||||
/** One shape's order given its sources' current orders. */
|
||||
const resolveOne = (id: string, current: (source: string) => Resolved): Resolved => {
|
||||
const resolveOne = (
|
||||
id: string,
|
||||
current: (source: string) => Resolved,
|
||||
raw: (source: string) => Resolved,
|
||||
): Resolved => {
|
||||
const shape = shapes.get(id);
|
||||
if (!inScope(shape)) return "unknown";
|
||||
const table = tableOf(shape);
|
||||
@@ -666,7 +931,8 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
const edges: [string, string][] = [];
|
||||
if (f.pinned) chain(table, edges);
|
||||
for (const segments of f.literals) {
|
||||
const sequences = literalSequences(segments, keySet, current);
|
||||
if (copiesItself(id, f, table, segments)) continue;
|
||||
const sequences = literalSequences(segments, keySet, current, raw);
|
||||
if (sequences === null) return "unknown";
|
||||
for (const seq of sequences) chain(seq, edges);
|
||||
}
|
||||
@@ -725,32 +991,57 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
const known = result.get(id);
|
||||
return known === undefined ? "unknown" : asSource(id, known);
|
||||
};
|
||||
for (const scc of sccs) {
|
||||
const members = new Set(scc);
|
||||
const guess = new Map<string, Resolved>();
|
||||
for (const id of scc) {
|
||||
const shape = shapes.get(id);
|
||||
guess.set(id, inScope(shape) ? { order: tableOf(shape) } : "unknown");
|
||||
}
|
||||
const lookup = (id: string): Resolved =>
|
||||
members.has(id) ? asSource(id, guess.get(id)!) : external(id);
|
||||
let stable = false;
|
||||
for (let round = 0; round < scc.length + 3 && !stable; round++) {
|
||||
stable = true;
|
||||
const resolveAll = (): void => {
|
||||
result.clear();
|
||||
late.clear();
|
||||
for (const scc of sccs) {
|
||||
const members = new Set(scc);
|
||||
const guess = new Map<string, Resolved>();
|
||||
for (const id of scc) {
|
||||
const next = resolveOne(id, lookup);
|
||||
const prev = guess.get(id)!;
|
||||
const same =
|
||||
next === "unknown"
|
||||
? prev === "unknown"
|
||||
: prev !== "unknown" && prev.order.join("\0") === next.order.join("\0");
|
||||
if (!same) {
|
||||
stable = false;
|
||||
guess.set(id, next);
|
||||
const shape = shapes.get(id);
|
||||
guess.set(id, inScope(shape) ? { order: tableOf(shape) } : "unknown");
|
||||
}
|
||||
const lookup = (id: string): Resolved =>
|
||||
members.has(id) ? asSource(id, guess.get(id)!) : external(id);
|
||||
const rawLookup = (id: string): Resolved =>
|
||||
members.has(id) ? guess.get(id)! : (result.get(id) ?? "unknown");
|
||||
let stable = false;
|
||||
for (let round = 0; round < scc.length + 3 && !stable; round++) {
|
||||
stable = true;
|
||||
for (const id of scc) {
|
||||
const next = resolveOne(id, lookup, rawLookup);
|
||||
const prev = guess.get(id)!;
|
||||
const same =
|
||||
next === "unknown"
|
||||
? prev === "unknown"
|
||||
: prev !== "unknown" && prev.order.join("\0") === next.order.join("\0");
|
||||
if (!same) {
|
||||
stable = false;
|
||||
guess.set(id, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const id of scc) result.set(id, stable ? guess.get(id)! : "unknown");
|
||||
}
|
||||
for (const id of scc) result.set(id, stable ? guess.get(id)! : "unknown");
|
||||
};
|
||||
// Ordered spreads join once their source resolves; each one can let its
|
||||
// own shape resolve, which a later spread of it needs.
|
||||
resolveAll();
|
||||
for (let grew = true; grew;) {
|
||||
grew = false;
|
||||
for (const [segments, sites] of leadingSpreads) {
|
||||
const first = segments[0];
|
||||
if (first?.kind !== "spread" || orderedSpreads.has(segments)) continue;
|
||||
// A self copy resolves with its own shape.
|
||||
const target = sites[0]!.literal.type;
|
||||
const source = result.get(first.shapeId) ?? "unknown";
|
||||
const self = target.kind === "record" && target.shapeId === first.shapeId;
|
||||
if ((source === "unknown" && !self) || !ordersSpread(segments, sites)) continue;
|
||||
orderedSpreads.add(segments);
|
||||
if (target.kind === "record") factsOf(target.shapeId).runtime = true;
|
||||
grew = true;
|
||||
}
|
||||
if (grew) resolveAll();
|
||||
}
|
||||
|
||||
// ── observation ─────────────────────────────────────────────────────
|
||||
@@ -759,6 +1050,19 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
for (const id of notes.observed)
|
||||
shapesIn({ kind: "record", shapeId: id }, observed, observedSeen);
|
||||
for (const root of observedRoots) shapesIn(root, observed, observedSeen);
|
||||
// An ordered spread's copies of values without a source take the source's
|
||||
// order from its table, which must then hold the resolved order.
|
||||
const orderedTarget = (sites: readonly LeadingSpread[]): string | null => {
|
||||
const target = sites[0]!.literal.type;
|
||||
return target.kind === "record" && (result.get(target.shapeId) ?? "unknown") !== "unknown"
|
||||
? target.shapeId
|
||||
: null;
|
||||
};
|
||||
for (const [segments, sites] of leadingSpreads) {
|
||||
const first = segments[0];
|
||||
if (orderedSpreads.has(segments) && first?.kind === "spread" && orderedTarget(sites) !== null)
|
||||
observed.add(first.shapeId);
|
||||
}
|
||||
|
||||
// ── apply ───────────────────────────────────────────────────────────
|
||||
for (const [id, resolved] of result) {
|
||||
@@ -771,6 +1075,46 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
notes.changed.add(id);
|
||||
}
|
||||
|
||||
// Ordered spreads carry their source's order over (see above).
|
||||
let orderedResults = 0;
|
||||
const rewritten = new Set<IrExpr>();
|
||||
for (const [segments, sites] of leadingSpreads) {
|
||||
const first = segments[0];
|
||||
if (!orderedSpreads.has(segments) || first?.kind !== "spread" || first.value === undefined)
|
||||
continue;
|
||||
// A shape that did not resolve keeps today's table for every value.
|
||||
if (orderedTarget(sites) === null) continue;
|
||||
for (const { wrapper, literal, fn } of sites) {
|
||||
if (rewritten.has(wrapper)) continue;
|
||||
rewritten.add(wrapper);
|
||||
const loc = literal.loc;
|
||||
const localId = `%orderedSpread.${orderedResults++}`;
|
||||
fn.locals.push({ id: localId, name: "%orderedSpread", type: literal.type, mutable: false });
|
||||
const built: IrExpr = { kind: "varRef", localId, type: literal.type, loc };
|
||||
const source: IrExpr = {
|
||||
kind: "varRef",
|
||||
localId: first.value.localId,
|
||||
type: { kind: "record", shapeId: first.shapeId },
|
||||
loc,
|
||||
};
|
||||
wrapper.stmts.push(
|
||||
{ kind: "varDecl", localId, init: literal, loc },
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "record.sourceSpread",
|
||||
args: [built, source],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
);
|
||||
wrapper.result = { kind: "varRef", localId, type: literal.type, loc };
|
||||
}
|
||||
}
|
||||
|
||||
// ── per-instance orders ─────────────────────────────────────────────
|
||||
// A shape whose literals write its keys in more than one order has no
|
||||
// one table. Each value then records which of the orders it was written
|
||||
|
||||
@@ -119,7 +119,11 @@ import {
|
||||
} from "./containers/indexed-objects.js";
|
||||
import { lowerTupleReadMethodCall } from "./containers/array-methods.js";
|
||||
import { tryFuseIndexRead } from "./array-values.js";
|
||||
import { lowerBytesStaticCall } from "./containers/bytes.js";
|
||||
import {
|
||||
lowerBufferConstruct,
|
||||
lowerBytesPrototypeSetCall,
|
||||
lowerBytesStaticCall,
|
||||
} from "./containers/bytes.js";
|
||||
import {
|
||||
lowerRegexMethodCall,
|
||||
lowerStringIndexCall,
|
||||
@@ -363,6 +367,13 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
const codeGeneration = codeGenerationCalleeOf(lowerer, expr.expression);
|
||||
if (codeGeneration !== null) return lowerCodeGenerationCall(lowerer, expr, codeGeneration);
|
||||
}
|
||||
// The deprecated Buffer(...) call (containers/bytes.ts). The checker
|
||||
// types it `any`, so the bytes cross as a checked-dynamic value, which
|
||||
// keeps the Buffer brand for inspection.
|
||||
{
|
||||
const buffer = lowerBufferConstruct(lowerer, expr);
|
||||
if (buffer) return { kind: "dynFrom", value: buffer, type: DYN, loc };
|
||||
}
|
||||
// A package shim's runtime intrinsic (a bodiless `declare function`
|
||||
// named scriptcUnixDgram* in a shipped shim): a direct runtime call.
|
||||
if (ts.isIdentifier(expr.expression) && isShimIntrinsicName(expr.expression.text)) {
|
||||
@@ -1579,6 +1590,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
lowerer.lowerTestMethodCall(expr, expr.expression) ??
|
||||
lowerer.lowerTimeoutMethodCall(expr, expr.expression) ??
|
||||
lowerArrayPrototypeBorrowCall(lowerer, expr, expr.expression) ??
|
||||
lowerBytesPrototypeSetCall(lowerer, expr, expr.expression) ??
|
||||
lowerObjectOwnPrototypeCall(lowerer, expr, expr.expression) ??
|
||||
lowerObjectPrototypeCall(lowerer, expr, expr.expression) ??
|
||||
lowerStringPrototypeCall(lowerer, expr, expr.expression) ??
|
||||
@@ -8777,11 +8789,13 @@ function lowerObjectStaticCall(
|
||||
const moduleSymbol = source ? lowerer.checker.getSymbolAtLocation(source) : undefined;
|
||||
const names: string[] = [];
|
||||
moduleSymbol?.getExports().forEach((symbol, key) => {
|
||||
// Internal names (`__export`) start with `__`; an exported name that
|
||||
// does is keyed with an extra `_` (escapedSymbolName).
|
||||
const name = String(key);
|
||||
if (name.startsWith("__") || name === "export=") return;
|
||||
if ((name.startsWith("__") && !name.startsWith("___")) || name === "export=") return;
|
||||
const target =
|
||||
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
|
||||
if (target.flags & ts.SymbolFlags.Value) names.push(name);
|
||||
if (target.flags & ts.SymbolFlags.Value) names.push(ts.symbolName(symbol));
|
||||
});
|
||||
names.sort();
|
||||
const loc = locOf(call);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { lowerAsyncLocalStorageOptions } from "./builtins/async-context.js";
|
||||
import {
|
||||
DATE_TIME_FIELD,
|
||||
dateRootOf,
|
||||
@@ -46,7 +47,6 @@ import {
|
||||
DATE_T,
|
||||
DYN,
|
||||
F64,
|
||||
bytesOf,
|
||||
type IrClassDef,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
@@ -115,8 +115,7 @@ import { isCjsRequireGlobal } from "./lower-node-module.js";
|
||||
import { lowerArrayConstructor } from "./containers/array-construction.js";
|
||||
import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js";
|
||||
import { lowerObjectConstructor } from "./containers/indexed-objects.js";
|
||||
import { knownBufEncoding, lowerNodeModulesBufferConstruct } from "./containers/bytes.js";
|
||||
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
|
||||
import { knownBufEncoding, lowerBufferConstruct } from "./containers/bytes.js";
|
||||
import { lowerSearchParamsNew } from "./builtins/url.js";
|
||||
import { lowerTextCodecNew } from "./builtins/text-codecs.js";
|
||||
import {
|
||||
@@ -1425,7 +1424,7 @@ function undefArmedFieldType(lowerer: Lowerer, p: ts.Symbol): IrType | null {
|
||||
const arms =
|
||||
mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped];
|
||||
for (const a of arms) {
|
||||
if (a.kind === "map" || a.kind === "regex" || a.kind === "date" || a.kind === "generator") {
|
||||
if (a.kind === "map" || a.kind === "regex" || a.kind === "generator") {
|
||||
return null;
|
||||
}
|
||||
byKey.set(typeKey(a), a);
|
||||
@@ -3982,7 +3981,6 @@ export function collectClassShapeInner(
|
||||
declared.kind !== "union" &&
|
||||
declared.kind !== "dyn" &&
|
||||
declared.kind !== "map" &&
|
||||
declared.kind !== "date" &&
|
||||
declared.kind !== "generator" &&
|
||||
declared.kind !== "void" &&
|
||||
declared.kind !== "caught";
|
||||
@@ -4152,7 +4150,7 @@ export function collectClassShapeInner(
|
||||
// An own JS callable shadows its prototype declaration.
|
||||
// Keep the method in the prototype and store the assigned
|
||||
// value in the instance property bag.
|
||||
lowerer.prototypeMethodAccesses.set(name, assign);
|
||||
lowerer.noteMethodAccess(name, assign);
|
||||
continue;
|
||||
}
|
||||
const sym = lowerer.checker.getSymbolAtLocation(assign);
|
||||
@@ -4234,7 +4232,7 @@ export function collectClassShapeInner(
|
||||
}
|
||||
if (!key && (fields.has(fieldName) || hasAccessor(fieldName))) continue;
|
||||
if (!key && (methods.has(fieldName) || lowerer.findMethodOn(base, fieldName))) {
|
||||
lowerer.prototypeMethodAccesses.set(fieldName, lhs);
|
||||
lowerer.noteMethodAccess(fieldName, lhs);
|
||||
continue;
|
||||
}
|
||||
// A key already declared (own or inherited) makes later
|
||||
@@ -9781,9 +9779,9 @@ function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean {
|
||||
|
||||
export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
// A dependency's deprecated `new Buffer(...)` fallback (containers/bytes.ts).
|
||||
// The deprecated `new Buffer(...)` (containers/bytes.ts).
|
||||
{
|
||||
const buffer = lowerNodeModulesBufferConstruct(lowerer, expr);
|
||||
const buffer = lowerBufferConstruct(lowerer, expr);
|
||||
if (buffer) return buffer;
|
||||
}
|
||||
// new Function(...): Node's EvalError under
|
||||
@@ -10423,8 +10421,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
if (arg.type.kind === "string") {
|
||||
return { kind: "libCall", fn: "date.newString", args: [arg], type: DATE_T, loc };
|
||||
}
|
||||
// new Date(date): a new Date object with the same time value.
|
||||
if (arg.type.kind === "date") {
|
||||
return arg;
|
||||
const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [arg], type: F64, loc };
|
||||
return { kind: "libCall", fn: "date.newMs", args: [time], type: DATE_T, loc };
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`new Date of '${lowerer.fmt(arg.type)}' values`,
|
||||
@@ -10627,7 +10627,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
expr,
|
||||
`Map values of type '${lowerer.checker.typeToString(targs[1])}' ` +
|
||||
`(Map values must be number, string, boolean, records, class instances, functions, ` +
|
||||
`typed arrays, arrays, promises, Maps, Sets, or supported unions and checked values)`,
|
||||
`typed arrays, arrays, promises, Dates, Maps, Sets, or supported unions and checked values)`,
|
||||
);
|
||||
}
|
||||
lowerer.badType(expr, tsType);
|
||||
@@ -10771,21 +10771,23 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
lowerer.badType(expr, tsType);
|
||||
}
|
||||
// `new AsyncLocalStorage()` (node:async_hooks): a fresh store id —
|
||||
// an f64 handle (type-mapper.ts), the Channel story. Construction options
|
||||
// ({ defaultValue, name }) have no lowering yet.
|
||||
// an f64 handle (type-mapper.ts), the Channel story. The options form
|
||||
// ({ defaultValue, name }) passes the options object as a dyn value.
|
||||
if (
|
||||
symbol !== null &&
|
||||
ts.symbolName(symbol) === "AsyncLocalStorage" &&
|
||||
lowerer.isStdlibSymbol(symbol)
|
||||
) {
|
||||
if ((expr.arguments?.nodes.length ?? 0) > 0) {
|
||||
lowerer.noLowering(
|
||||
"new AsyncLocalStorage(options)",
|
||||
expr,
|
||||
"the zero-argument constructor is the supported form (defaultValue/name options have no lowering yet)",
|
||||
);
|
||||
}
|
||||
return { kind: "libCall", fn: "als.new", args: [], type: F64, loc };
|
||||
const options = expr.arguments?.nodes[0];
|
||||
if (options === undefined)
|
||||
return { kind: "libCall", fn: "als.new", args: [], type: F64, loc };
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "als.newOptions",
|
||||
args: [lowerAsyncLocalStorageOptions(lowerer, options)],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// `new Promise<T>((resolve) => ...)`: the ambient Promise constructor.
|
||||
if (symbol !== null && ts.symbolName(symbol) === "Promise" && lowerer.isStdlibSymbol(symbol)) {
|
||||
@@ -10860,43 +10862,6 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
// families carry pointed hints: each states WHY no honest static
|
||||
// lowering exists (or what to use instead).
|
||||
if (lowerer.isStdlibSymbol(symbol ?? undefined)) {
|
||||
// The deprecated `new Buffer(string, encoding?)` ctor's string arm
|
||||
// with a NON-STRING first argument and a string second: Node
|
||||
// throws ERR_INVALID_ARG_TYPE synchronously (and DEP0005 never
|
||||
// fires on this throwing path, so the compiled silence matches).
|
||||
// Every other new Buffer form keeps the fence below — the
|
||||
// constructing forms would owe the deprecation warning.
|
||||
if (
|
||||
expr.expression.text === "Buffer" &&
|
||||
expr.arguments?.nodes.length === 2 &&
|
||||
!expr.arguments.nodes.some(ts.isSpreadElement) &&
|
||||
lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind !== "string" &&
|
||||
lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!))?.kind === "string"
|
||||
) {
|
||||
const first = lowerer.lowerExpr(expr.arguments.nodes[0]!);
|
||||
if (
|
||||
first.kind === "unitLit" ||
|
||||
first.type.kind === "dyn" ||
|
||||
lowerer.dynConvertible(first.type)
|
||||
) {
|
||||
const got: IrExpr =
|
||||
first.type.kind === "dyn" ? first : { kind: "dynFrom", value: first, type: DYN, loc };
|
||||
// The encoding argument still evaluates in Node before the
|
||||
// throw only via the ctor body's later reads — it does NOT
|
||||
// observe it before throwing, so dropping it is exact for
|
||||
// effect-free operands; effectful ones keep the fence.
|
||||
const enc = lowerer.lowerExpr(expr.arguments.nodes[1]!);
|
||||
if (enc.kind === "strLit" || isSafeToRepeat(enc)) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "buffer.newStringFail",
|
||||
args: [got],
|
||||
type: bytesOf("u8"),
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const ctorHints: Record<string, string | undefined> = {
|
||||
RegExp: "use a regex literal (/pattern/flags) — constructed regexes have no lowering",
|
||||
String:
|
||||
|
||||
@@ -1087,6 +1087,10 @@ export function lowerCondition(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
lowerer.isStdlibGlobal(e.expression, "Object")
|
||||
)
|
||||
return { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) };
|
||||
// `if (runtimeBuffer)` over an alias of the Buffer global (a
|
||||
// capability probe): Node always defines Buffer.
|
||||
if (lowerer.isStdlibGlobal(e, "Buffer"))
|
||||
return { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) };
|
||||
if (ts.isPropertyAccessExpression(e)) {
|
||||
const presence = lowerPromiseThenPresence(lowerer, e, {
|
||||
kind: "boolLit",
|
||||
|
||||
@@ -333,7 +333,7 @@ export function stepDrivable(genT: GenType): boolean {
|
||||
|
||||
/** Whether a yield channel's values have an OUT-slot form `value` reads. */
|
||||
function hasOutSlot(yt: IrType): boolean {
|
||||
return yt.kind === "f64" || yt.kind === "date" || yt.kind === "bool" || isRefCounted(yt);
|
||||
return yt.kind === "f64" || yt.kind === "bool" || isRefCounted(yt);
|
||||
}
|
||||
|
||||
/** `step` and `value` resumes of a generator-typed borrowed read. */
|
||||
|
||||
@@ -15,7 +15,7 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { unsignedHex } from "../../format-integer.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { jsFuncNameOf, type Lowerer } from "./lowerer.js";
|
||||
import { isJsSourceFile } from "../program.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -2444,6 +2444,12 @@ function directCallableInspect(lowerer: Lowerer, node: ts.Expression, loc: SrcLo
|
||||
}
|
||||
const decl = sym.declarations[0];
|
||||
if (!decl) return null;
|
||||
// A JavaScript function can hold own properties (`f.x = 1`, computed
|
||||
// keys), which the runtime walk prints after its name.
|
||||
if (isJsSourceFile(node.getSourceFile())) {
|
||||
const type = lowerer.mapTypeOf(lowerer.typeOf(node));
|
||||
if (type?.kind === "func" && lowerer.dynConvertible(type)) return null;
|
||||
}
|
||||
if (ts.isFunctionDeclaration(decl) && decl.name) {
|
||||
return strLit(`[Function: ${decl.name.text}]`, loc);
|
||||
}
|
||||
@@ -2504,7 +2510,17 @@ export function lowerInspectValue(
|
||||
loc,
|
||||
);
|
||||
}
|
||||
const value = lowerer.lowerExpr(valueNode);
|
||||
let value = lowerer.lowerExpr(valueNode);
|
||||
// A JavaScript function renders through the runtime walk, which prints
|
||||
// its own properties after its name.
|
||||
if (
|
||||
value.type.kind === "func" &&
|
||||
isJsSourceFile(valueNode.getSourceFile()) &&
|
||||
lowerer.dynConvertible(value.type)
|
||||
) {
|
||||
const name = jsFuncNameOf(valueNode);
|
||||
value = { kind: "dynFrom", value, type: DYN, ...(name !== null ? { fnName: name } : {}), loc };
|
||||
}
|
||||
if (value.type.kind === "bytes") {
|
||||
// One runtime representation serves Buffer AND Uint8Array; their
|
||||
// renderings differ (<Buffer aa> vs Uint8Array(1) [ 170 ]), so the
|
||||
|
||||
@@ -29,6 +29,7 @@ import { lowerOptionalIndex } from "./undefined-index.js";
|
||||
import { processDataMemberValue, programReadsExitCode } from "./builtins/process.js";
|
||||
import { requireInitFlagStmt } from "./require-initialization.js";
|
||||
import { isJsonRequireDecl } from "./json-require.js";
|
||||
import { isTimeValueDateDeclaration } from "./date-values.js";
|
||||
import { bindingSource } from "../binding-source.js";
|
||||
import { lowerConstructorReturn } from "./constructor-returns.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
@@ -64,6 +65,7 @@ import {
|
||||
BOOL,
|
||||
BYTES_U8,
|
||||
CAUGHT,
|
||||
DATE_T,
|
||||
DYN,
|
||||
F64,
|
||||
type IrExpr,
|
||||
@@ -5415,6 +5417,16 @@ export function lowerVarDecl(
|
||||
if (alias) return alias;
|
||||
}
|
||||
|
||||
// A Date local whose identity no use observes stores its time value.
|
||||
if (isTimeValueDateDeclaration(lowerer, decl)) {
|
||||
const loc = locOf(decl);
|
||||
const date = lowerer.lowerExprExpecting(decl.initializer!, DATE_T);
|
||||
const local = lowerer.declareLocal(decl.name, decl.name.text, F64, isLet);
|
||||
lowerer.timeValueDateLocals.add(local);
|
||||
const init: IrExpr = { kind: "libCall", fn: "date.getTime", args: [date], type: F64, loc };
|
||||
return { kind: "varDecl", localId: local.id, init, loc };
|
||||
}
|
||||
|
||||
// A NULLISH generic binding (`const i: I<A & B> = null as any` — the
|
||||
// declared type has no mapping, the value is provably null/undefined
|
||||
// forever): no storage exists; reads know the value (member reads and
|
||||
@@ -6885,7 +6897,7 @@ function lowerDynSwitch(
|
||||
loc: locOf(expression),
|
||||
};
|
||||
}
|
||||
if (test.type.kind === "date" || !lowerer.dynConvertible(test.type)) {
|
||||
if (!lowerer.dynConvertible(test.type)) {
|
||||
lowerer.unsupported(
|
||||
"SC1100",
|
||||
expression,
|
||||
@@ -8749,13 +8761,20 @@ function destrSpellingOf(rhs: ts.Expression): string | null {
|
||||
return base === null ? null : `${base}.${e.name.text}`;
|
||||
}
|
||||
// `table[key]` and `list[0]`; V8 prints a numeric key in its canonical
|
||||
// number form (`o[1.50]` reads "o[1.5]").
|
||||
// number form (`o[1.50]` reads "o[1.5]"), and a string key as a property
|
||||
// name with its text decoded (`o["a.b"]` and `o[\`a.b\`]` read "o.a.b").
|
||||
// The message is a C string, so a key holding NUL keeps the refusal.
|
||||
if (ts.isElementAccessExpression(e) && !e.questionDotToken) {
|
||||
const base = destrSpellingOf(e.expression);
|
||||
const key = e.argumentExpression;
|
||||
if (base === null) return null;
|
||||
if (ts.isIdentifier(key)) return `${base}[${key.text}]`;
|
||||
if (ts.isNumericLiteral(key)) return `${base}[${String(Number(key.text))}]`;
|
||||
if (
|
||||
(ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) &&
|
||||
!key.text.includes("\0")
|
||||
)
|
||||
return `${base}.${key.text}`;
|
||||
return null;
|
||||
}
|
||||
if (ts.isCallExpression(e) && !e.questionDotToken) {
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
refuseUncheckedCycleReads,
|
||||
type CycleInitFlag,
|
||||
} from "./cycle-initialization.js";
|
||||
import { bindCompletionCallbacks } from "./completion-callbacks.js";
|
||||
import { isJsonRequireDecl, type JsonRequireModule } from "./json-require.js";
|
||||
import {
|
||||
checkRequireBindingAccesses,
|
||||
@@ -146,12 +147,14 @@ import {
|
||||
} from "./function-context.js";
|
||||
import type { FrontendServices } from "../services.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import type { FsPathPlan } from "./builtins/fs-paths.js";
|
||||
import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "./optional-arguments.js";
|
||||
import { type ProtocolDispatch } from "./class-protocol-dispatch.js";
|
||||
import { ClassDynamicDispatch, trackClassFieldCreation } from "./class-dynamic-dispatch.js";
|
||||
import { guardClassAttributeWrites } from "./class-attributes.js";
|
||||
import { guardFrozenWrites, markSloppyRecordWrites } from "./frozen-records.js";
|
||||
import { applyHandleProperties } from "./handle-properties.js";
|
||||
import { installBufferValue } from "./containers/bytes.js";
|
||||
import { ClassProtocols } from "./class-protocols.js";
|
||||
import { earlyReadVarSymbols } from "./var-hoisting.js";
|
||||
import { recordTextCodecClass } from "../../ir/ir.js";
|
||||
@@ -2339,6 +2342,10 @@ export class Lowerer {
|
||||
readonly runtimeOptionalFields = new Set<string>();
|
||||
/** Bindings initialized by a Date's `toJSON()` (analyzeDateJsonResults). */
|
||||
readonly dateJsonBindings = new Set<ts.Symbol>();
|
||||
/** isTimeValueDateDeclaration's answers, and the locals that hold a
|
||||
* Date's time value instead of a Date object (date-values.ts). */
|
||||
readonly timeValueDateDecisions = new Map<ts.Node, boolean>();
|
||||
readonly timeValueDateLocals = new Set<IrLocal>();
|
||||
/** Record bindings whose flow class reaches storage outside the class:
|
||||
* they keep the declared layout, so an initializer from unchecked reads
|
||||
* converts once instead of the binding adopting the widened variant. */
|
||||
@@ -2942,6 +2949,10 @@ export class Lowerer {
|
||||
* checks resolve through this map (stdlibGlobalNameOf), declarations
|
||||
* emit nothing. */
|
||||
readonly stdlibGlobalAliases = new Map<ts.Symbol, string>();
|
||||
/** The stdlibGlobalAliases bound through a cast of the global object
|
||||
* (`(globalThis as { Buffer?: B }).Buffer`). Their static type is the
|
||||
* cast's, so their value reads are refused (identifiers.ts). */
|
||||
readonly castGlobalAliases = new Set<ts.Symbol>();
|
||||
/** CJS export-table ACCESSORS (`module.exports = { get path() {...} }`),
|
||||
* lifted lazily as module-level functions and interned per accessor
|
||||
* declaration: member reads call the getter (lower-exprs). */
|
||||
@@ -3028,6 +3039,23 @@ export class Lowerer {
|
||||
/** Named method slots observed before class collection need a callable
|
||||
* ABI and shared prototype descriptors instead of call-site specialization. */
|
||||
readonly prototypeMethodAccesses = new Map<string, ts.Node>();
|
||||
/** Every site the census found for a name in prototypeMethodAccesses (which
|
||||
* keeps one): reads of the method and writes that may replace it. The
|
||||
* key-order witness asks which replacements can return records. */
|
||||
readonly methodAccessSites = new Map<string, ts.Node[]>();
|
||||
|
||||
/** Marks `name` as a method the program may read or replace at `site`. */
|
||||
noteMethodAccess(name: string, site: ts.Node): void {
|
||||
this.prototypeMethodAccesses.set(name, site);
|
||||
this.noteMethodSite(name, site);
|
||||
}
|
||||
|
||||
/** Records a site for `name` without changing its prototypeMethodAccesses entry. */
|
||||
noteMethodSite(name: string, site: ts.Node): void {
|
||||
const sites = this.methodAccessSites.get(name);
|
||||
if (sites === undefined) this.methodAccessSites.set(name, [site]);
|
||||
else sites.push(site);
|
||||
}
|
||||
/** Whether the program reads `Error.prototype`, the checked-dynamic
|
||||
* object an ES5 constructor function can inherit from, or has a class
|
||||
* whose heritage is evaluated at run time (`extends (Base ?? Error)`):
|
||||
@@ -3123,8 +3151,13 @@ export class Lowerer {
|
||||
* (builtins/fs-paths.ts): each lowers to the hidden local that holds its
|
||||
* value, which is null until the path's lowering has saved it. */
|
||||
readonly stagedArguments = new Map<ts.Expression, { localId: string; type: IrType } | null>();
|
||||
/** The staged arguments that follow each such URL path, in order. */
|
||||
readonly stagedAfterPath = new Map<ts.Expression, ts.Expression[]>();
|
||||
/** What each fs path argument of a URL-path call lowers to (the plans
|
||||
* of builtins/fs-paths.ts). */
|
||||
readonly fsPathPlans = new Map<ts.Expression, FsPathPlan>();
|
||||
/** The existsSync/fs.promises calls whose converted URL paths are being
|
||||
* lowered into the call itself (lowerFallibleFsUrlCall). A call may be
|
||||
* lowered more than once, and each lowering converts again. */
|
||||
readonly fallibleFsCalls = new Set<ts.CallExpression>();
|
||||
readonly moduleNsParamOverrides = new Map<
|
||||
ts.ParameterDeclaration,
|
||||
IrType & { kind: "moduleNs" }
|
||||
@@ -4224,7 +4257,7 @@ export class Lowerer {
|
||||
// exact mappings, while star exports resolve to the mapped declaration
|
||||
// owner through externalTypeSpecifiersByFile.
|
||||
const moduleSymbol = this.checker.getSymbolAtLocation(sf);
|
||||
const exported = moduleSymbol?.getExports().get(exportName as ts.__String);
|
||||
const exported = moduleSymbol?.getExports().get(ts.escapedSymbolName(exportName));
|
||||
return this.externalTypeSpecifierOfSymbol(exported, seenSymbols, seenExports);
|
||||
}
|
||||
|
||||
@@ -4367,7 +4400,7 @@ export class Lowerer {
|
||||
cjsModuleExportSymbol(sf: ts.SourceFile, name: string): ts.Symbol | undefined {
|
||||
const moduleSym = this.checker.getSymbolAtLocation(sf);
|
||||
const exports = moduleSym?.getExports();
|
||||
const direct = exports?.get(name as ts.__String);
|
||||
const direct = exports?.get(ts.escapedSymbolName(name));
|
||||
if (direct) return direct;
|
||||
// tsgo represents `module.exports = { ... }` as one `export=` symbol
|
||||
// and does not duplicate the object literal's properties into the
|
||||
@@ -7968,6 +8001,7 @@ export class Lowerer {
|
||||
guardClassAttributeWrites(this, functions);
|
||||
guardFrozenWrites(this, functions);
|
||||
applyHandleProperties(this, functions);
|
||||
installBufferValue(functions);
|
||||
|
||||
// Globals typed by a class that never REGISTERED (a JS class whose
|
||||
// collection fenced — Symbol-keyed fields, an unsupported base): the
|
||||
@@ -8086,6 +8120,7 @@ export class Lowerer {
|
||||
};
|
||||
if (module) sanitizeUnregisteredClassTypes(module, (name) => this.classes.has(name));
|
||||
if (module) checkCycleBindingAccesses(this, module);
|
||||
if (module) bindCompletionCallbacks(module);
|
||||
// Execution tiers from the final bodies (ir/tiers.ts). Assigned with or
|
||||
// without a module: coverage reports the dynamic tier of programs that
|
||||
// still have blockers.
|
||||
@@ -14147,6 +14182,9 @@ export class Lowerer {
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// The buffer module's Buffer is the global constructor's value.
|
||||
if (bi.module === "buffer" && bi.member === "Buffer")
|
||||
return { kind: "libCall", fn: "buffer.constructor", args: [], type: DYN, loc };
|
||||
if (bi.module === "fs" && (bi.member === "realpathSync" || bi.member === "realpathSync.native"))
|
||||
return fsRealpathValue(this, bi.member === "realpathSync.native", loc);
|
||||
const fn = builtinModuleFnOf(this, bi.module, bi.member);
|
||||
|
||||
@@ -6,6 +6,20 @@ function unparenthesized(e: ts.Expression): ts.Expression {
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
return e;
|
||||
}
|
||||
|
||||
/** `e` without parentheses and type-only wrappers (as, satisfies, type
|
||||
* assertions, non-null assertions), which change no runtime value. */
|
||||
function withoutTypeCasts(e: ts.Expression): ts.Expression {
|
||||
while (
|
||||
ts.isParenthesizedExpression(e) ||
|
||||
ts.isAsExpression(e) ||
|
||||
ts.isSatisfiesExpression(e) ||
|
||||
ts.isTypeAssertion(e) ||
|
||||
ts.isNonNullExpression(e)
|
||||
)
|
||||
e = e.expression;
|
||||
return e;
|
||||
}
|
||||
import { UNSUPPORTED } from "../../diagnostics/diagnostic.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -1563,6 +1577,7 @@ export const BUILTIN_MODULE_FNS: Record<
|
||||
generateKeyPair: { fn: "crypto.generateKeyPairCb", params: [], result: VOID },
|
||||
sign: { fn: "crypto.signOneshot", params: [], result: DYN },
|
||||
verify: { fn: "crypto.verifyOneshot", params: [], result: DYN },
|
||||
diffieHellman: { fn: "crypto.diffieHellman", params: [], result: DYN },
|
||||
pbkdf2Sync: {
|
||||
fn: "crypto.pbkdf2",
|
||||
params: [BYTES_U8, BYTES_U8, F64, F64, STRING],
|
||||
@@ -2070,7 +2085,16 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<
|
||||
"fs.readFdEncoded",
|
||||
"fs.readFdSync",
|
||||
"fs.readFdSyncBytes",
|
||||
// The runtime encoding's check before a URL path converts
|
||||
// (fs-paths.ts).
|
||||
"fs.checkReadEncoding",
|
||||
],
|
||||
// The string source's check before a URL destination converts
|
||||
// (fs-paths.ts).
|
||||
copyFileSync: ["fs.checkPath"],
|
||||
renameSync: ["fs.checkPath"],
|
||||
copyFile: ["fs.checkPath"],
|
||||
rename: ["fs.checkPath"],
|
||||
readlinkSync: ["fs.readlinkSyncStr", "fs.readlinkSyncBuffer"],
|
||||
// The bytes-data form and the { mode } options form.
|
||||
writeFileSync: ["fs.writeFileSyncBytes", "fs.writeFileModeSync"],
|
||||
@@ -3101,9 +3125,6 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record<
|
||||
"createSign, createVerify, createECDH, and createDiffieHellman",
|
||||
]),
|
||||
),
|
||||
diffieHellman:
|
||||
"crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; " +
|
||||
"the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets",
|
||||
setFips:
|
||||
"a compiled binary has no FIPS provider to enable, and Node itself throws on setFips(true) " +
|
||||
"in a non-FIPS build — getFips() answers 0 here",
|
||||
@@ -3440,6 +3461,20 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin
|
||||
return ts.symbolName(resolved) === "global" ? "globalThis" : ts.symbolName(resolved);
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
|
||||
// The buffer module's Buffer (`require("buffer").Buffer`, `buffer_1.Buffer`)
|
||||
// is the global constructor.
|
||||
if (bufferNamespaceMember(lowerer, expr)) return "Buffer";
|
||||
// A cast of the global object (`(globalThis as { Buffer?: B }).Buffer`,
|
||||
// `(globalThis as any).Buffer`) changes only its static type: the
|
||||
// member is the global object's own.
|
||||
const uncast = withoutTypeCasts(expr.expression);
|
||||
if (uncast !== unparenthesized(expr.expression)) {
|
||||
if (!ts.isIdentifier(uncast) || stdlibGlobalNameOf(lowerer, uncast) !== "globalThis")
|
||||
return null;
|
||||
const member = lowerer.checker.getPropertyOfType(lowerer.typeOf(uncast), expr.name.text);
|
||||
if (!member || !lowerer.isStdlibSymbol(member)) return null;
|
||||
return ts.symbolName(member) === "global" ? "globalThis" : ts.symbolName(member);
|
||||
}
|
||||
if (stdlibGlobalNameOf(lowerer, expr.expression) !== "globalThis") return null;
|
||||
// Through a detection chain the checker types the receiver as the
|
||||
// union of every arm; the selected arm (the global object) owns the
|
||||
@@ -3572,9 +3607,28 @@ export function stdlibGlobalAliasDecl(
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
|
||||
if (!symbol) return false;
|
||||
lowerer.stdlibGlobalAliases.set(symbol, name);
|
||||
if (init !== undefined && castGlobalAliasSource(lowerer, init))
|
||||
lowerer.castGlobalAliases.add(symbol);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** True when an alias initializer reads its global through a cast of the
|
||||
* global object (`(globalThis as { Buffer?: B }).Buffer`), directly or as
|
||||
* the left operand of `||` (stdlibGlobalAliasNameOf's guard), or copies an
|
||||
* alias made that way. Such an alias carries the cast's static type. */
|
||||
function castGlobalAliasSource(lowerer: Lowerer, init: ts.Expression): boolean {
|
||||
let source = unparenthesized(init);
|
||||
if (ts.isBinaryExpression(source) && source.operatorToken.kind === ts.SyntaxKind.BarBarToken)
|
||||
source = unparenthesized(source.left);
|
||||
if (ts.isPropertyAccessExpression(source))
|
||||
return withoutTypeCasts(source.expression) !== unparenthesized(source.expression);
|
||||
if (ts.isIdentifier(source)) {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(source);
|
||||
return symbol !== undefined && lowerer.castGlobalAliases.has(symbol);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** True iff the symbol is declared ONLY by the adopted @types/node
|
||||
* surface (no es-lib or shipped-ambient declaration merges in) — chooses
|
||||
* the SC2020 fence's wording: "typed by @types/node", not "standard
|
||||
|
||||
@@ -1378,6 +1378,11 @@ function suppressedJsStrictness7(d: ts.Diagnostic): boolean {
|
||||
if (JS_ANY_OPERATOR_CODES.has(d.code)) {
|
||||
return ts.flattenDiagnosticMessageText(d, "\n").includes("'any'");
|
||||
}
|
||||
// The deprecated Buffer(...) call without `new`, which Node's Buffer
|
||||
// function accepts (containers/bytes.ts lowers it).
|
||||
if (d.code === 2348) {
|
||||
return ts.flattenDiagnosticMessageText(d, "\n").includes("'BufferConstructor'");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -6437,7 +6442,7 @@ function analyzeEsmNamedImportLinks(
|
||||
const nextSeen = new Set(seenModules);
|
||||
nextSeen.add(dep);
|
||||
const module = checker.getSymbolAtLocation(dep);
|
||||
const exported = module?.getExports().get(name as ts.__String);
|
||||
const exported = module?.getExports().get(ts.escapedSymbolName(name));
|
||||
if (exported !== undefined) {
|
||||
let resolved = exported;
|
||||
const seenSymbols = new Set<ts.Symbol>();
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
SemanticSnapshot,
|
||||
SemanticType,
|
||||
symbolNameOf,
|
||||
escapedSymbolName,
|
||||
} from "./semantic-model.js";
|
||||
import type { __String } from "./ast-types.js";
|
||||
import {
|
||||
@@ -313,6 +314,11 @@ test("symbol tables keep escaped keys, display names, canonical context and cach
|
||||
const exports = parent.getExports();
|
||||
expect(symbolNameOf(members.get("___x" as __String))).toBe("__x");
|
||||
expect(symbolNameOf(exports.get("__x" as __String))).toBe("__x");
|
||||
// A name that starts with `__` is keyed with an extra `_`.
|
||||
expect(symbolNameOf(members.get(escapedSymbolName("__x")))).toBe("__x");
|
||||
expect(escapedSymbolName("x")).toBe("x");
|
||||
expect(escapedSymbolName("_x")).toBe("_x");
|
||||
expect(escapedSymbolName("___y")).toBe("____y");
|
||||
expect(parent.getMembers()).toBe(members);
|
||||
expect(parent.getExports()).toBe(exports);
|
||||
expect(parent.getParent()).toBe(first.symbol(symbol(2)));
|
||||
|
||||
@@ -454,6 +454,12 @@ export function symbolName(symbol: SemanticSymbol): string {
|
||||
return name.startsWith("___") ? name.slice(1) : name;
|
||||
}
|
||||
|
||||
/** The key of the name `name` in a symbol table (`getExports()`, `getMembers()`): the name, with
|
||||
* an extra `_` when it starts with `__`. The inverse of `symbolName`. */
|
||||
export function escapedSymbolName(name: string): __String {
|
||||
return (name.startsWith("__") ? `_${name}` : name) as __String;
|
||||
}
|
||||
|
||||
/** `symbolName(symbol)`, or undefined without a symbol. */
|
||||
export function symbolNameOf(symbol: SemanticSymbol | null | undefined): string | undefined {
|
||||
return symbol === undefined || symbol === null ? undefined : symbolName(symbol);
|
||||
|
||||
@@ -30,7 +30,7 @@ export type {
|
||||
SemanticSignature as Signature,
|
||||
SemanticSymbol as Symbol,
|
||||
} from "./semantic-model.js";
|
||||
export { symbolName, symbolNameOf } from "./semantic-model.js";
|
||||
export { escapedSymbolName, symbolName, symbolNameOf } from "./semantic-model.js";
|
||||
export type {
|
||||
SemanticIndexInfo as IndexInfo,
|
||||
SemanticTypePredicate as TypePredicate,
|
||||
|
||||
@@ -183,6 +183,7 @@ export const CRYPTO_KEY_OPTION_DYN_TYPES: ReadonlySet<string> = new Set([
|
||||
"PrivateKeyInput",
|
||||
"PublicKeyInput",
|
||||
"RSAKeyPairKeyObjectOptions",
|
||||
"X25519KeyPairOptions",
|
||||
"RSAKeyPairOptions",
|
||||
"SignKeyObjectInput",
|
||||
"SignPrivateKeyInput",
|
||||
@@ -1393,9 +1394,17 @@ function genChannels(
|
||||
if (!nextT || nextT.kind === "void" || nextT.kind === "nullT") return null;
|
||||
const ownChannel = (t: IrType): boolean =>
|
||||
t.kind === "f64" || t.kind === "bool" || t.kind === "string";
|
||||
// A record yield channel keeps its type beside a dyn return (an
|
||||
// `IterableIterator<Row>`). A record return channel beside a dyn yield
|
||||
// does not: the value a delegating `yield*` answers (Effect.gen's
|
||||
// generators) is often an object whose methods a record would drop.
|
||||
const dynMix =
|
||||
(yieldT.kind === "dyn" && retT.kind !== "dyn" && retT.kind !== "void" && !ownChannel(retT)) ||
|
||||
(retT.kind === "dyn" && yieldT.kind !== "dyn" && yieldT.kind !== "void" && !ownChannel(yieldT));
|
||||
(retT.kind === "dyn" &&
|
||||
yieldT.kind !== "dyn" &&
|
||||
yieldT.kind !== "void" &&
|
||||
!ownChannel(yieldT) &&
|
||||
yieldT.kind !== "record");
|
||||
if (dynMix) return { yieldT: DYN, retT: DYN, nextT };
|
||||
return { yieldT, retT, nextT };
|
||||
}
|
||||
@@ -3222,10 +3231,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
}
|
||||
const collectionView = mapCollectionView(widened, ctx);
|
||||
if (collectionView) return collectionView;
|
||||
// Date: a TimeClip'd epoch-millisecond scalar in the static runtime.
|
||||
// This supports stored/passed values and the read-only getter slice;
|
||||
// identity and mutation stay fenced because the scalar deliberately
|
||||
// carries no object identity.
|
||||
// Date: a refcounted object holding its TimeClip'd time value (ScrDate),
|
||||
// shared with the checked-dynamic Date handle, so it keeps its identity
|
||||
// through every slot and through `unknown`. Setters stay fenced.
|
||||
if (isStdlibInterface("Date")) return DATE_T;
|
||||
// RegExp: a reference to the lib RegExp interface — provenance, not the
|
||||
// name (a user's own `interface RegExp` maps as a record). Regex values
|
||||
@@ -3958,8 +3966,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// Mixed dyn/concrete value channels share checked storage, so either
|
||||
// branch of the result record has the same representation. A number,
|
||||
// boolean or string channel keeps its own type beside a dyn one (the
|
||||
// `Generator<number>` defaults): its values ride OUT typed and box only
|
||||
// when the result record is built.
|
||||
// `Generator<number>` default), and so does a record yield channel beside
|
||||
// a dyn return (`IterableIterator<Row>`): its values ride OUT typed and box
|
||||
// only when the result record is built, so for-of and spread read them
|
||||
// back unchecked.
|
||||
if (
|
||||
isStdlibInterface("Generator") ||
|
||||
isStdlibInterface("AsyncGenerator") ||
|
||||
@@ -4758,6 +4768,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
if (hybrid) return hybrid;
|
||||
}
|
||||
const callSigs = checker.getCallSignatures(widened);
|
||||
// AsyncLocalStorage.snapshot()'s generic runner is a dyn function value
|
||||
// (als.snapshot), called through the dyn call path.
|
||||
if (callSigs.length === 1 && isAsyncLocalStorageSnapshot(callSigs[0]!, ctx)) return DYN;
|
||||
if (callSigs.length === 1) return mapCallSignature(callSigs[0]!, ctx);
|
||||
// Records: object types whose members are all data properties (shorthand
|
||||
// methods in type position count — they're func-typed fields) with
|
||||
@@ -5054,7 +5067,6 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
(a) =>
|
||||
a.kind === "void" ||
|
||||
a.kind === "union" ||
|
||||
a.kind === "date" ||
|
||||
a.kind === "dyn" ||
|
||||
// A generator arm only comes from an Iterable<T> (an optional
|
||||
// one keeps its unit arms beside the iterable union's).
|
||||
@@ -5090,6 +5102,26 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
/** One non-overloaded call signature's func type (mapTypeInner's
|
||||
* single-signature rule; erasedOverloadMemberType maps each overload of a
|
||||
* member through it). */
|
||||
/** Whether `sig` is the function type AsyncLocalStorage.snapshot() returns:
|
||||
* declared as that static method's return type in the async_hooks module
|
||||
* of the standard library declarations. */
|
||||
function isAsyncLocalStorageSnapshot(sig: ts.Signature, ctx: TypeMapperCtx): boolean {
|
||||
const decl = sig.declaration;
|
||||
if (decl === undefined || !ts.isFunctionTypeNode(decl)) return false;
|
||||
const method = decl.parent;
|
||||
return (
|
||||
method !== undefined &&
|
||||
ts.isMethodDeclaration(method) &&
|
||||
ts.isIdentifier(method.name) &&
|
||||
method.name.text === "snapshot" &&
|
||||
method.parent !== undefined &&
|
||||
ts.isClassDeclaration(method.parent) &&
|
||||
method.parent.name?.text === "AsyncLocalStorage" &&
|
||||
ctx.isStdlibFile(ts.getSourceFileOfNode(decl)) &&
|
||||
isDeclaredInAmbientModule(decl, "async_hooks")
|
||||
);
|
||||
}
|
||||
|
||||
function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null {
|
||||
const { checker } = ctx;
|
||||
if (sig.getTypeParameters().length && ctx.erasingSignature !== sig) return null;
|
||||
@@ -6587,7 +6619,7 @@ export function genResultRecord(
|
||||
for (const a of unions.get(t.unionId)?.arms ?? []) byKey.set(typeKey(a), a);
|
||||
return true;
|
||||
}
|
||||
if (t.kind === "regex" || t.kind === "date" || t.kind === "generator") {
|
||||
if (t.kind === "regex" || t.kind === "generator") {
|
||||
return false; // no legal union arm exists for these kinds
|
||||
}
|
||||
byKey.set(typeKey(t), t);
|
||||
@@ -6676,15 +6708,15 @@ export function withUnitArm(
|
||||
if (t.kind === "union") {
|
||||
const def = unions.get(t.unionId);
|
||||
if (!def) return null;
|
||||
if (def.arms.some((a) => a.kind === "date")) return null;
|
||||
if (def.arms.some((a) => a.kind === kind)) return t;
|
||||
const arms = [...def.arms, unit];
|
||||
// Date and Promise arms beside other data arms have no union home.
|
||||
if (!unionContainerArmsOk(arms)) return null;
|
||||
arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
|
||||
return { kind: "union", unionId: unions.transform(def, arms) };
|
||||
}
|
||||
if (
|
||||
t.kind === "void" ||
|
||||
t.kind === "date" ||
|
||||
t.kind === "dyn" ||
|
||||
// A bare unit field type cannot occur (units live only inside unions),
|
||||
// but guard against constructing a single-arm union from one.
|
||||
@@ -8274,7 +8306,6 @@ function armHasUnionHome(arm: IrType): boolean {
|
||||
case "void":
|
||||
case "union":
|
||||
case "regex":
|
||||
case "date":
|
||||
case "generator":
|
||||
case "dyn":
|
||||
return false;
|
||||
|
||||
@@ -625,6 +625,7 @@ async function compileExecutableNative(
|
||||
dgram: features.dgram,
|
||||
dnsPromises: features.dnsPromises,
|
||||
dgramDyn: features.dgramDyn,
|
||||
textCodecDyn: features.textCodecDyn,
|
||||
watch: features.watch,
|
||||
foreignFfi: features.foreignFfi,
|
||||
...(features.workers ? { workers: true } : {}),
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
BYTES_ELEMENT_NAME,
|
||||
DYN_HANDLE_KINDS,
|
||||
RUNTIME_STREAM_CLASSES,
|
||||
recordTextCodecClass,
|
||||
typeEquals,
|
||||
type IrExpr,
|
||||
type IrLibFn,
|
||||
@@ -141,8 +142,10 @@ export function dynDesc(
|
||||
return "string";
|
||||
case "bool":
|
||||
return "boolean";
|
||||
case "record":
|
||||
return recordsById.get(t.shapeId)?.tuple ? "array" : "object";
|
||||
case "record": {
|
||||
const shape = recordsById.get(t.shapeId);
|
||||
return shape?.tuple ? "array" : ((shape && recordTextCodecClass(shape)) ?? "object");
|
||||
}
|
||||
case "array":
|
||||
return "array";
|
||||
case "nullT":
|
||||
|
||||
@@ -207,6 +207,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"async.awaitDyn",
|
||||
"als.run",
|
||||
"als.exitRun",
|
||||
"als.newOptions",
|
||||
"als.bindCallbackDyn",
|
||||
"dc.chanBindStore",
|
||||
"dc.chanRunStores",
|
||||
"http.resWriteHeadDyn",
|
||||
@@ -737,6 +739,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"url.set",
|
||||
"url.setChecked",
|
||||
"url.fileURLToPathUrl",
|
||||
"fs.checkPath",
|
||||
"fs.checkReadEncoding",
|
||||
"url.fileURLToPathChecked",
|
||||
"url.fileURLToPathOptions",
|
||||
"url.fileURLToPathBuffer",
|
||||
@@ -833,12 +837,15 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"buffer.poolFromDyn",
|
||||
"buffer.poolFromDynRef",
|
||||
"buffer.poolFromDynOffset",
|
||||
"buffer.poolFromDynRange",
|
||||
// The checked-dynamic compare/equals validators: Node's argument
|
||||
// ladders throw ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE catchably.
|
||||
"buffer.compareChk",
|
||||
"bytes.equalsChk",
|
||||
"bytes.compareChk",
|
||||
"buffer.newStringFail",
|
||||
// Inherited and symbol-keyed Buffer.prototype members throw SC2020.
|
||||
"buffer.prototypeMember",
|
||||
"fs.toUnixTimestamp",
|
||||
"fs.existsChk",
|
||||
"fs.mkdtempChk",
|
||||
@@ -1030,6 +1037,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"crypto.generateKeyPairCb",
|
||||
"crypto.signOneshot",
|
||||
"crypto.verifyOneshot",
|
||||
"crypto.diffieHellman",
|
||||
"crypto.createSign",
|
||||
"crypto.createVerify",
|
||||
]);
|
||||
|
||||
@@ -1006,6 +1006,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"url.set": { argTypes: [URL_T, STRING, STRING], result: VOID },
|
||||
"url.setChecked": { argTypes: [URL_T, STRING, DYN], result: VOID },
|
||||
"url.fileURLToPathUrl": { argTypes: [URL_T], result: STRING },
|
||||
/** fileURLToPathUrl as a value: the path string, or the error it would
|
||||
* throw (fs.existsSync and fs.promises URL paths). Never throws. */
|
||||
"url.fileURLToPathValue": { argTypes: [URL_T], result: DYN },
|
||||
/** getValidatedPath's null-byte check on a string path, and
|
||||
* readFileSync's encoding check: what Node validates before it converts
|
||||
* a later URL path. (path, argument name) and (encoding). */
|
||||
"fs.checkPath": { argTypes: [STRING, STRING], result: VOID },
|
||||
"fs.checkReadEncoding": { argTypes: [STRING], result: VOID },
|
||||
"url.fileURLToPathChecked": { argTypes: [DYN], result: STRING },
|
||||
"url.fileURLToPathOptions": { argTypes: [DYN, DYN], result: STRING },
|
||||
/** node:url's legacy API (scr_url_legacy.c): parse(url, parseQueryString,
|
||||
@@ -3016,6 +3024,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
/** poolFromDyn that also copies an array crossing by reference. */
|
||||
"buffer.poolFromDynRef": { argTypes: [DYN, STRING], result: BYTES_U8 },
|
||||
"buffer.poolFromDynOffset": { argTypes: [DYN, DYN], result: BYTES_U8 },
|
||||
/** Buffer.from(value, byteOffset, length): an ArrayBuffer's range with
|
||||
* Node's fromArrayBuffer checks; other values as poolFromDynOffset. */
|
||||
"buffer.poolFromDynRange": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 },
|
||||
"buffer.poolSize": { argTypes: [], result: F64 },
|
||||
"buffer.setPoolSize": { argTypes: [F64], result: VOID },
|
||||
/** Buffer.byteLength(string, enc) — enc a NORMALIZED literal like
|
||||
@@ -3050,6 +3061,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* always throws Node's ERR_INVALID_ARG_TYPE ("The \"string\" argument
|
||||
* must be of type string. Received ..."). Borrowed dyn; may-throw. */
|
||||
"buffer.newStringFail": { argTypes: [DYN], result: BYTES_U8 },
|
||||
"buffer.deprecation": { argTypes: [], result: VOID },
|
||||
"buffer.deprecationCount": { argTypes: [], result: VOID },
|
||||
"buffer.constructor": { argTypes: [], result: DYN },
|
||||
"buffer.installValue": { argTypes: [], result: VOID },
|
||||
"buffer.staticValue": { argTypes: [STRING], result: DYN },
|
||||
"buffer.prototypeMember": { argTypes: [DYN], result: DYN },
|
||||
/** fs._toUnixTimestamp(time) over a dyn value: numeric strings and
|
||||
* finite numbers coerce (negatives answer now/1000, Node's shape);
|
||||
* everything else throws Node's ERR_INVALID_ARG_TYPE. May-throw. */
|
||||
@@ -3194,6 +3211,18 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
// or an optional Error | null slot (same success shape as fs.rename).
|
||||
"process.stdoutWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL },
|
||||
"process.stderrWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL },
|
||||
/** The value of a FULFILLED promise, read without a microtask turn (+1
|
||||
* for references): the tuple form of Promise.all reads its entries this
|
||||
* way once the combinator fulfills, each entry widened to Promise<void>
|
||||
* and read at the type it was built with (program-dependent). */
|
||||
"promise.fulfilledValue": { argTypes: [null], result: VOID },
|
||||
/** Promise.all's tuple form as a value: the entries (each widened to
|
||||
* Promise<void>) join the runtime combinator, and when it fulfills the
|
||||
* function reads the fulfilled entries into the tuple inside that
|
||||
* settlement, so the result settles on the combinator's turn; a
|
||||
* rejection settles it the same way. The result is the tuple's promise
|
||||
* (program-dependent). */
|
||||
"promise.allTuple": { argTypes: [null, null], result: VOID },
|
||||
"fsp.readFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: STRING } },
|
||||
"fsp.writeFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } },
|
||||
/** fs.promises.writeFile(path, data, { mode }): the settled-promise
|
||||
@@ -4467,6 +4496,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* (inspect, Object.keys/values/entries) then answer through its live
|
||||
* checked view. The receiver is any such record type. Never throws. */
|
||||
"record.sourceHas": { argTypes: [null], result: BOOL },
|
||||
/** `record.sourceSpread(built, value)`: the record a literal starting
|
||||
* with `...value` just built keeps the order of value's kept source
|
||||
* (key-order-witness.ts ordered spreads). Both are record types. Never
|
||||
* throws. */
|
||||
"record.sourceSpread": { argTypes: [null, null], result: VOID },
|
||||
"insp.key": { argTypes: [STRING], result: STRING },
|
||||
"insp.moreItems": { argTypes: [F64], result: STRING },
|
||||
"insp.end": { argTypes: [STRING, STRING, STRING, F64, BOOL, BOOL], result: STRING },
|
||||
@@ -4757,6 +4791,20 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"als.exitRun": { argTypes: [F64, DYN, DYN], result: DYN },
|
||||
"als.enterWith": { argTypes: [F64, DYN], result: VOID },
|
||||
"als.disable": { argTypes: [F64], result: VOID },
|
||||
/** new AsyncLocalStorage(options) (Node's object check throws; MAY
|
||||
* THROW) and the store's name option (dyn; "" without one). */
|
||||
"als.newOptions": { argTypes: [DYN], result: F64 },
|
||||
"als.name": { argTypes: [F64], result: DYN },
|
||||
/** A function bound to the active context: the completion callbacks of
|
||||
* callback-style APIs (always false: an empty context leaves the
|
||||
* callback unbound) and AsyncLocalStorage.bind (always true). The typed
|
||||
* form answers its closure argument's type; the dyn form throws Node's
|
||||
* ERR_INVALID_ARG_TYPE for a non-function when always (MAY THROW). */
|
||||
"als.bindCallback": { argTypes: [null, BOOL], result: VOID },
|
||||
"als.bindCallbackDyn": { argTypes: [DYN, BOOL], result: DYN },
|
||||
/** AsyncLocalStorage.snapshot(): a dyn function (cb, ...args) that runs
|
||||
* cb in the context active at the snapshot. */
|
||||
"als.snapshot": { argTypes: [], result: DYN },
|
||||
/** Channel.bindStore/unbindStore/runStores (scr_dc.c): the
|
||||
* AsyncLocalStorage integration — set-semantics bindings per store,
|
||||
* runStores entering every bound store with transform(data) around the
|
||||
@@ -4812,6 +4860,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"date.getMilliseconds": { argTypes: [DATE_T], result: F64 },
|
||||
"date.getUTCMilliseconds": { argTypes: [DATE_T], result: F64 },
|
||||
"date.getTimezoneOffset": { argTypes: [DATE_T], result: F64 },
|
||||
/** A Date object for a time value a local holds (date-values.ts): a
|
||||
* lib-call argument passes the time value itself, and any other use
|
||||
* creates the Date. */
|
||||
"date.fromTime": { argTypes: [F64], result: DATE_T },
|
||||
/** Date.prototype.toString / toDateString / toTimeString and String(date):
|
||||
* V8's local-time formats with the host zone's long name, "Invalid Date"
|
||||
* for an invalid date. Owned (+1) string; never throws. */
|
||||
@@ -4864,6 +4916,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"crypto.generateKeyPairSync": { argTypes: [DYN, DYN], result: DYN },
|
||||
"crypto.generateKeyPairCb": { argTypes: [DYN, DYN, DYN], result: VOID },
|
||||
"crypto.signOneshot": { argTypes: [DYN, DYN, DYN], result: DYN },
|
||||
"crypto.diffieHellman": { argTypes: [DYN, DYN], result: DYN },
|
||||
"crypto.verifyOneshot": { argTypes: [DYN, DYN, DYN, DYN], result: DYN },
|
||||
"crypto.createSign": { argTypes: [DYN], result: DYN },
|
||||
"crypto.createVerify": { argTypes: [DYN], result: DYN },
|
||||
@@ -4884,6 +4937,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"text.decoderEncoding": { argTypes: [DYN], result: F64 },
|
||||
"text.decoderName": { argTypes: [F64], result: STRING },
|
||||
"text.encodeInto": { argTypes: [DYN, DYN], result: DYN },
|
||||
/** `value instanceof TextDecoder` (true) or `TextEncoder` (false) on a
|
||||
* checked-dynamic value: a codec record's handle box (scr_text_codec_dyn.c).
|
||||
* Borrowed; never throws. */
|
||||
"text.codecIs": { argTypes: [DYN, BOOL], result: BOOL },
|
||||
"buffer.isAscii": { argTypes: [DYN], result: BOOL },
|
||||
"buffer.isUtf8": { argTypes: [DYN], result: BOOL },
|
||||
"buffer.transcode": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 },
|
||||
|
||||
@@ -449,6 +449,8 @@ const DYN_READ_ONLY = new Set<string>([
|
||||
"error.new",
|
||||
"error.newOptions",
|
||||
"fs.utimesSync",
|
||||
// Buffer.prototype members read as values: a lookup by key.
|
||||
"buffer.prototypeMember",
|
||||
"insp.dyn",
|
||||
"insp.dynS",
|
||||
// The dynamic-tier operators run their dyn twins (scr_js_value.c); the
|
||||
|
||||
@@ -57,7 +57,8 @@ describe("IR kind sets", () => {
|
||||
|
||||
test("distinguishes pointer values from object-like scalars", () => {
|
||||
expect(POINTER_KINDS.has("record")).toBe(true);
|
||||
expect(POINTER_KINDS.has("date")).toBe(false);
|
||||
expect(POINTER_KINDS.has("date")).toBe(true);
|
||||
expect(POINTER_KINDS.has("procStream")).toBe(false);
|
||||
});
|
||||
|
||||
test("distinguishes typed-rest closure ABIs", () => {
|
||||
|
||||
@@ -440,6 +440,7 @@ const POINTER_KIND_LIST = [
|
||||
"url",
|
||||
"searchParams",
|
||||
"symbol",
|
||||
"date",
|
||||
...POINTER_HANDLE_KINDS,
|
||||
"func",
|
||||
"object",
|
||||
@@ -463,7 +464,7 @@ export const POINTER_KINDS = irKindSet(POINTER_KIND_LIST);
|
||||
export const RUNTIME_RC_STEMS: Record<IrType["kind"], string> = {
|
||||
f64: "",
|
||||
bigint: "scr_bigint",
|
||||
date: "",
|
||||
date: "scr_date",
|
||||
string: "scr_str",
|
||||
bool: "",
|
||||
array: "scr_arr",
|
||||
@@ -645,6 +646,7 @@ export function isSupportedArrayElem(t: IrType): boolean {
|
||||
case "func":
|
||||
case "promise":
|
||||
case "regex":
|
||||
case "date":
|
||||
case "child":
|
||||
case "netServer":
|
||||
// fs.Stats snapshots (`await Promise.all(paths.map(stat))`): immutable
|
||||
@@ -689,7 +691,8 @@ export function isIdentityCollectionKey(t: IrType): boolean {
|
||||
// functions intern one immortal closure, inner closures allocate once
|
||||
// per evaluation), so pointer identity is JS function identity.
|
||||
// Closures are cycle-headered and traced like records.
|
||||
t.kind === "func"
|
||||
t.kind === "func" ||
|
||||
t.kind === "date"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -797,6 +800,9 @@ export function isSupportedMapValue(t: IrType): boolean {
|
||||
// the pre-settle closures.
|
||||
case "secureCtx":
|
||||
return true;
|
||||
// A Date object: immutable, holds no references.
|
||||
case "date":
|
||||
return true;
|
||||
// A promise (Map<string, Promise<SecureContext>> — the SNI callback's
|
||||
// in-flight dedupe map): refcounted, with trace/cycle support already
|
||||
// in place (scr_promise_trace_v — the race machinery's refcounting).
|
||||
@@ -849,8 +855,9 @@ export function funcOf(params: IrType[], ret: IrType): IrType {
|
||||
* promises nullable-only; declared maybe-promise unions are admitted by
|
||||
* maybePromiseUnionArmsOk. */
|
||||
export function unionContainerArmsOk(arms: IrType[]): boolean {
|
||||
return arms.every(
|
||||
(a, i) => a.kind !== "promise" || arms.every((b, j) => j === i || isUnitType(b)),
|
||||
return (
|
||||
arms.every((a, i) => a.kind !== "promise" || arms.every((b, j) => j === i || isUnitType(b))) &&
|
||||
dateUnionArmsOk(arms)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -861,7 +868,13 @@ export function unionContainerArmsOk(arms: IrType[]): boolean {
|
||||
* non-thenable path). Data arms must not be thenable; the type mapper
|
||||
* checks that against the checker types before minting such a union. */
|
||||
export function maybePromiseUnionArmsOk(arms: IrType[]): boolean {
|
||||
return arms.filter((a) => a.kind === "promise").length <= 1;
|
||||
return arms.filter((a) => a.kind === "promise").length <= 1 && dateUnionArmsOk(arms);
|
||||
}
|
||||
|
||||
/** A Date arm has a union home beside undefined and null arms only: no
|
||||
* narrowing contract tells it from other data arms yet. */
|
||||
function dateUnionArmsOk(arms: IrType[]): boolean {
|
||||
return arms.every((a, i) => a.kind !== "date" || arms.every((b, j) => j === i || isUnitType(b)));
|
||||
}
|
||||
|
||||
/** Canonical, injective text form of an IrType — the building block of
|
||||
@@ -1757,6 +1770,18 @@ export function recordTextCodecClass(shape: IrRecordShape): "TextEncoder" | "Tex
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The codec class of a TextEncoder or TextDecoder record type, else null.
|
||||
* Such a record crosses an `unknown` slot as a native handle that holds the
|
||||
* record itself (scr_text_codec_dyn.c), never as a copy or a field view. */
|
||||
export function textCodecRecordClass(
|
||||
t: IrType,
|
||||
getRecord: (shapeId: string) => IrRecordShape | undefined,
|
||||
): "TextEncoder" | "TextDecoder" | null {
|
||||
if (t.kind !== "record") return null;
|
||||
const shape = getRecord(t.shapeId);
|
||||
return shape ? recordTextCodecClass(shape) : null;
|
||||
}
|
||||
|
||||
/** Object-literal ACCESSOR properties (`{ get x() {...}, set x(v) {...} }`)
|
||||
* live on the shape as reserved '%'-fields holding closures: `%get:x` a
|
||||
* `() => T` invoked once per property READ (side effects and all — JS's
|
||||
@@ -4031,6 +4056,9 @@ function isJsonSafeAt(
|
||||
case "string":
|
||||
case "bool":
|
||||
return true;
|
||||
// JSON.stringify writes a Date's toJSON(); JSON text never parses to one.
|
||||
case "date":
|
||||
return stringify;
|
||||
case "array":
|
||||
return isJsonSafeAt(
|
||||
t.elem,
|
||||
@@ -4129,7 +4157,6 @@ function isJsonSafeAt(
|
||||
// Maps are not JSON (JSON.stringify(new Map()) is "{}" in Node — an
|
||||
// empty-object husk nobody wants; stringify/dynCheck reject instead).
|
||||
case "map":
|
||||
case "date":
|
||||
// URLSearchParams stringifies as the same "{}" husk — rejected; use
|
||||
// sp.toString() instead.
|
||||
case "searchParams":
|
||||
@@ -4285,6 +4312,7 @@ export function canConvertToDyn(
|
||||
visiting: Set<string> = new Set(),
|
||||
): boolean {
|
||||
if (isJsonSafeType(t, getRecord, getUnion)) return true;
|
||||
if (textCodecRecordClass(t, getRecord) !== null) return true;
|
||||
// numeric typed arrays and boxable functions are dyn kinds the walker boxes
|
||||
// ANYWHERE (bytes and functions held by identity), including nested
|
||||
// in records/arrays/unions. isJsonSafeType rejects them, but dynFrom
|
||||
@@ -4331,6 +4359,7 @@ export function canConvertToDyn(
|
||||
// either its result or a promise of it.
|
||||
canBoxDynComposite(a, getRecord, getUnion, visiting) ||
|
||||
isDynTypedRefType(a) ||
|
||||
textCodecRecordClass(a, getRecord) !== null ||
|
||||
a.kind === "classval" ||
|
||||
DYN_HANDLE_KINDS.has(a.kind) ||
|
||||
(a.kind === "func" && canBoxFuncIntoDyn(a, getRecord, getUnion, visiting)) ||
|
||||
@@ -4410,7 +4439,8 @@ function canBoxDynCompositeAt(
|
||||
case "record": {
|
||||
const shape = getRecord(t.shapeId);
|
||||
if (!shape) return false;
|
||||
if (recordTextCodecClass(shape) !== null) return false;
|
||||
// A codec boxes as its own handle wherever it sits (toDynHelper).
|
||||
if (recordTextCodecClass(shape) !== null) return true;
|
||||
// Recursive shapes answer coinductively, like isJsonSafeType.
|
||||
if (visiting.has(t.shapeId)) return true;
|
||||
visiting.add(t.shapeId);
|
||||
@@ -4511,6 +4541,8 @@ function canDynCheckToAt(
|
||||
visiting.delete(key);
|
||||
return result;
|
||||
}
|
||||
// A codec handle unboxes to the record it holds.
|
||||
if (textCodecRecordClass(t, getRecord) !== null) return true;
|
||||
if (t.kind === "record") {
|
||||
const key = typeKey(t);
|
||||
if (visiting.has(key)) return false;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { IrClassDef, IrExpr, IrModule, IrRecordShape, IrType, IrUnionDef }
|
||||
import type { IrLibFn } from "./builtin-signatures.js";
|
||||
import {
|
||||
isDirentShape,
|
||||
recordTextCodecClass,
|
||||
RUNTIME_EMITTER_CLASS,
|
||||
RUNTIME_EVENT_CLASS,
|
||||
RUNTIME_EVENT_TARGET_CLASS,
|
||||
@@ -99,6 +100,8 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"process.setDeprecationFlag",
|
||||
"process.setEmitWarning",
|
||||
"punycode.warn",
|
||||
// DEP0005 for the deprecated Buffer constructor.
|
||||
"buffer.deprecation",
|
||||
"timers.immediatePromise",
|
||||
// The performance timeline (scr_perf.c, linked with this unit for its
|
||||
// MaxPerformanceEntryBufferExceededWarning).
|
||||
@@ -116,12 +119,16 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"perf.measure",
|
||||
// Blob and File (scr_blob.c, linked with this unit, which boxes their
|
||||
// reads as dyn promises).
|
||||
"als.bindCallbackDyn",
|
||||
"als.disable",
|
||||
"als.enterWith",
|
||||
"als.exitRun",
|
||||
"als.get",
|
||||
"als.name",
|
||||
"als.new",
|
||||
"als.newOptions",
|
||||
"als.run",
|
||||
"als.snapshot",
|
||||
"blob.arrayBuffer",
|
||||
"blob.bytes",
|
||||
"blob.fileNew",
|
||||
@@ -361,6 +368,10 @@ const SYMBOL_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"dyn.usingDispose",
|
||||
"dyn.usingMethod",
|
||||
"util.isDeepStrictEqual",
|
||||
// The Buffer function value's Symbol(kIsEncodingSymbol) member
|
||||
"buffer.constructor",
|
||||
"buffer.staticValue",
|
||||
"buffer.prototypeMember",
|
||||
]);
|
||||
|
||||
/** Entry points that link the querystring unit. */
|
||||
@@ -415,7 +426,10 @@ interface TypeTables {
|
||||
unions: Map<string, IrUnionDef>;
|
||||
classes: Map<string, IrClassDef>;
|
||||
/** mentionsDgramSocket's carriers, computed on first use. */
|
||||
dgramCarriers?: DgramCarriers;
|
||||
dgramCarriers?: TypeCarriers;
|
||||
/** mentionsTextCodec's and mentionsTextDecoder's carriers. */
|
||||
textCodecCarriers?: TypeCarriers;
|
||||
textDecoderCarriers?: TypeCarriers;
|
||||
}
|
||||
|
||||
/** Whether boxing a value of type `t` as dyn boxes a promise, which goes
|
||||
@@ -479,30 +493,42 @@ function boxesPromise(t: IrType, tables: TypeTables, seen: Set<string>): boolean
|
||||
}
|
||||
}
|
||||
|
||||
/** The record shapes, unions and classes whose values carry a dgram socket
|
||||
* (directly or through other members), computed once per module. */
|
||||
interface DgramCarriers {
|
||||
/** The record shapes, unions and classes whose values carry a leaf type
|
||||
* (directly or through other members), computed once per module and leaf. */
|
||||
interface TypeCarriers {
|
||||
records: Set<string>;
|
||||
unions: Set<string>;
|
||||
anyClass: boolean;
|
||||
}
|
||||
|
||||
/** Whether `t` carries a dgram socket, given the carrier sets so far:
|
||||
* structural through functions (parameters and result), promises and
|
||||
* collections; named records, unions and class instances by the sets. */
|
||||
function carriesDgram(t: IrType, carriers: DgramCarriers): boolean {
|
||||
/** The leaf a carrier set tracks: a dgram socket, a codec record. */
|
||||
type CarrierLeaf = (t: IrType, tables: TypeTables) => boolean;
|
||||
|
||||
/** Whether `t` carries a leaf, given the carrier sets so far: structural
|
||||
* through functions (parameters and result), promises and collections;
|
||||
* named records, unions and class instances by the sets. */
|
||||
function carriesLeaf(
|
||||
t: IrType,
|
||||
carriers: TypeCarriers,
|
||||
leaf: CarrierLeaf,
|
||||
tables: TypeTables,
|
||||
): boolean {
|
||||
if (leaf(t, tables)) return true;
|
||||
switch (t.kind) {
|
||||
case "dgramSocket":
|
||||
return true;
|
||||
case "func":
|
||||
return t.params.some((p) => carriesDgram(p, carriers)) || carriesDgram(t.ret, carriers);
|
||||
return (
|
||||
t.params.some((p) => carriesLeaf(p, carriers, leaf, tables)) ||
|
||||
carriesLeaf(t.ret, carriers, leaf, tables)
|
||||
);
|
||||
case "promise":
|
||||
return carriesDgram(t.inner, carriers);
|
||||
return carriesLeaf(t.inner, carriers, leaf, tables);
|
||||
case "array":
|
||||
case "set":
|
||||
return carriesDgram(t.elem, carriers);
|
||||
return carriesLeaf(t.elem, carriers, leaf, tables);
|
||||
case "map":
|
||||
return carriesDgram(t.key, carriers) || carriesDgram(t.value, carriers);
|
||||
return (
|
||||
carriesLeaf(t.key, carriers, leaf, tables) || carriesLeaf(t.value, carriers, leaf, tables)
|
||||
);
|
||||
case "record":
|
||||
return carriers.records.has(t.shapeId);
|
||||
case "union":
|
||||
@@ -515,25 +541,29 @@ function carriesDgram(t: IrType, carriers: DgramCarriers): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** The module's dgram carriers: a fixpoint over its records, unions and
|
||||
/** The module's carriers of `leaf`: a fixpoint over its records, unions and
|
||||
* class fields. */
|
||||
function dgramCarriers(tables: TypeTables): DgramCarriers {
|
||||
const carriers: DgramCarriers = { records: new Set(), unions: new Set(), anyClass: false };
|
||||
function leafCarriers(tables: TypeTables, leaf: CarrierLeaf): TypeCarriers {
|
||||
const carriers: TypeCarriers = { records: new Set(), unions: new Set(), anyClass: false };
|
||||
const carries = (t: IrType): boolean => carriesLeaf(t, carriers, leaf, tables);
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const [id, shape] of tables.records)
|
||||
if (!carriers.records.has(id) && shape.fields.some((f) => carriesDgram(f.type, carriers))) {
|
||||
if (
|
||||
!carriers.records.has(id) &&
|
||||
(leaf({ kind: "record", shapeId: id }, tables) || shape.fields.some((f) => carries(f.type)))
|
||||
) {
|
||||
carriers.records.add(id);
|
||||
changed = true;
|
||||
}
|
||||
for (const [id, def] of tables.unions)
|
||||
if (!carriers.unions.has(id) && def.arms.some((a) => carriesDgram(a, carriers))) {
|
||||
if (!carriers.unions.has(id) && def.arms.some(carries)) {
|
||||
carriers.unions.add(id);
|
||||
changed = true;
|
||||
}
|
||||
if (
|
||||
!carriers.anyClass &&
|
||||
[...tables.classes.values()].some((c) => c.fields.some((f) => carriesDgram(f.type, carriers)))
|
||||
[...tables.classes.values()].some((c) => c.fields.some((f) => carries(f.type)))
|
||||
) {
|
||||
carriers.anyClass = true;
|
||||
changed = true;
|
||||
@@ -542,13 +572,42 @@ function dgramCarriers(tables: TypeTables): DgramCarriers {
|
||||
return carriers;
|
||||
}
|
||||
|
||||
const dgramLeaf: CarrierLeaf = (t) => t.kind === "dgramSocket";
|
||||
|
||||
/** Whether converting a value of type `t` to or from dyn converts a dgram
|
||||
* socket, which goes through scr_dgram_dyn.c: the socket itself, or a
|
||||
* function, collection, record, union, promise or class instance that
|
||||
* carries one (boxed functions convert their parameters too). */
|
||||
function mentionsDgramSocket(t: IrType, tables: TypeTables): boolean {
|
||||
tables.dgramCarriers ??= dgramCarriers(tables);
|
||||
return carriesDgram(t, tables.dgramCarriers);
|
||||
tables.dgramCarriers ??= leafCarriers(tables, dgramLeaf);
|
||||
return carriesLeaf(t, tables.dgramCarriers, dgramLeaf, tables);
|
||||
}
|
||||
|
||||
const textCodecLeaf: CarrierLeaf = (t, tables) => {
|
||||
if (t.kind !== "record") return false;
|
||||
const shape = tables.records.get(t.shapeId);
|
||||
return shape !== undefined && recordTextCodecClass(shape) !== null;
|
||||
};
|
||||
|
||||
const textDecoderLeaf: CarrierLeaf = (t, tables) => {
|
||||
if (t.kind !== "record") return false;
|
||||
const shape = tables.records.get(t.shapeId);
|
||||
return shape !== undefined && recordTextCodecClass(shape) === "TextDecoder";
|
||||
};
|
||||
|
||||
/** Whether converting a value of type `t` to or from dyn converts a
|
||||
* TextEncoder or TextDecoder record, which boxes it as a handle of
|
||||
* scr_text_codec_dyn.c (the same carrier walk as dgram sockets). */
|
||||
function mentionsTextCodec(t: IrType, tables: TypeTables): boolean {
|
||||
tables.textCodecCarriers ??= leafCarriers(tables, textCodecLeaf);
|
||||
return carriesLeaf(t, tables.textCodecCarriers, textCodecLeaf, tables);
|
||||
}
|
||||
|
||||
/** The TextDecoder half: its boxed decode runs scr_text_decode_stream,
|
||||
* which reaches the legacy decoders. */
|
||||
function mentionsTextDecoder(t: IrType, tables: TypeTables): boolean {
|
||||
tables.textDecoderCarriers ??= leafCarriers(tables, textDecoderLeaf);
|
||||
return carriesLeaf(t, tables.textDecoderCarriers, textDecoderLeaf, tables);
|
||||
}
|
||||
|
||||
/** The libCall fns served by the OPTIONAL events unit (scr_events.c):
|
||||
@@ -679,6 +738,7 @@ export const PKI_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"crypto.generateKeyPairSync",
|
||||
"crypto.signOneshot",
|
||||
"crypto.verifyOneshot",
|
||||
"crypto.diffieHellman",
|
||||
]);
|
||||
|
||||
/** The WebCrypto algorithms scr_webcrypto_pki.c implements over the pki
|
||||
@@ -794,6 +854,15 @@ const RUNTIME_FEATURE_RULES = {
|
||||
(node.kind === "dynFrom" && mentionsDgramSocket(node.value.type, tables())) ||
|
||||
(node.kind === "dynCheck" && mentionsDgramSocket(node.type, tables())),
|
||||
}),
|
||||
/** A TextEncoder or TextDecoder record converts to or from an untyped
|
||||
* value (its handle box, or a boxed function, record or collection that
|
||||
* carries one): scr_text_codec_dyn.c. */
|
||||
textCodecDyn: rule({
|
||||
fns: ["text.codecIs"],
|
||||
expr: (node, tables) =>
|
||||
(node.kind === "dynFrom" && mentionsTextCodec(node.value.type, tables())) ||
|
||||
(node.kind === "dynCheck" && mentionsTextCodec(node.type, tables())),
|
||||
}),
|
||||
/** node:dns/promises: scr_dns_promises.c over the dgram unit. */
|
||||
dnsPromises: rule({ fns: ["dns.promisesLookup", "dns.promisesLookupService"] }),
|
||||
dynAsync: rule({
|
||||
@@ -844,6 +913,7 @@ const RUNTIME_FEATURE_RULES = {
|
||||
...UTIL_CALLABLE_LIB_FNS,
|
||||
...BUILTIN_CLASS_LIB_FNS,
|
||||
"util.promisifyDyn",
|
||||
"buffer.prototypeMember",
|
||||
],
|
||||
// Constant-key reads of Object.prototype member names answer the
|
||||
// inherited member through scr_dyn_invoke.c.
|
||||
@@ -872,10 +942,22 @@ const RUNTIME_FEATURE_RULES = {
|
||||
/** Buffer.prototype methods called on checked-dynamic values
|
||||
* (scr_bytes_dyn.c). */
|
||||
dynBuffer: rule({
|
||||
// Buffer.prototype members read as values dispatch to the same entries.
|
||||
fns: ["buffer.prototypeMember"],
|
||||
expr: (node, _tables) => node.kind === "dynInvoke" && DYN_BUFFER_METHODS.has(node.method),
|
||||
}),
|
||||
/** Builtin functions read as values (scr_builtin_values.c). */
|
||||
builtinValues: rule({ fns: ["buffer.byteLenValue", "buffer.allocValue", "fs.realpathValue"] }),
|
||||
builtinValues: rule({
|
||||
fns: [
|
||||
"buffer.byteLenValue",
|
||||
"buffer.allocValue",
|
||||
"fs.realpathValue",
|
||||
"buffer.constructor",
|
||||
"buffer.installValue",
|
||||
"buffer.staticValue",
|
||||
"buffer.prototypeMember",
|
||||
],
|
||||
}),
|
||||
/** The stream unit emits through the emitter unit, also when the
|
||||
* program constructs no stream (a pipeline of functions). */
|
||||
/** scr_ecdh.c over the mbedTLS archive; DEP0031 reports through the
|
||||
@@ -942,7 +1024,12 @@ const RUNTIME_FEATURE_RULES = {
|
||||
prefixes: ["insp.", "console.", "test."],
|
||||
libCall: (node) => node.fn === "rl.terminal" && !isDynUndefined(node.args[3]!),
|
||||
}),
|
||||
legacyTextDecoder: rule({ fns: [...LEGACY_TEXT_DECODER_LIB_FNS] }),
|
||||
legacyTextDecoder: rule({
|
||||
fns: [...LEGACY_TEXT_DECODER_LIB_FNS],
|
||||
expr: (node, tables) =>
|
||||
(node.kind === "dynFrom" && mentionsTextDecoder(node.value.type, tables())) ||
|
||||
(node.kind === "dynCheck" && mentionsTextDecoder(node.type, tables())),
|
||||
}),
|
||||
/** The program calls mock.timers members: its timer calls go through
|
||||
* scr_test.c's mock-aware entry points. */
|
||||
mockTimers: rule({ prefixes: ["test.mockTimers", "test.ctxMockTimers"] }),
|
||||
@@ -1056,6 +1143,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
|
||||
dc: false,
|
||||
dgram: false,
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
dnsPromises: false,
|
||||
dynAsync: false,
|
||||
dynBuiltinClass: false,
|
||||
|
||||
@@ -473,9 +473,10 @@ export function everyStmtChild(
|
||||
throw new Error("unhandled IR node");
|
||||
}
|
||||
|
||||
/** Called with each record literal mapExprChildren rebuilds and its
|
||||
* replacement, so a side table keyed by node identity (the lowering's
|
||||
* key-order witness notes) follows the literal into the rewritten tree. */
|
||||
/** Called with each record literal and checked conversion mapExprChildren
|
||||
* rebuilds and its replacement, so a side table keyed by node identity (the
|
||||
* lowering's key-order witness notes) follows the node into the rewritten
|
||||
* tree. */
|
||||
let recordLiteralRebuilt: ((from: IrExpr, to: IrExpr) => void) | null = null;
|
||||
|
||||
/** Installs (or with null removes) the record-literal rebuild observer. */
|
||||
@@ -504,8 +505,11 @@ export function mapExprChildren(
|
||||
return node;
|
||||
case "libCall":
|
||||
return { ...node, args: mapOf(node.args, expr) };
|
||||
case "dynCheck":
|
||||
return { ...node, value: expr(node.value) };
|
||||
case "dynCheck": {
|
||||
const rebuilt: IrExpr = { ...node, value: expr(node.value) };
|
||||
if (recordLiteralRebuilt !== null) recordLiteralRebuilt(node, rebuilt);
|
||||
return rebuilt;
|
||||
}
|
||||
case "strEq":
|
||||
return { ...node, left: expr(node.left), right: expr(node.right) };
|
||||
case "fieldGet":
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
typeEquals,
|
||||
typeKey,
|
||||
maybePromiseUnionArmsOk,
|
||||
unionContainerArmsOk,
|
||||
VOID,
|
||||
} from "./ir.js";
|
||||
import { DYN_CLASS_PROPERTIES, callSiteReturnType, classValueConstructor } from "./ir.js";
|
||||
@@ -803,7 +804,6 @@ export function validateModule(mod: IrModule): IrValidationError[] {
|
||||
arm.kind === "void" ||
|
||||
arm.kind === "union" ||
|
||||
arm.kind === "dyn" ||
|
||||
arm.kind === "date" ||
|
||||
(arm.kind === "generator" && iterableArmsElem(u.arms) === null)
|
||||
) {
|
||||
errors.push({ message: `union ${u.id}: arm ${i} is ${arm.kind}`, loc: noLoc });
|
||||
@@ -814,6 +814,9 @@ export function validateModule(mod: IrModule): IrValidationError[] {
|
||||
loc: noLoc,
|
||||
});
|
||||
}
|
||||
if (arm.kind === "date" && !unionContainerArmsOk(u.arms)) {
|
||||
errors.push({ message: `union ${u.id}: date arm ${i} beside non-unit arms`, loc: noLoc });
|
||||
}
|
||||
if (arm.kind === "record" && !recordsById.has(arm.shapeId)) {
|
||||
errors.push({
|
||||
message: `union ${u.id}: arm ${i} references undeclared shape "${arm.shapeId}"`,
|
||||
@@ -1507,6 +1510,7 @@ function validateFunction(
|
||||
e.left.type.kind === "map" ||
|
||||
e.left.type.kind === "set" ||
|
||||
e.left.type.kind === "regex" ||
|
||||
e.left.type.kind === "date" ||
|
||||
e.left.type.kind === "url" ||
|
||||
e.left.type.kind === "object" ||
|
||||
e.left.type.kind === "record" ||
|
||||
@@ -2372,9 +2376,13 @@ function validateFunction(
|
||||
(e.method === "sortPrimitive" || e.method === "toSortedPrimitive") &&
|
||||
elem.kind !== "string" &&
|
||||
elem.kind !== "f64" &&
|
||||
elem.kind !== "bool"
|
||||
elem.kind !== "bool" &&
|
||||
elem.kind !== "date"
|
||||
) {
|
||||
err(`arrIntrinsic ${e.method} requires primitive elements, got ${elem.kind}`, e.loc);
|
||||
err(
|
||||
`arrIntrinsic ${e.method} requires primitive or Date elements, got ${elem.kind}`,
|
||||
e.loc,
|
||||
);
|
||||
}
|
||||
if (e.method === "sortValues" || e.method === "sortDense") {
|
||||
const arms = elem.kind === "union" ? unions.get(elem.unionId)?.arms : undefined;
|
||||
@@ -2456,13 +2464,18 @@ function validateFunction(
|
||||
elem.kind !== "f64" &&
|
||||
elem.kind !== "string" &&
|
||||
elem.kind !== "bool" &&
|
||||
elem.kind !== "date" &&
|
||||
!(
|
||||
elem.kind === "union" &&
|
||||
(unions
|
||||
.get(elem.unionId)
|
||||
?.arms.every(
|
||||
(a) =>
|
||||
a.kind === "f64" || a.kind === "string" || a.kind === "bool" || isUnitType(a),
|
||||
a.kind === "f64" ||
|
||||
a.kind === "string" ||
|
||||
a.kind === "bool" ||
|
||||
a.kind === "date" ||
|
||||
isUnitType(a),
|
||||
) ??
|
||||
false)
|
||||
)
|
||||
@@ -5341,6 +5354,39 @@ function validateFunction(
|
||||
}
|
||||
|
||||
function checkLibSpecialCall(e: IrExpr & { kind: "libCall" }): boolean {
|
||||
if (e.fn === "promise.fulfilledValue") {
|
||||
const promise = e.args[0]?.type;
|
||||
if (
|
||||
promise?.kind !== "promise" ||
|
||||
e.type.kind === "void" ||
|
||||
isUnitType(e.type) ||
|
||||
promise.inner.kind !== "void"
|
||||
)
|
||||
err(`libCall ${e.fn} must read a Promise<void> entry at a non-void type`, e.loc);
|
||||
return true;
|
||||
}
|
||||
if (e.fn === "promise.allTuple") {
|
||||
const [entries, build] = e.args;
|
||||
if (
|
||||
entries?.type.kind !== "array" ||
|
||||
entries.type.elem.kind !== "promise" ||
|
||||
entries.type.elem.inner.kind !== "void" ||
|
||||
build?.kind !== "closure" ||
|
||||
build.captures.length > 0 ||
|
||||
build.type.kind !== "func" ||
|
||||
e.type.kind !== "promise" ||
|
||||
!typeEquals(build.type.ret, e.type.inner)
|
||||
)
|
||||
err(`libCall ${e.fn} must join Promise<void> entries into its builder's tuple`, e.loc);
|
||||
return true;
|
||||
}
|
||||
if (e.fn === "als.bindCallback") {
|
||||
// The bound function, at its closure argument's own type.
|
||||
const fn = e.args[0]?.type;
|
||||
if (fn?.kind !== "func" || !typeEquals(e.type, fn))
|
||||
err(`libCall ${e.fn} must return its function argument's type`, e.loc);
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
e.fn === "crypto.randomFillView" ||
|
||||
e.fn === "crypto.randomFillViewRest" ||
|
||||
|
||||
@@ -63,6 +63,7 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [
|
||||
["crypto.generateKeyPair", "asymmetric KeyObjects, generateKeyPair, sign and verify"],
|
||||
["crypto.signOneshot", "asymmetric KeyObjects, generateKeyPair, sign and verify"],
|
||||
["crypto.verifyOneshot", "asymmetric KeyObjects, generateKeyPair, sign and verify"],
|
||||
["crypto.diffieHellman", "crypto.diffieHellman"],
|
||||
["webcrypto.", "WebCrypto (crypto.subtle)"],
|
||||
["fs.watch", "fs.watch"],
|
||||
["watcher.", "fs.watch"],
|
||||
|
||||
@@ -137,6 +137,7 @@ async function main(): Promise<number> {
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -176,7 +176,9 @@ test("serialized IR travels as bounded parts through the frontend cache", () =>
|
||||
const key = readdirSync(family).find((name) => /^[0-9a-f]{64}$/.test(name))!;
|
||||
const payload = JSON.parse(readFileSync(join(family, key), "utf8"));
|
||||
expect(payload.schema).toBe("scriptc.native-frontend.v3");
|
||||
expect(payload.input.llvm).toBe("");
|
||||
// The placeholders keep NativeExecutableInput's member types: the native
|
||||
// compiler checks the parsed entry against them when it reads it back.
|
||||
expect(payload.input.llvm).toEqual([]);
|
||||
expect(payload.input.ir).toBeNull();
|
||||
expect(payload.llvm).toBe(first.llvm.join(""));
|
||||
expect(payload.ir).toEqual(first.ir);
|
||||
|
||||
@@ -59,7 +59,7 @@ export interface NativeExecutableInput {
|
||||
|
||||
/** The two large artifacts follow the input metadata as separate members so
|
||||
* an entry can be written as parts instead of one JSON string; the stored
|
||||
* input carries empty placeholders (`""`, `null`) for them. */
|
||||
* input carries empty placeholders (`[]`, `null`) for them. */
|
||||
interface FrontendCacheEntry {
|
||||
schema: "scriptc.native-frontend.v3";
|
||||
probes: FrontendInputSnapshot;
|
||||
@@ -78,7 +78,7 @@ function* frontendCacheParts(
|
||||
const metadata = JSON.stringify({
|
||||
schema: "scriptc.native-frontend.v3",
|
||||
probes,
|
||||
input: { ...input, llvm: "", ir: null },
|
||||
input: { ...input, llvm: [], ir: null },
|
||||
});
|
||||
yield metadata.slice(0, metadata.length - 1) + ',"llvm":"';
|
||||
for (const part of joinedParts(input.llvm))
|
||||
@@ -239,6 +239,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures {
|
||||
"dgram",
|
||||
"dnsPromises",
|
||||
"dgramDyn",
|
||||
"textCodecDyn",
|
||||
"watch",
|
||||
"foreignFfi",
|
||||
"nodeTest",
|
||||
|
||||
@@ -114,6 +114,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -568,9 +568,7 @@
|
||||
"id": "node-builtin.crypto.diffieHellman",
|
||||
"kind": "node-builtin",
|
||||
"name": "crypto.diffieHellman",
|
||||
"status": "unsupported",
|
||||
"code": "SC2020",
|
||||
"note": "crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets"
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.crypto.generateKey",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user