Merge main

This commit is contained in:
Chris Tate
2026-10-22 06:59:47 -05:00
274 changed files with 19956 additions and 1001 deletions
+1
View File
@@ -241,6 +241,7 @@
"packages/compiler/src/frontend/comptime-node.ts",
"packages/compiler/src/frontend/ts7/world-check.ts",
"packages/compiler/src/frontend/ts7/source-parser.test.ts",
"packages/compiler/src/frontend/jsx-source.test.ts",
"packages/compiler/src/frontend/npm-static-rewrite.ts",
"packages/compiler/src/frontend/npm-static-bundled-cjs.ts",
"packages/compiler/src/library/semantic-source.ts",
+2
View File
@@ -34,6 +34,8 @@ title: "Limitations"
<include>../limitations/language-features/commonjs-require.mdx</include>
<include>../limitations/language-features/jsx.mdx</include>
### Types and class definitions
<include>../limitations/types-and-class-definitions/bigint.mdx</include>
@@ -4,7 +4,7 @@
`copy()` of a Hash whose stream flushed, and stream use of a value after its stream closed, throw a coded SC2020 error.
Through `unknown` and `any` slots and untyped JavaScript, Hash and Hmac values keep their identity and support `update()`, `digest()`, and Hash's `copy()`, called or read as values; their stream and event members there throw a coded SC2020 error.
`timingSafeEqual` compares any view or ArrayBuffer.
Secret KeyObjects (`createSecretKey`, `generateKey` and `generateKeySync` for `hmac` and `aes`) support `type`, `symmetricKeySize`, `export()`, `equals()`, and `instanceof KeyObject`, and createHmac, hkdf, createCipheriv, and createDecipheriv accept them; asymmetric keys, `KeyObject.from()`, and `toCryptoKey()` are not available.
Secret KeyObjects (`createSecretKey`, `generateKey` and `generateKeySync` for `hmac` and `aes`) support `type`, `symmetricKeySize`, `export()`, `equals()`, and `instanceof KeyObject`, and createHmac, hkdf, createCipheriv, and createDecipheriv accept them; `KeyObject.from()` and `toCryptoKey()` are not available.
`randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`.
Scrypt supports its default parameters, numeric options, aliases, and memory limit.
Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers.
@@ -18,8 +18,21 @@
`createECDH` and `ECDH.convertKey` support prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1, and brainpoolP512r1, on native targets, through `generateKeys()`, `computeSecret()`, `getPublicKey()`, `getPrivateKey()`, `setPrivateKey()`, and `setPublicKey()`, in compressed, uncompressed, and hybrid forms; the other curves `crypto.getCurves()` lists and wasm32-wasi builds are refused.
A call on an ECDH value whose arguments no @types/node overload accepts (`getPublicKey("HEX")`) is refused at compile time, as is `computeSecret(view, encoding)`: Node reads that encoding as the input encoding and answers a Buffer where @types/node declares a string.
ECDH property reads and writes throw a coded SC2020 error.
Other public-key operations remain unsupported.
RSA, EC, and Ed25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets.
The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1.
Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate.
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.
Comparing keys of different types with `equals()` answers false; Node also leaves an OpenSSL error queued, which a later key operation can report.
Diffie-Hellman and `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.
Its `subtle` object, also node:crypto's `subtle` export, supports `generateKey()` and `exportKey()` for HMAC and AES keys, `importKey()` for HMAC, AES, PBKDF2, and HKDF keys, `encrypt()` and `decrypt()` with AES-GCM, AES-CBC, and AES-CTR and `wrapKey()` and `unwrapKey()` with AES-KW or those ciphers on native targets, `deriveBits()` and `deriveKey()` with PBKDF2 and HKDF, HMAC `sign()` and `verify()`, and SHA-1 and SHA-2 `digest()`.
Secret keys import and export in the `raw`, `raw-secret`, and `jwk` formats; other SubtleCrypto methods, algorithms, and key formats are refused.
Its `subtle` object, also node:crypto's `subtle` export, supports `generateKey()` and `exportKey()` for HMAC and AES keys, `importKey()` for HMAC, AES, PBKDF2, and HKDF keys, `encrypt()` and `decrypt()` with AES-GCM, AES-CBC, and AES-CTR and `wrapKey()` and `unwrapKey()` of secret keys with AES-KW or those ciphers on native targets, `deriveBits()` and `deriveKey()` with PBKDF2 and HKDF, HMAC `sign()` and `verify()`, and SHA-1 and SHA-2 `digest()`.
Secret keys import and export in the `raw`, `raw-secret`, and `jwk` formats.
`generateKey()`, `importKey()`, `exportKey()`, `sign()`, and `verify()` also support ECDSA (P-256, P-384, and P-521), Ed25519, RSASSA-PKCS1-v1_5, and RSA-PSS, with the `spki`, `pkcs8`, and `jwk` formats and `raw` for ECDSA and Ed25519 public keys; ECDSA, Ed25519, and RSA are refused in wasm32-wasi builds.
ECDSA, Ed25519, and RSA link when a string literal in the program names the algorithm; a name built only at run time throws a coded SC2020 error.
`wrapKey()` and `unwrapKey()` of ECDSA, Ed25519, and RSA keys throw a coded SC2020 error.
Other SubtleCrypto methods, algorithms, and key formats are refused or throw a coded SC2020 error.
Native derivations finish before their deferred callbacks, so computation blocks the caller.
@@ -1,13 +1,17 @@
- **Streams and readline:** Stream chunks are Buffers or strings.
- **Streams and readline:** Stream chunks are Buffers or strings, and on an object-mode side any value that converts to a checked-dynamic value.
`NodeJS.ReadableStream` and `NodeJS.WritableStream`, and `stream.Readable` and `stream.Writable` under `@types/node`, accept every stream: when a program passes file, zlib, or `node:stream` streams through them, child-process pipes and `process.stdout` and `process.stderr` join those streams as `Readable` and `Writable` instances.
`readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream.
When an untyped value reaches `NodeJS.ReadableStream` or `NodeJS.WritableStream`, those two interfaces hold any value, so JavaScript stream objects (constructor-function streams, plain objects with `on` and `pipe`) pass through TypeScript functions typed with them and come back to JavaScript as the same objects.
Member calls on such a value use the native stream when it is one and call the object's own members otherwise; `readable.pipe()` into a JavaScript destination follows Node's pipe protocol.
A `pipeline` stage and a `for await` source that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error.
Native streams expose no `emit` to JavaScript, so a JavaScript stream whose own `pipe()` emits on its destination (the legacy `Stream` pipe) cannot pipe into one.
`Readable.from` accepts arrays, Sets, Map and Set iterators, generators, async generators, and class iterables and async iterables whose values are strings or Buffers, and in JavaScript any iterable or async iterable of them; a stream that mixes strings and Buffers is unsupported.
`Readable.from` accepts strings, Buffers, arrays, Sets, Maps, Map and Set iterators, generators, async generators, and class iterables and async iterables, and in JavaScript any iterable or async iterable.
In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported.
Object-mode streams of other values (including `Readable.from` over a `Map`, whose values are entry arrays) are unsupported.
The `objectMode` option, and the `readableObjectMode` and `writableObjectMode` options of a `Duplex`, `Transform`, or `PassThrough`, take `true` or `false` literals in an options object literal; options passed as a value are read when the stream is built.
In typed code, a `write` or `transform` callback, or a `_write` or `_transform` method, may declare its chunk with any type that converts to a checked-dynamic value: the chunk converts when the callback runs, and a value of another type throws a TypeError there.
An object-mode write passes its encoding argument to `write` and `transform` as Node does (`undefined` when absent, `null` when a callback took its place); a parameter declared `string` receives `""` for `undefined` and `null`, and one declared `string | null | undefined` receives Node's value.
`readable.read()` on an object-mode stream answers its Buffers; another value throws a not-supported error (read such values with `'data'` listeners, `for await`, or `pipe`).
`node:stream/consumers` and readline interfaces read object-mode streams of strings and Buffers; other values throw a not-supported error.
`pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), and an async function destination whose value resolves the promise form and reaches the callback's second argument.
A stage is written inline, or named by a function declaration or a `const` bound to a function expression that nothing reassigns.
The source may also be an array, a string, a synchronous generator object, or a synchronous generator function without parameters; their elements reach the next stage on the same ticks as in Node.
@@ -28,7 +28,7 @@ The `captureRejections` option and `events.errorMonitor` listeners are not suppo
The stream calls the object's `_read`, `_write`, `_final`, `_transform`, `_flush`, and `_destroy` with the object as `this`, and the stream prototypes' methods and accessors work on it.
Its listeners live in the native stream rather than in `_events`, and `_readableState` and `_writableState` are read-only views that answer the commonly read fields.
Typed code that receives such an object as a stream class uses its native stream, and piping a native stream into it works.
The options the native streams refuse (`objectMode`, `decodeStrings: false`, `signal`, and others) are refused here too, as are `_writev`, `_construct`, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, strings written in encodings other than UTF-8, and pipes into a destination that overrides `write()` or `end()`.
The options the native streams refuse (`decodeStrings: false`, `signal`, and others) are refused here too, as are `_writev`, `_construct`, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, strings written to a byte-mode stream in encodings other than UTF-8, and pipes into a destination that overrides `write()` or `end()`.
Other class constructors passed to `util.inherits` or `util.deprecate` are refused.
Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported.
A stored `util.deprecate` reports a `length` of 4, where Node reports 3.
@@ -0,0 +1,5 @@
- **JSX:** JSX in `.tsx`, `.jsx` and JavaScript files compiles with the project's `jsx` setting: `"react"` with `jsxFactory`, `jsxFragmentFactory` or `reactNamespace`, and `"react-jsx"` or `"react-jsxdev"` with `jsxImportSource`, as well as the `@jsx`, `@jsxFrag`, `@jsxImportSource` and `@jsxRuntime` pragmas.
The checker types JSX with these settings, and each element becomes the call TypeScript emits for it, importing the automatic runtime's functions from `<jsxImportSource>/jsx-runtime` or `jsx-dev-runtime`.
`"jsx": "preserve"` and `"react-native"`, which keep JSX in the output, are refused.
The call an element becomes must type-check: a factory or runtime function whose parameters do not accept what an element passes is reported at the element.
With `"react-jsxdev"`, `jsxDEV` receives `undefined` as `self` where `this` has no declared type (in a function that is not a method), which is the value such a function observes when called without a receiver.
+54 -54
View File
@@ -11338,9 +11338,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -11361,9 +11361,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -11407,9 +11407,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -11430,9 +11430,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -11545,9 +11545,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -11568,9 +11568,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -12327,9 +12327,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -12373,9 +12373,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13523,9 +13523,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13546,9 +13546,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13684,9 +13684,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13707,9 +13707,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13730,9 +13730,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13753,9 +13753,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13776,9 +13776,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -13799,9 +13799,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented by a dedicated static compiler/runtime path.",
"verification": "test-backed"
}
},
{
@@ -67806,9 +67806,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -68565,9 +68565,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -19316,6 +19316,10 @@
"tests/corpus/7055-cipher-streams.ts",
"tests/corpus/7056-cipher-stream-errors.ts",
"tests/corpus/7057-cipher-streams.mjs",
"tests/corpus/7080-crypto-ed25519.mjs",
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/corpus/7122-webcrypto-node-crypto.ts",
"tests/corpus/7123-webcrypto-node-crypto.cjs",
"tests/corpus/node24-crypto-kdf-errors.ts",
@@ -19324,6 +19328,7 @@
"tests/corpus/node24-crypto-sha2.ts",
"tests/fixtures/node-types/cipher.ts",
"tests/fixtures/node-types/ecdh.ts",
"tests/fixtures/node-types/keys.ts",
"tests/harness/npm-static.test.ts"
]
},
@@ -19972,8 +19977,15 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.crypto.createPrivateKey"
"status": "partial",
"evidence": "compiler-dedicated:crypto.createPrivateKey",
"tests": [
"tests/corpus/7080-crypto-ed25519.mjs",
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/fixtures/node-types/keys.ts"
]
},
"anchorSource": "exact"
},
@@ -20018,8 +20030,15 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.crypto.createPublicKey"
"status": "partial",
"evidence": "compiler-dedicated:crypto.createPublicKey",
"tests": [
"tests/corpus/7080-crypto-ed25519.mjs",
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/fixtures/node-types/keys.ts"
]
},
"anchorSource": "exact"
},
@@ -20068,8 +20087,15 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.crypto.createSign"
"status": "partial",
"evidence": "compiler-dedicated:crypto.createSign",
"tests": [
"tests/corpus/7080-crypto-ed25519.mjs",
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/fixtures/node-types/keys.ts"
]
},
"anchorSource": "exact"
},
@@ -20091,8 +20117,15 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.crypto.createVerify"
"status": "partial",
"evidence": "compiler-dedicated:crypto.createVerify",
"tests": [
"tests/corpus/7080-crypto-ed25519.mjs",
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/fixtures/node-types/keys.ts"
]
},
"anchorSource": "exact"
},
@@ -20210,8 +20243,15 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.crypto.generateKeyPair"
"status": "partial",
"evidence": "compiler-dedicated:crypto.generateKeyPair",
"tests": [
"tests/corpus/7080-crypto-ed25519.mjs",
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/fixtures/node-types/keys.ts"
]
},
"anchorSource": "exact"
},
@@ -20233,8 +20273,15 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.crypto.generateKeyPairSync"
"status": "partial",
"evidence": "compiler-dedicated:crypto.generateKeyPairSync",
"tests": [
"tests/corpus/7080-crypto-ed25519.mjs",
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/fixtures/node-types/keys.ts"
]
},
"anchorSource": "exact"
},
@@ -21107,8 +21154,15 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.crypto.sign"
"status": "partial",
"evidence": "compiler-dedicated:crypto.sign",
"tests": [
"tests/corpus/7080-crypto-ed25519.mjs",
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/fixtures/node-types/keys.ts"
]
},
"anchorSource": "exact"
},
@@ -21157,8 +21211,15 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.crypto.verify"
"status": "partial",
"evidence": "compiler-dedicated:crypto.verify",
"tests": [
"tests/corpus/7080-crypto-ed25519.mjs",
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/fixtures/node-types/keys.ts"
]
},
"anchorSource": "exact"
},
@@ -23062,8 +23123,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:asymmetricKeyDetails",
"tests": [
"tests/corpus/7080-crypto-ed25519.mjs",
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/fixtures/node-types/keys.ts"
]
},
"anchorSource": "ancestor"
},
@@ -23085,8 +23153,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:asymmetricKeyType",
"tests": [
"tests/corpus/7080-crypto-ed25519.mjs",
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/fixtures/node-types/keys.ts"
]
},
"anchorSource": "ancestor"
},
@@ -23223,8 +23298,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:Sign",
"tests": [
"tests/corpus/7080-crypto-ed25519.mjs",
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/fixtures/node-types/keys.ts"
]
},
"anchorSource": "exact"
},
@@ -23246,8 +23328,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:sign.sign",
"tests": [
"tests/corpus/7080-crypto-ed25519.mjs",
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/fixtures/node-types/keys.ts"
]
},
"anchorSource": "exact"
},
@@ -23269,8 +23358,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:sign.update",
"tests": [
"tests/corpus/7080-crypto-ed25519.mjs",
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/fixtures/node-types/keys.ts"
]
},
"anchorSource": "exact"
},
@@ -23292,8 +23388,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:Verify",
"tests": [
"tests/corpus/7080-crypto-ed25519.mjs",
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/fixtures/node-types/keys.ts"
]
},
"anchorSource": "exact"
},
@@ -23315,8 +23418,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:verify.update",
"tests": [
"tests/corpus/7080-crypto-ed25519.mjs",
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/fixtures/node-types/keys.ts"
]
},
"anchorSource": "exact"
},
@@ -23338,8 +23448,15 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:crypto"
"status": "partial",
"evidence": "compiler-dedicated:verify.verify",
"tests": [
"tests/corpus/7080-crypto-ed25519.mjs",
"tests/corpus/7082-crypto-key-objects.ts",
"tests/corpus/7083-crypto-sign-verify.ts",
"tests/corpus/7084-crypto-key-pairs.cjs",
"tests/fixtures/node-types/keys.ts"
]
},
"anchorSource": "exact"
},
@@ -95421,7 +95538,10 @@
"tests/corpus/2557-tls-ca-store.ts",
"tests/corpus/2596-net-arg-ladders.cjs",
"tests/corpus/6721-tls-alpn-protocols.ts",
"tests/corpus/6722-tls-min-max-version.ts"
"tests/corpus/6722-tls-min-max-version.ts",
"tests/corpus/7087-tls-socket-wrap.ts",
"tests/corpus/7088-tls-socket-connect-tunnel.ts",
"tests/fixtures/node-types/tls-socket-wrap.ts"
]
},
"anchorSource": "chapter"
@@ -96123,8 +96243,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:tls"
"status": "partial",
"evidence": "compiler-feature:tls.tls.TLSSocket",
"tests": [
"tests/corpus/7087-tls-socket-wrap.ts",
"tests/corpus/7088-tls-socket-connect-tunnel.ts",
"tests/fixtures/node-types/tls-socket-wrap.ts"
]
},
"anchorSource": "exact"
},
@@ -96911,8 +97036,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:tls"
"status": "partial",
"evidence": "compiler-feature:tls.'secure'",
"tests": [
"tests/corpus/7087-tls-socket-wrap.ts",
"tests/corpus/7088-tls-socket-connect-tunnel.ts",
"tests/fixtures/node-types/tls-socket-wrap.ts"
]
},
"anchorSource": "exact"
},
@@ -106669,6 +106799,9 @@
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/corpus/7081-webcrypto-ed25519.mjs",
"tests/corpus/7085-webcrypto-ecdsa-rsa.ts",
"tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs",
"tests/corpus/7120-webcrypto-random-uuid.ts",
"tests/corpus/7121-webcrypto-crypto-object.mjs",
"tests/corpus/7124-webcrypto-secret-keys.ts",
@@ -110567,7 +110700,10 @@
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
"tests/fixtures/node-types/webcrypto-hmac.ts",
"tests/corpus/7081-webcrypto-ed25519.mjs",
"tests/corpus/7085-webcrypto-ecdsa-rsa.ts",
"tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"
]
},
"anchorSource": "exact"
@@ -110608,7 +110744,10 @@
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
"tests/fixtures/node-types/webcrypto-hmac.ts",
"tests/corpus/7081-webcrypto-ed25519.mjs",
"tests/corpus/7085-webcrypto-ecdsa-rsa.ts",
"tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"
]
},
"anchorSource": "exact"
@@ -110649,7 +110788,10 @@
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
"tests/fixtures/node-types/webcrypto-hmac.ts",
"tests/corpus/7081-webcrypto-ed25519.mjs",
"tests/corpus/7085-webcrypto-ecdsa-rsa.ts",
"tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"
]
},
"anchorSource": "exact"
@@ -110690,7 +110832,10 @@
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
"tests/fixtures/node-types/webcrypto-hmac.ts",
"tests/corpus/7081-webcrypto-ed25519.mjs",
"tests/corpus/7085-webcrypto-ecdsa-rsa.ts",
"tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"
]
},
"anchorSource": "exact"
@@ -110777,7 +110922,10 @@
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
"tests/fixtures/node-types/webcrypto-hmac.ts",
"tests/corpus/7081-webcrypto-ed25519.mjs",
"tests/corpus/7085-webcrypto-ecdsa-rsa.ts",
"tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"
]
},
"anchorSource": "exact"
@@ -110818,7 +110966,10 @@
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
"tests/fixtures/node-types/webcrypto-hmac.ts",
"tests/corpus/7081-webcrypto-ed25519.mjs",
"tests/corpus/7085-webcrypto-ecdsa-rsa.ts",
"tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"
]
},
"anchorSource": "exact"
@@ -110882,7 +111033,10 @@
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
"tests/fixtures/node-types/webcrypto-hmac.ts",
"tests/corpus/7081-webcrypto-ed25519.mjs",
"tests/corpus/7085-webcrypto-ecdsa-rsa.ts",
"tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"
]
},
"anchorSource": "exact"
@@ -110923,7 +111077,10 @@
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
"tests/fixtures/node-types/webcrypto-hmac.ts",
"tests/corpus/7081-webcrypto-ed25519.mjs",
"tests/corpus/7085-webcrypto-ecdsa-rsa.ts",
"tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"
]
},
"anchorSource": "exact"
@@ -110964,7 +111121,10 @@
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
"tests/fixtures/node-types/webcrypto-hmac.ts",
"tests/corpus/7081-webcrypto-ed25519.mjs",
"tests/corpus/7085-webcrypto-ecdsa-rsa.ts",
"tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"
]
},
"anchorSource": "exact"
@@ -111005,7 +111165,10 @@
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
"tests/fixtures/node-types/webcrypto-hmac.ts",
"tests/corpus/7081-webcrypto-ed25519.mjs",
"tests/corpus/7085-webcrypto-ecdsa-rsa.ts",
"tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"
]
},
"anchorSource": "exact"
@@ -111046,7 +111209,10 @@
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
"tests/fixtures/node-types/webcrypto-hmac.ts",
"tests/corpus/7081-webcrypto-ed25519.mjs",
"tests/corpus/7085-webcrypto-ecdsa-rsa.ts",
"tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"
]
},
"anchorSource": "exact"
@@ -111087,7 +111253,10 @@
"tests/corpus/6293-webcrypto-digest.ts",
"tests/corpus/6294-webcrypto-js-values.mjs",
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/fixtures/node-types/webcrypto-hmac.ts"
"tests/fixtures/node-types/webcrypto-hmac.ts",
"tests/corpus/7081-webcrypto-ed25519.mjs",
"tests/corpus/7085-webcrypto-ecdsa-rsa.ts",
"tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"
]
},
"anchorSource": "exact"
+3 -1
View File
@@ -194,7 +194,7 @@
"stdlib.abort": ["tests/harness/fetch-conformance.test.ts"]
},
"features": [
{ "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.generateKey", "subtle.exportKey", "subtle.encrypt", "subtle.decrypt", "subtle.wrapKey", "subtle.unwrapKey", "subtle.deriveBits", "subtle.deriveKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7130-webcrypto-wrap-key.ts", "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] },
{ "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.generateKey", "subtle.exportKey", "subtle.encrypt", "subtle.decrypt", "subtle.wrapKey", "subtle.unwrapKey", "subtle.deriveBits", "subtle.deriveKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7130-webcrypto-wrap-key.ts", "tests/corpus/7131-webcrypto-wrap-key-errors.mjs", "tests/corpus/7132-jose-pbes2/main.mjs", "tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts", "tests/corpus/7081-webcrypto-ed25519.mjs", "tests/corpus/7085-webcrypto-ecdsa-rsa.ts", "tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"] },
{ "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
{ "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] },
{ "chapter": "webcrypto", "signatures": ["Class: `Crypto`"], "status": "partial", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6293-webcrypto-digest.ts"] },
@@ -369,6 +369,7 @@
{ "chapter": "console", "signatures": ["Class: `Console`"], "status": "partial", "evidence": ["tests/corpus/4900-console-instances.ts", "tests/corpus/4901-console-instance-errors.ts"] },
{ "chapter": "tls", "signatures": ["`alpnProtocol` Type: {string|boolean|null}"], "status": "partial", "evidence": ["tests/corpus/6721-tls-alpn-protocols.ts"] },
{ "chapter": "tls", "signatures": ["`tlsSocket.getProtocol()`"], "status": "partial", "evidence": ["tests/corpus/6722-tls-min-max-version.ts"] },
{ "chapter": "tls", "signatures": ["Class: `tls.TLSSocket`", "Event: `'secure'`"], "status": "partial", "evidence": ["tests/corpus/7087-tls-socket-wrap.ts", "tests/corpus/7088-tls-socket-connect-tunnel.ts", "tests/fixtures/node-types/tls-socket-wrap.ts"] },
{ "chapter": "webstreams", "signatures": ["`readableStream.values([options])`"], "status": "partial", "evidence": ["tests/corpus/2862-web-readable-async-iteration.ts"] },
{ "chapter": "webstreams", "signatures": ["`streamConsumers.text(stream)`", "`streamConsumers.json(stream)`", "`streamConsumers.buffer(stream)`"], "status": "partial", "evidence": ["tests/corpus/2629-stream-consumers.ts", "tests/corpus/2630-stream-consumers-errors.ts"] },
{ "chapter": "webstreams", "signatures": ["`streamConsumers.arrayBuffer(stream)`"], "status": "partial", "evidence": ["tests/corpus/4875-stream-consumers-array-buffer.ts"] },
@@ -421,6 +422,7 @@
{ "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] },
{ "symbols": ["crypto.randomFill", "crypto.randomFillSync", "crypto.randomUUID", "crypto.randomUUIDv7"], "status": "supported", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/1358-crypto-random.ts"] },
{ "symbols": ["crypto.createSecretKey", "crypto.generateKey", "crypto.generateKeySync", "KeyObject", "keyObject.export", "keyObject.equals", "keyObject.symmetricKeySize", "keyObject.type"], "status": "partial", "evidence": ["tests/corpus/5445-crypto-secret-key-objects.ts", "tests/corpus/5446-crypto-secret-key-errors.js"] },
{ "symbols": ["crypto.createPrivateKey", "crypto.createPublicKey", "crypto.generateKeyPair", "crypto.generateKeyPairSync", "crypto.sign", "crypto.verify", "crypto.createSign", "crypto.createVerify", "Sign", "sign.update", "sign.sign", "Verify", "verify.update", "verify.verify", "asymmetricKeyType", "asymmetricKeyDetails"], "status": "partial", "evidence": ["tests/corpus/7080-crypto-ed25519.mjs", "tests/corpus/7082-crypto-key-objects.ts", "tests/corpus/7083-crypto-sign-verify.ts", "tests/corpus/7084-crypto-key-pairs.cjs", "tests/fixtures/node-types/keys.ts"] },
{ "symbols": ["crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] },
{ "symbols": ["crypto.timingSafeEqual"], "status": "supported", "evidence": ["tests/corpus/5444-crypto-timing-safe-equal.js", "tests/corpus/2890-crypto-static-utility.ts"] },
{ "symbols": ["crypto.randomInt"], "status": "partial", "evidence": ["tests/corpus/5442-crypto-random-values.ts", "tests/corpus/2890-crypto-static-utility.ts"] },
+55 -1
View File
@@ -1,12 +1,13 @@
import { execFile } from "node:child_process";
import { createRequire } from "node:module";
import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
import { chmod, mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { delimiter, dirname, join } from "node:path";
import { promisify } from "node:util";
import { expect, test } from "vitest";
import { precompiledRuntimePackTarget } from "../../compiler/src/startup-cache.js";
import { platformLinkerSupportsPersistentCache } from "../../compiler/src/backend/linker.js";
import { toolchainEnvironmentCachePolicy } from "../../compiler/src/backend/toolchain-environment.js";
const execFileAsync = promisify(execFile);
const require = createRequire(import.meta.url);
@@ -102,6 +103,59 @@ test("exact executable repeats skip lowering while edits and damaged outputs sta
}
}, 120_000);
// Sanitize builds compile the LLVM text with the C driver rather than linking a precompiled
// runtime pack, and that route's native cache proof records the output directory.
test.skipIf(process.platform === "win32")(
"sanitize builds compile uncached and keep the executable on unchanged repeats",
async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-cli-executable-sanitize-"));
const cacheRoot = join(dir, "cache");
const entry = join(dir, "main.ts");
const outPath = join(dir, "program");
const build = async (env: NodeJS.ProcessEnv = {}): Promise<{ stderr: string }> => {
const { stderr } = await execFileAsync(
process.execPath,
["--import", tsxLoader, cliEntry, "build", entry, "--sanitize", "-o", outPath],
{
env: { ...process.env, SCRIPTC_CACHE_DIR: cacheRoot, SCRIPTC_TIMING: "1", ...env },
maxBuffer: 4 * 1024 * 1024,
},
);
return { stderr };
};
const run = async (): Promise<string> =>
(await execFileAsync(outPath, [], { maxBuffer: 1024 * 1024 })).stdout;
try {
await mkdir(cacheRoot, { mode: 0o700 });
await writeFile(entry, 'console.log("sanitized");\n');
// With the native cache off, clang compiles the build's private copy of the LLVM text.
await build({ SCRIPTC_NO_CACHE: "1" });
expect(await run()).toBe("sanitized\n");
expect((await build()).stderr).toContain("scriptc lowering");
expect(await run()).toBe("sanitized\n");
const built = await stat(outPath);
const tuTime = (await stat(join(dir, "main.ll"))).mtimeMs;
// The output directory does not change after the native cache proof is recorded, so an
// unchanged repeat keeps the executable instead of linking it again.
expect((await build()).stderr).not.toContain("scriptc lowering");
if (toolchainEnvironmentCachePolicy(process.env).completeArtifacts) {
const repeated = await stat(outPath);
expect(repeated.ino).toBe(built.ino);
expect(repeated.mtimeMs).toBe(built.mtimeMs);
}
expect(await run()).toBe("sanitized\n");
expect((await stat(join(dir, "main.ll"))).mtimeMs).toBe(tuTime);
expect((await readdir(dir)).sort()).toEqual(["cache", "main.ll", "main.ts", "program"]);
} finally {
await rm(dir, { recursive: true, force: true });
}
},
240_000,
);
test.skipIf(process.platform === "win32")(
"an executable appearing earlier on unchanged PATH invalidates the early binary",
async () => {
+2 -2
View File
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
minimum_os: "14.0",
},
program: { object, entry_symbol: "main" },
runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" },
runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" },
runtime_pack: {
kind: "precompiled",
package: "@scriptc/runtime-darwin-arm64",
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
() => null,
(failure: { stderr?: string }) => failure,
);
expect(error?.stderr).toContain("scr_runtime_abi_v25");
expect(error?.stderr).toContain("scr_runtime_abi_v26");
});
});
+372 -33
View File
@@ -544,16 +544,17 @@ declare var DOMException: {
readonly prototype: DOMException;
};
/* WebCrypto's secret-key slice (scr_webcrypto.c): crypto.subtle's
* importKey, generateKey and exportKey for HMAC and AES keys (importKey
* also for PBKDF2 and HKDF keys), AES-GCM/CBC/CTR encrypt and decrypt,
* wrapKey and unwrapKey (AES-KW or those ciphers), PBKDF2 and HKDF
* deriveBits and deriveKey, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as
* exactly that surface (the declarations mirror @types/node's webcrypto
* shapes), plus the global's randomUUID and getRandomValues. Every other
* SubtleCrypto method and crypto member is absent, so a use is an honest
* type error. CryptoKey values are native handles whose four members read
* through the handle. */
/* WebCrypto (scr_webcrypto.c): crypto.subtle's importKey, generateKey and
* exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF
* keys), AES-GCM/CBC/CTR encrypt and decrypt, wrapKey and unwrapKey of
* secret keys (AES-KW or those ciphers), PBKDF2 and HKDF deriveBits and
* deriveKey, HMAC sign/verify, generateKey, importKey, exportKey, sign
* and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and the
* SHA-1/SHA-2 digests, typed as exactly that surface (the declarations
* mirror @types/node's webcrypto shapes), plus the global's randomUUID and
* getRandomValues. Every other SubtleCrypto method and crypto member is
* absent, so a use is an honest type error. CryptoKey values are native
* handles whose four members read through the handle. */
declare var crypto: Crypto;
/* The Crypto and SubtleCrypto constructors (feature probes only) stay
* `unknown`; the values keep this declaration order. */
@@ -662,6 +663,42 @@ interface JsonWebKey {
x?: string;
y?: string;
}
interface EcKeyGenParams extends Algorithm {
namedCurve: string;
}
interface EcKeyImportParams extends Algorithm {
namedCurve: string;
}
interface EcdsaParams extends Algorithm {
hash: HashAlgorithmIdentifier;
}
interface RsaHashedKeyGenParams extends Algorithm {
modulusLength: number;
publicExponent: Uint8Array;
hash: HashAlgorithmIdentifier;
}
interface RsaHashedImportParams extends Algorithm {
hash: HashAlgorithmIdentifier;
}
interface RsaPssParams extends Algorithm {
saltLength: number;
}
interface EcKeyAlgorithm extends KeyAlgorithm {
namedCurve: string;
}
interface RsaHashedKeyAlgorithm extends KeyAlgorithm {
modulusLength: number;
publicExponent: Uint8Array;
hash: KeyAlgorithm;
}
interface CryptoKeyPair {
privateKey: CryptoKey;
publicKey: CryptoKey;
}
/* HMAC and AES keys generate, import and export as 'raw' and 'jwk'
* (PBKDF2 and HKDF keys import as 'raw'); ECDSA and RSA keys generate,
* import and export as 'spki', 'pkcs8' and 'jwk', and ECDSA public keys as
* 'raw'. */
interface SubtleCrypto {
decrypt(
algorithm: AesGcmParams | AesCbcParams | AesCtrParams,
@@ -688,6 +725,11 @@ interface SubtleCrypto {
): Promise<ArrayBuffer>;
exportKey(format: "jwk", key: CryptoKey): Promise<JsonWebKey>;
exportKey(format: Exclude<KeyFormat, "jwk">, key: CryptoKey): Promise<ArrayBuffer>;
generateKey(
algorithm: EcKeyGenParams | RsaHashedKeyGenParams | "Ed25519" | { name: "Ed25519" },
extractable: boolean,
keyUsages: readonly KeyUsage[],
): Promise<CryptoKeyPair>;
generateKey(
algorithm: AesKeyGenParams | HmacKeyGenParams,
extractable: boolean,
@@ -696,18 +738,22 @@ interface SubtleCrypto {
importKey(
format: "jwk",
keyData: JsonWebKey,
algorithm: AlgorithmIdentifier | HmacImportParams,
algorithm: AlgorithmIdentifier | HmacImportParams | EcKeyImportParams | RsaHashedImportParams,
extractable: boolean,
keyUsages: readonly KeyUsage[],
): Promise<CryptoKey>;
importKey(
format: Exclude<KeyFormat, "jwk">,
keyData: BufferSource,
algorithm: AlgorithmIdentifier | HmacImportParams,
algorithm: AlgorithmIdentifier | HmacImportParams | EcKeyImportParams | RsaHashedImportParams,
extractable: boolean,
keyUsages: readonly KeyUsage[],
): Promise<CryptoKey>;
sign(algorithm: AlgorithmIdentifier, key: CryptoKey, data: BufferSource): Promise<ArrayBuffer>;
sign(
algorithm: AlgorithmIdentifier | EcdsaParams | RsaPssParams,
key: CryptoKey,
data: BufferSource,
): Promise<ArrayBuffer>;
unwrapKey(
format: KeyFormat,
wrappedKey: BufferSource,
@@ -718,7 +764,7 @@ interface SubtleCrypto {
keyUsages: readonly KeyUsage[],
): Promise<CryptoKey>;
verify(
algorithm: AlgorithmIdentifier,
algorithm: AlgorithmIdentifier | EcdsaParams | RsaPssParams,
key: CryptoKey,
signature: BufferSource,
data: BufferSource,
@@ -2789,8 +2835,20 @@ declare module "crypto" {
import { Transform } from "stream";
export type KeyObjectType = "secret" | "public" | "private";
export interface JsonWebKey {
kty?: string;
crv?: string;
d?: string;
dp?: string;
dq?: string;
e?: string;
k?: string;
kty?: string;
n?: string;
p?: string;
q?: string;
qi?: string;
x?: string;
y?: string;
[key: string]: unknown;
}
export interface JwkKeyExportOptions {
format: "jwk";
@@ -2798,16 +2856,183 @@ declare module "crypto" {
export interface KeyExportOptions {
format?: "buffer" | undefined;
}
/** Secret keys only; the asymmetric key types are not available. */
export type KeyFormat = "pem" | "der" | "jwk";
export interface AsymmetricKeyExportOptions<T extends KeyFormat> {
type: "pkcs1" | "spki" | "pkcs8" | "sec1";
format: T;
}
export interface AsymmetricKeyDetails {
modulusLength?: number;
publicExponent?: bigint;
namedCurve?: string;
}
/* Secret keys, and RSA and EC private and public keys. */
export class KeyObject {
private constructor();
type: KeyObjectType;
symmetricKeySize?: number;
asymmetricKeyType?: string;
asymmetricKeyDetails?: AsymmetricKeyDetails;
export(options: AsymmetricKeyExportOptions<"pem">): string | Buffer;
export(options: AsymmetricKeyExportOptions<"der">): Buffer;
export(options?: KeyExportOptions): Buffer;
export(options: JwkKeyExportOptions): JsonWebKey;
equals(otherKeyObject: KeyObject): boolean;
}
export interface PrivateKeyInput {
key: string | Buffer;
format?: "pem" | "der" | undefined;
type?: "pkcs1" | "pkcs8" | "sec1" | undefined;
encoding?: string | undefined;
}
export interface PublicKeyInput {
key: string | Buffer;
format?: "pem" | "der" | undefined;
type?: "pkcs1" | "spki" | undefined;
encoding?: string | undefined;
}
export interface JsonWebKeyInput {
key: JsonWebKey;
format: "jwk";
}
export function createPrivateKey(
key: PrivateKeyInput | string | Buffer | JsonWebKeyInput,
): KeyObject;
export function createPublicKey(
key: PublicKeyInput | string | Buffer | KeyObject | JsonWebKeyInput,
): KeyObject;
export interface KeyPairKeyObjectResult {
publicKey: KeyObject;
privateKey: KeyObject;
}
export interface KeyPairSyncResult<T1 extends string | Buffer, T2 extends string | Buffer> {
publicKey: T1;
privateKey: T2;
}
export interface RSAKeyPairKeyObjectOptions {
modulusLength: number;
publicExponent?: number | undefined;
}
export interface ECKeyPairKeyObjectOptions {
namedCurve: string;
}
export interface RSAKeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
modulusLength: number;
publicExponent?: number | undefined;
publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF };
privateKeyEncoding: { type: "pkcs1" | "pkcs8"; format: PrivF };
}
export interface ECKeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
namedCurve: string;
publicKeyEncoding: { type: "pkcs1" | "spki"; format: PubF };
privateKeyEncoding: { type: "sec1" | "pkcs8"; format: PrivF };
}
export function generateKeyPairSync(
type: "rsa",
options: RSAKeyPairOptions<"pem", "pem">,
): KeyPairSyncResult<string, string>;
export function generateKeyPairSync(
type: "rsa",
options: RSAKeyPairOptions<"der", "der">,
): KeyPairSyncResult<Buffer, Buffer>;
export function generateKeyPairSync(
type: "rsa",
options: RSAKeyPairKeyObjectOptions,
): KeyPairKeyObjectResult;
export function generateKeyPairSync(
type: "ec",
options: ECKeyPairOptions<"pem", "pem">,
): KeyPairSyncResult<string, string>;
export function generateKeyPairSync(
type: "ec",
options: ECKeyPairOptions<"der", "der">,
): KeyPairSyncResult<Buffer, Buffer>;
export function generateKeyPairSync(
type: "ec",
options: ECKeyPairKeyObjectOptions,
): KeyPairKeyObjectResult;
export interface ED25519KeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
publicKeyEncoding: { type: "spki"; format: PubF };
privateKeyEncoding: { type: "pkcs8"; format: PrivF };
}
export function generateKeyPairSync(
type: "ed25519",
options: ED25519KeyPairOptions<"pem", "pem">,
): KeyPairSyncResult<string, string>;
export function generateKeyPairSync(
type: "ed25519",
options: ED25519KeyPairOptions<"der", "der">,
): KeyPairSyncResult<Buffer, Buffer>;
export function generateKeyPairSync(type: "ed25519", options?: {}): KeyPairKeyObjectResult;
export function generateKeyPair(
type: "ed25519",
options: {} | undefined,
callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void,
): void;
export function generateKeyPair(
type: "rsa",
options: RSAKeyPairKeyObjectOptions,
callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void,
): void;
export function generateKeyPair(
type: "ec",
options: ECKeyPairKeyObjectOptions,
callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void,
): void;
export interface SigningOptions {
padding?: number | undefined;
saltLength?: number | undefined;
dsaEncoding?: "der" | "ieee-p1363" | undefined;
}
export interface SignPrivateKeyInput extends PrivateKeyInput, SigningOptions {}
export interface SignKeyObjectInput extends SigningOptions {
key: KeyObject;
}
export interface VerifyPublicKeyInput extends PublicKeyInput, SigningOptions {}
export interface VerifyKeyObjectInput extends SigningOptions {
key: KeyObject;
}
export function sign(
algorithm: string | null | undefined,
data: ArrayBufferView,
key: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput,
): Buffer;
export function verify(
algorithm: string | null | undefined,
data: ArrayBufferView,
key: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput,
signature: ArrayBufferView,
): boolean;
/* Sign and Verify accumulate update() data; they are not usable as
* writable streams. */
export class Sign {
private constructor();
update(data: string | ArrayBufferView): this;
update(data: string, inputEncoding: Encoding): this;
sign(
privateKey: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput,
): Buffer;
sign(
privateKey: KeyObject | string | Buffer | SignKeyObjectInput | SignPrivateKeyInput,
outputFormat: BinaryToTextEncoding,
): string;
}
export class Verify {
private constructor();
update(data: string | ArrayBufferView): this;
update(data: string, inputEncoding: Encoding): this;
verify(
object: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput,
signature: ArrayBufferView,
): boolean;
verify(
object: KeyObject | string | Buffer | VerifyKeyObjectInput | VerifyPublicKeyInput,
signature: string,
signatureEncoding?: BinaryToTextEncoding,
): boolean;
}
export function createSign(algorithm: string): Sign;
export function createVerify(algorithm: string): Verify;
export function createSecretKey(key: ArrayBufferView | ArrayBufferLike): KeyObject;
export function createSecretKey(key: string, encoding: BufferEncoding): KeyObject;
export function generateKeySync(type: "hmac" | "aes", options: { length: number }): KeyObject;
@@ -4874,8 +5099,8 @@ declare module "net" {
* unshift — read answers exactly n buffered bytes or null. */
read(size?: number): Buffer | null;
unshift(chunk: Uint8Array): void;
/* secureConnect/session are TLS-socket events declared on the shared
* socket surface (the lowering collapses TLSSocket onto this kind):
/* secureConnect/secure/session are TLS-socket events declared on the
* shared socket surface (the lowering collapses TLSSocket onto this kind):
* the runtime gates on the transport, so a plain socket's
* registration never fires — Node's exact split. 'session' fires
* once with the serialized session (the received-ticket event). */
@@ -4883,7 +5108,15 @@ declare module "net" {
/* After setEncoding the chunk is a string. */
on(event: "data", listener: (chunk: string) => void): void;
on(
event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect",
event:
| "end"
| "connect"
| "ready"
| "timeout"
| "readable"
| "finish"
| "secureConnect"
| "secure",
listener: () => void,
): void;
on(event: "close", listener: (hadError: boolean) => void): void;
@@ -4899,7 +5132,15 @@ declare module "net" {
/* After setEncoding the chunk is a string. */
addListener(event: "data", listener: (chunk: string) => void): void;
addListener(
event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect",
event:
| "end"
| "connect"
| "ready"
| "timeout"
| "readable"
| "finish"
| "secureConnect"
| "secure",
listener: () => void,
): void;
addListener(event: "close", listener: (hadError: boolean) => void): void;
@@ -4914,7 +5155,15 @@ declare module "net" {
/* After setEncoding the chunk is a string. */
once(event: "data", listener: (chunk: string) => void): void;
once(
event: "end" | "connect" | "ready" | "timeout" | "readable" | "finish" | "secureConnect",
event:
| "end"
| "connect"
| "ready"
| "timeout"
| "readable"
| "finish"
| "secureConnect"
| "secure",
listener: () => void,
): void;
once(event: "close", listener: (hadError: boolean) => void): void;
@@ -4965,9 +5214,10 @@ declare module "net" {
on(event: "timeout", listener: (socket: Socket) => void): void;
on(event: "error", listener: (err: Error) => void): void;
/* The WebSocket handover: fires INSTEAD of 'request' for
* Connection: upgrade requests, with the raw socket + head bytes. */
* Connection: upgrade requests, with the raw socket + head bytes;
* 'connect' is its twin for CONNECT requests (HTTP tunneling). */
on(
event: "upgrade",
event: "upgrade" | "connect",
listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void,
): void;
/* http.createServer(handler)'s event twin (a no-parser net server
@@ -4983,7 +5233,7 @@ declare module "net" {
addListener(event: "timeout", listener: (socket: Socket) => void): void;
addListener(event: "error", listener: (err: Error) => void): void;
addListener(
event: "upgrade",
event: "upgrade" | "connect",
listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void,
): void;
addListener(
@@ -4996,7 +5246,7 @@ declare module "net" {
once(event: "timeout", listener: (socket: Socket) => void): void;
once(event: "error", listener: (err: Error) => void): void;
once(
event: "upgrade",
event: "upgrade" | "connect",
listener: (req: import("http").IncomingMessage, socket: Socket, head: Buffer) => void,
): void;
once(
@@ -5467,6 +5717,7 @@ declare module "tls" {
* split — authorizationError is the verify-failure CODE STRING
* (DEPTH_ZERO_SELF_SIGNED_CERT, ...), or null. */
export interface TLSSocket extends Socket {
readonly encrypted: true;
authorized: boolean;
authorizationError: string | null;
/* null before the handshake, then the negotiated protocol or false */
@@ -5478,6 +5729,21 @@ declare module "tls" {
/* whether the handshake resumed a cached session */
isSessionReused(): boolean;
}
/** new tls.TLSSocket(socket, options): the server-mode wrap of an open
* socket (a server connection or an http CONNECT socket). The options
* record reads at runtime; isServer, cert, key, secureContext,
* ALPNProtocols, minVersion and maxVersion are implemented. */
export interface TLSSocketOptions {
isServer?: boolean;
cert?: string | Uint8Array;
key?: string | Uint8Array;
secureContext?: SecureContext;
ALPNProtocols?: string[] | Uint8Array;
minVersion?: string;
maxVersion?: string;
[option: string]: unknown;
}
export const TLSSocket: new (socket: Socket, options: TLSSocketOptions) => TLSSocket;
export interface CipherNameAndProtocol {
name: string;
standardName: string;
@@ -5486,12 +5752,14 @@ declare module "tls" {
export const DEFAULT_MIN_VERSION: string;
export const DEFAULT_MAX_VERSION: string;
/** tls.connect options — the RUNTIME options record (members read at
* runtime; port/host/rejectUnauthorized/ca/servername are the
* runtime; port/host/socket/rejectUnauthorized/ca/servername are the
* honestly-implemented set, every other documented member throws the
* runtime fence — the options-record stance's divergence-66 shape). */
export interface ConnectionOptions {
port?: number;
host?: string;
/* an open socket to run the client handshake over */
socket?: Socket;
rejectUnauthorized?: boolean;
ca?: string | Uint8Array | ReadonlyArray<string | Uint8Array>;
servername?: string;
@@ -6726,11 +6994,10 @@ declare module "node:events" {
* write(chunk, enc, cb) {} })`, Duplex/Transform/PassThrough, push/read,
* flowing vs paused, write/end backpressure + 'drain', pipe/unpipe, and
* destroy — with Node's event orderings. Chunks are BYTES (Buffer), with
* utf8 strings converted at push/write like Node; objectMode and
* setEncoding are declared so uses fence at their sites with their own
* words. `class My extends Readable` (the _read override form) is a
* separate feature and fences at the class declaration. The runtime-fired
* events carry per-class FORCED argument tuples ('data' is one Buffer,
* strings converted at push/write like Node, and any value on an
* object-mode side (objectMode, readableObjectMode, writableObjectMode —
* the object-mode constructor overloads come first). The runtime-fired
* events carry per-class FORCED argument tuples ('data' is one chunk,
* 'error' one Error, 'pipe'/'unpipe' one Readable, the rest empty) — the
* overloads below hand tsc the same contract so unannotated listener
* parameters contextually type in .js sources. */
@@ -6836,6 +7103,51 @@ declare module "stream" {
signal?: AbortSignal;
}
/* Object-mode options (objectMode: true, or a Duplex's or Transform's
* readableObjectMode / writableObjectMode): chunks and completion data
* are any values, and an object-mode write's encoding argument reaches
* write/transform as passed (undefined when absent, null when a
* callback took its place). The constructors take these before the
* byte-mode options, so a byte-mode stream's callbacks keep Buffer
* chunks. */
type ObjectModeEncoding = string | null | undefined;
type ObjectModeFlag =
| { objectMode: true }
| { readableObjectMode: true }
| { writableObjectMode: true };
interface ObjectWritableOptions extends Omit<WritableOptions, "objectMode" | "write"> {
objectMode: true;
write?(
this: Writable,
chunk: any,
encoding: ObjectModeEncoding,
callback: (error?: Error | null) => void,
): void;
}
type ObjectDuplexOptions = Omit<DuplexOptions, "write"> & {
write?(
this: Duplex,
chunk: any,
encoding: ObjectModeEncoding,
callback: (error?: Error | null) => void,
): void;
} & ObjectModeFlag;
type ObjectTransformOptions = Omit<TransformOptions, "transform" | "flush" | "write"> & {
transform?(
this: Transform,
chunk: any,
encoding: ObjectModeEncoding,
callback: (error?: Error | null, data?: any) => void,
): void;
flush?(this: Transform, callback: (error?: Error | null, data?: any) => void): void;
write?(
this: Transform,
chunk: any,
encoding: ObjectModeEncoding,
callback: (error?: Error | null) => void,
): void;
} & ObjectModeFlag;
/* The operators' options: concurrency and highWaterMark bound map's
* in-flight callbacks and queued results. */
interface ArrayOptions {
@@ -6855,7 +7167,10 @@ declare module "stream" {
[Symbol.asyncIterator](): AsyncIterableIterator<any>;
iterator(options?: { destroyOnReturn?: boolean }): AsyncIterableIterator<any>;
push(chunk: Buffer | string | null, encoding?: string): boolean;
/* An object-mode stream's values. */
push(chunk: any, encoding?: string): boolean;
unshift(chunk: Buffer | string, encoding?: string): void;
unshift(chunk: any, encoding?: string): void;
read(size?: number): Buffer | null;
pause(): this;
resume(): this;
@@ -6962,6 +7277,7 @@ declare module "stream" {
}
class Writable extends EventEmitter {
constructor(opts: ObjectWritableOptions);
constructor(opts?: WritableOptions);
/* The underscore-method surface (see Readable). */
_write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void;
@@ -6977,9 +7293,19 @@ declare module "stream" {
encoding: string,
callback?: (error?: Error | null) => void,
): boolean;
/* An object-mode stream's values (the encoding argument reaches
* _write as passed). */
write(chunk: any, callback?: (error?: Error | null) => void): boolean;
write(
chunk: any,
encoding: ObjectModeEncoding,
callback?: (error?: Error | null) => void,
): boolean;
end(callback?: () => void): this;
end(chunk: Buffer | string, callback?: () => void): this;
end(chunk: Buffer | string, encoding: string, callback?: () => void): this;
end(chunk: any, callback?: () => void): this;
end(chunk: any, encoding: ObjectModeEncoding, callback?: () => void): this;
cork(): void;
uncork(): void;
setDefaultEncoding(encoding: string): this;
@@ -7029,6 +7355,7 @@ declare module "stream" {
* mirrors @types/node: extends Readable, the writable half declared
* again. The lowering treats both halves as first-class. */
class Duplex extends Readable {
constructor(opts: ObjectDuplexOptions);
constructor(opts?: DuplexOptions);
/* The underscore-method surface (see Readable). */
_write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void;
@@ -7043,9 +7370,19 @@ declare module "stream" {
encoding: string,
callback?: (error?: Error | null) => void,
): boolean;
/* An object-mode stream's values (the encoding argument reaches
* _write as passed). */
write(chunk: any, callback?: (error?: Error | null) => void): boolean;
write(
chunk: any,
encoding: ObjectModeEncoding,
callback?: (error?: Error | null) => void,
): boolean;
end(callback?: () => void): this;
end(chunk: Buffer | string, callback?: () => void): this;
end(chunk: Buffer | string, encoding: string, callback?: () => void): this;
end(chunk: any, callback?: () => void): this;
end(chunk: any, encoding: ObjectModeEncoding, callback?: () => void): this;
cork(): void;
uncork(): void;
setDefaultEncoding(encoding: string): this;
@@ -7117,16 +7454,18 @@ declare module "stream" {
}
class Transform extends Duplex {
constructor(opts: ObjectTransformOptions);
constructor(opts?: TransformOptions);
/* The underscore-method surface (see Readable). */
_transform(
chunk: any,
encoding: string,
callback: (error?: Error | null, data?: Buffer | string) => void,
callback: (error?: Error | null, data?: any) => void,
): void;
_flush(callback: (error?: Error | null, data?: Buffer | string) => void): void;
_flush(callback: (error?: Error | null, data?: any) => void): void;
}
class PassThrough extends Transform {
constructor(opts: ObjectTransformOptions);
constructor(opts?: TransformOptions);
}
@@ -56,6 +56,8 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
tlsCa: features.tlsCa,
cipher: features.cipher,
ecdh: features.ecdh,
pki: features.pki,
ed25519: features.ed25519,
webcrypto: features.webcrypto,
perfLoop: features.perfLoop,
};
@@ -60,6 +60,8 @@ export async function compilePackedLibrary(
tls: false,
cipher: false,
ecdh: false,
pki: false,
ed25519: false,
webcrypto: false,
perfLoop: false,
tlsCa: false,
@@ -1896,14 +1896,14 @@ export class LlEmitter {
/** Whether a fiberless body running on its caller's stack needs the
* frame scr_async_inline_enter/exit keep: it isolates the Error.stack
* frame chain, which only stack-observing programs push, and the
* AsyncLocalStorage context, which only programs that enter stores
* change (RuntimeFeatures asyncContext), as a fresh fiber would. Other
* programs run the body with a plain call. The frame also counts the
* bodies running on a fiber's stack for the runtime's trap on a fiberless
* body that suspends. */
* frame chain, which only stack-observing programs push, as a fresh fiber
* would. Other programs run the body with a plain call. The body runs in
* its caller's AsyncLocalStorage context either way, as Node runs an async
* body up to its first await. The frame also counts the bodies running on
* a fiber's stack for the runtime's trap on a fiberless body that
* suspends. */
get fiberlessFrames(): boolean {
return this.stackTraces || this.schedulesInContext;
return this.stackTraces;
}
/** The spawn wrapper of a fiberless async function: runs the body on the
@@ -15,23 +15,55 @@ export function streamDataAdapter(
sym = `sc_ee_dad_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
const p = cbT.params[0];
if (
cbT.params.length > 1 ||
(p && p.kind !== "bytes" && p.kind !== "string" && p.kind !== "dyn" && p.kind !== "union")
) {
if (cbT.params.length > 1) {
throw new InternalCompilerError(
"llvm emitter bug: stream data listener param shape (frontend must fence)",
);
}
// A `Buffer | string` union wraps the filled slot; any other union (or
// other type) converts from the chunk's checked-dynamic value.
const bytesOrString =
p?.kind === "union" &&
(() => {
const def = host.unionsById.get(p.unionId);
return (
def !== undefined &&
!host.nullableUnions.has(p.unionId) &&
def.arms.length === 2 &&
def.arms.some((a) => a.kind === "bytes" && a.elem === "u8") &&
def.arms.some((a) => a.kind === "string")
);
})();
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
host.declare(`declare void @scr_closure_release(ptr)`);
// The emit ABI's three slots (scr_stream_emit_data): the bytes chunk,
// the string chunk, and an object-mode entry's value.
const d: string[] = [
`define internal void @${sym}(ptr %cb, ptr %a0, ptr %a1) ${FN_ATTRS} { ; stream 'data' adapter ${typeKey(cbT)}`,
`define internal void @${sym}(ptr %cb, ptr %a0, ptr %a1, ptr %a2) ${FN_ATTRS} { ; stream 'data' adapter ${typeKey(cbT)}`,
`entry:`,
` %capsp = getelementptr inbounds %ScrClosure, ptr %cb, i64 1`,
` %bx = load ptr, ptr %capsp`,
` %orig = call ptr @scr_box_get_ref(ptr %bx) ; the listener, +1`,
];
const finishTyped = (arg: string, argTy: string): void => {
d.push(
` %fnp = getelementptr inbounds %ScrClosure, ptr %orig, i64 0, i32 1`,
` %fn = load ptr, ptr %fnp`,
);
const retTy = host.llType(cbT.ret);
const argList = `ptr %orig, ${argTy} ${arg}`;
if (retTy === "void") {
d.push(` call void %fn(${argList})`);
} else {
d.push(` %ret = call ${retTy} %fn(${argList})`);
if (isRefCounted(cbT.ret)) {
d.push(
` call void ${releaseSym(host.shapeHost, cbT.ret)}(ptr %ret) ; discarded listener result`,
);
}
}
d.push(` call void @scr_closure_release(ptr %orig)`, ` ret void`);
};
const finish = (arg: string | null): void => {
d.push(
` %fnp = getelementptr inbounds %ScrClosure, ptr %orig, i64 0, i32 1`,
@@ -57,8 +89,8 @@ export function streamDataAdapter(
} else if (p.kind === "bytes" || p.kind === "string") {
const msg =
p.kind === "bytes"
? "a 'data' listener declaring a Buffer chunk received a string (the stream has an encoding set)"
: "a 'data' listener declaring a string chunk received a Buffer (call setEncoding, or declare the chunk as a Buffer)";
? "a 'data' listener declaring a Buffer chunk received a string or an object-mode value (the stream has an encoding set, or is in object mode)"
: "a 'data' listener declaring a string chunk received a Buffer or an object-mode value (call setEncoding, or declare the chunk's type)";
const slot = p.kind === "bytes" ? "%a0" : "%a1";
host.declare(`declare void @scr_throw_error_msg(i32, ptr, ${host.sizeType})`);
d.push(
@@ -73,7 +105,7 @@ export function streamDataAdapter(
);
finish("%r0");
d.push(`}`, ``);
} else if (p.kind === "union") {
} else if (p.kind === "union" && bytesOrString) {
// A `Buffer | string` chunk: wrap whichever slot the stream filled at
// its arm of the (boxed) union.
const def = host.unionsById.get(p.unionId);
@@ -94,6 +126,9 @@ export function streamDataAdapter(
host.declare(`declare void @scr_bytes_release_v(ptr)`);
host.declare(`declare ptr @scr_str_retain_v(ptr)`);
host.declare(`declare void @scr_str_release_v(ptr)`);
host.declare(`declare void @scr_throw_error_msg(i32, ptr, ${host.sizeType})`);
const unionMsg =
"a 'data' listener declaring a Buffer | string chunk received an object-mode value that is neither (declare the chunk's type)";
d.push(
` %uslot = alloca ptr`,
` %isb = icmp ne ptr %a0, null`,
@@ -104,6 +139,15 @@ export function streamDataAdapter(
` store ptr %ub, ptr %uslot`,
` br label %go`,
`str:`,
// an object-mode value that is neither: the catchable error the
// one-type adapters throw
` %nos = icmp eq ptr %a1, null`,
` br i1 %nos, label %bad, label %strok`,
`bad:`,
` call void @scr_throw_error_msg(i32 1, ptr ${host.cstr(unionMsg)}, ${host.sizeType} ${Buffer.byteLength(unionMsg, "utf8")})`,
` call void @scr_closure_release(ptr %orig)`,
` ret void`,
`strok:`,
` %rs = call ptr @scr_str_retain_v(ptr %a1)`,
` %us = call ptr @scr_union_new_ref(i32 ${strTag}, ptr %rs, ptr @scr_str_retain_v, ptr @scr_str_release_v, ptr null)`,
` store ptr %us, ptr %uslot`,
@@ -114,11 +158,20 @@ export function streamDataAdapter(
finish("%uv");
d.push(`}`, ``);
} else {
// dyn: box by runtime tag — the JS lane's adapter parameter.
// dyn: the object-mode value, else box by runtime tag — the JS lane's
// adapter parameter. Any other type converts from that value.
host.declare(`declare ptr @scr_dyn_new_buffer(ptr)`);
host.declare(`declare ptr @scr_dyn_new_str(ptr)`);
host.declare(`declare ptr @scr_dyn_retain_v(ptr)`);
d.push(
` %dslot = alloca ptr`,
` %isv = icmp ne ptr %a2, null`,
` br i1 %isv, label %val, label %chunk`,
`val:`,
` %dvv = call ptr @scr_dyn_retain_v(ptr %a2)`,
` store ptr %dvv, ptr %dslot`,
` br label %go`,
`chunk:`,
` %isb = icmp ne ptr %a0, null`,
` br i1 %isb, label %buf, label %str`,
`buf:`,
@@ -132,7 +185,25 @@ export function streamDataAdapter(
`go:`,
` %dv = load ptr, ptr %dslot`,
);
finish("%dv");
if (p.kind === "dyn") {
finish("%dv");
} else {
const helper = host.dyn.dynCheckHelper(p, false);
const ty = host.llType(p);
host.declare(`declare void @scr_dyn_release(ptr)`);
host.declare(`declare zeroext i1 @scr_exc_pending()`);
d.push(
` %tv = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %dv, ptr null)`,
` call void @scr_dyn_release(ptr %dv)`,
` %pend = call zeroext i1 @scr_exc_pending()`,
` br i1 %pend, label %fail, label %conv`,
`fail:`,
` call void @scr_closure_release(ptr %orig)`,
` ret void`,
`conv:`,
);
finishTyped("%tv", ty);
}
d.push(`}`, ``);
}
host.resolveThunkDefs.push(...d);
@@ -164,12 +235,25 @@ export function streamDoneFnFor(
const dataT = kind === "t" || kind === "l" ? doneT.params[1] : undefined;
let bytesTag = -1;
let strTag = -1;
// data of any type other than a union of Buffer, string, undefined and
// null (an object-mode Transform's value) completes through the
// value entry points with the data boxed.
let valueData = false;
if (dataT !== undefined) {
if (dataT.kind !== "union")
throw new InternalCompilerError("llvm emitter bug: stream done data param not a union");
const def = host.unionsById.get(dataT.unionId);
bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes") : -1;
strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
const def = dataT.kind === "union" ? host.unionsById.get(dataT.unionId) : undefined;
valueData =
def === undefined ||
!def.arms.every(
(a) =>
(a.kind === "bytes" && a.elem === "u8") ||
a.kind === "string" ||
a.kind === "undefinedT" ||
a.kind === "nullT",
);
if (!valueData) {
bytesTag = def!.arms.findIndex((a) => a.kind === "bytes");
strTag = def!.arms.findIndex((a) => a.kind === "string");
}
}
const entry =
kind === "w"
@@ -179,10 +263,16 @@ export function streamDoneFnFor(
: kind === "d"
? "scr_stream_destroy_done"
: kind === "t"
? "scr_stream_transform_done"
: "scr_stream_flush_done";
const twoSlot = kind === "t" || kind === "l";
host.declare(`declare void @${entry}(ptr, ptr${twoSlot ? ", ptr, ptr" : ""})`);
? valueData
? "scr_stream_transform_done_value"
: "scr_stream_transform_done"
: valueData
? "scr_stream_flush_done_value"
: "scr_stream_flush_done";
const twoSlot = (kind === "t" || kind === "l") && !valueData;
host.declare(
`declare void @${entry}(ptr, ptr${twoSlot ? ", ptr, ptr" : valueData ? ", ptr" : ""})`,
);
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
host.declare(`declare void @scr_stream_release_v(ptr)`);
host.declare(`declare ptr @scr_error_retain_v(ptr)`);
@@ -190,7 +280,7 @@ export function streamDoneFnFor(
const params = [
"ptr %self",
...(errT !== undefined ? ["ptr %e"] : []),
...(dataT !== undefined ? ["ptr %d"] : []),
...(dataT !== undefined ? [`${host.llType(dataT)} %d`] : []),
];
const d: string[] = [
`define internal void @${sym}(${params.join(", ")}) ${FN_ATTRS} { ; stream '${kind}' done ${typeKey(doneT)}`,
@@ -234,7 +324,15 @@ export function streamDoneFnFor(
unwrap("%d", strTag, "@scr_str_retain_v", "%sslot");
}
d.push(` %ev = load ptr, ptr %eslot`);
if (twoSlot) {
if (valueData) {
const conv = host.dyn.toDynHelper(dataT!);
host.declare(`declare void @scr_dyn_release(ptr)`);
d.push(
` %dv = call ptr @${conv}(${host.llType(dataT!)} %d)`,
` call void @${entry}(ptr %s, ptr %ev, ptr %dv) ; moves err; borrows s and the value`,
` call void @scr_dyn_release(ptr %dv)`,
);
} else if (twoSlot) {
d.push(
` %bv = load ptr, ptr %bslot`,
` %sv = load ptr, ptr %sslot`,
@@ -245,7 +343,8 @@ export function streamDoneFnFor(
}
d.push(` call void @scr_stream_release_v(ptr %s)`);
if (errT !== undefined) d.push(` call void ${releaseSym(host.shapeHost, errT)}(ptr %e)`);
if (dataT !== undefined) d.push(` call void ${releaseSym(host.shapeHost, dataT)}(ptr %d)`);
if (dataT !== undefined && isRefCounted(dataT))
d.push(` call void ${releaseSym(host.shapeHost, dataT)}(ptr %d)`);
d.push(` ret void`, `}`, ``);
host.resolveThunkDefs.push(...d);
return sym;
@@ -611,18 +710,14 @@ export function streamCbThunkFor(
: pos === 1;
if (p.kind === "dyn") {
if (isChunkPos) {
host.declare(`declare ptr @scr_dyn_new_buffer(ptr)`);
d.push(` %dc${i} = call ptr @scr_dyn_new_buffer(ptr %chunk)`);
// the chunk's value: a Buffer, or an object-mode side's value
host.declare(`declare ptr @scr_stream_chunk_value(ptr, ptr)`);
d.push(` %dc${i} = call ptr @scr_stream_chunk_value(ptr %s, ptr %chunk)`);
passed.push(`ptr %dc${i}`);
} else if (isEncPos) {
host.declare(`declare ptr @scr_str_new(ptr, ${host.sizeType})`);
host.declare(`declare ptr @scr_dyn_new_str(ptr)`);
host.declare(`declare void @scr_str_release(ptr)`);
d.push(
` %es${i} = call ptr @scr_str_new(ptr ${host.cstr("buffer")}, ${host.sizeType} 6)`,
` %ed${i} = call ptr @scr_dyn_new_str(ptr %es${i})`,
` call void @scr_str_release(ptr %es${i})`,
);
// 'buffer', or the encoding argument an object-mode write passed
host.declare(`declare ptr @scr_stream_chunk_encoding_value(ptr)`);
d.push(` %ed${i} = call ptr @scr_stream_chunk_encoding_value(ptr %s)`);
passed.push(`ptr %ed${i}`);
} else if (isErrPos) {
// finished/pipeline succeed with UNDEFINED (Node calls the eos
@@ -673,15 +768,71 @@ export function streamCbThunkFor(
}
continue;
}
if (isChunkPos) {
host.declare(`declare ptr @scr_bytes_retain_v(ptr)`);
d.push(` %rc${i} = call ptr @scr_bytes_retain_v(ptr %chunk)`);
if (isChunkPos && p.kind === "bytes") {
// A Buffer, which an object-mode side's value must be (else a
// TypeError, thrown out of the write).
host.declare(`declare ptr @scr_stream_chunk_bytes(ptr, ptr)`);
d.push(
` %rc${i} = call ptr @scr_stream_chunk_bytes(ptr %s, ptr %chunk)`,
` %rcbad${i} = icmp eq ptr %rc${i}, null`,
` br i1 %rcbad${i}, label %rcfail${i}, label %rcok${i}`,
`rcfail${i}:`,
...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []),
` ret void`,
`rcok${i}:`,
);
if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`);
passed.push(`ptr %rc${i}`);
} else if (isEncPos) {
// Node's encoding for decoded (Buffer) chunks is 'buffer'.
host.declare(`declare ptr @scr_str_new(ptr, ${host.sizeType})`);
d.push(` %en${i} = call ptr @scr_str_new(ptr ${host.cstr("buffer")}, ${host.sizeType} 6)`);
} else if (isChunkPos) {
// Any other declared type converts from the chunk's value (an
// object-mode side's value, or the Buffer), or throws out of the
// write.
const helper = host.dyn.dynCheckHelper(p, false);
const ty = host.llType(p);
host.declare(`declare ptr @scr_stream_chunk_value(ptr, ptr)`);
host.declare(`declare void @scr_dyn_release(ptr)`);
host.declare(`declare zeroext i1 @scr_exc_pending()`);
if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`);
d.push(
` %cv${i} = call ptr @scr_stream_chunk_value(ptr %s, ptr %chunk)`,
` %ct${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %cv${i}, ptr null)`,
` call void @scr_dyn_release(ptr %cv${i})`,
` %ctbad${i} = call zeroext i1 @scr_exc_pending()`,
` br i1 %ctbad${i}, label %ctfail${i}, label %ctok${i}`,
`ctfail${i}:`,
...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []),
` ret void`,
`ctok${i}:`,
);
passed.push(`${ty} %ct${i}`);
} else if (isEncPos && p.kind === "string") {
// Node's encoding for decoded (Buffer) chunks is 'buffer'; an
// object-mode side passes the write's encoding argument ("" for
// undefined and null).
host.declare(`declare ptr @scr_stream_chunk_encoding(ptr)`);
d.push(` %en${i} = call ptr @scr_stream_chunk_encoding(ptr %s)`);
passed.push(`ptr %en${i}`);
} else if (isEncPos) {
// Another declared type (string | null | undefined) converts from
// the encoding argument's value.
const helper = host.dyn.dynCheckHelper(p, false);
const ty = host.llType(p);
host.declare(`declare ptr @scr_stream_chunk_encoding_value(ptr)`);
host.declare(`declare void @scr_dyn_release(ptr)`);
host.declare(`declare zeroext i1 @scr_exc_pending()`);
if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`);
d.push(
` %ev${i} = call ptr @scr_stream_chunk_encoding_value(ptr %s)`,
` %et${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %ev${i}, ptr null)`,
` call void @scr_dyn_release(ptr %ev${i})`,
` %etbad${i} = call zeroext i1 @scr_exc_pending()`,
` br i1 %etbad${i}, label %etfail${i}, label %etok${i}`,
`etfail${i}:`,
...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []),
` ret void`,
`etok${i}:`,
);
passed.push(`${ty} %et${i}`);
} else if (isErrPos) {
// destroy's error argument: `Error | null` — wrap type-directedly.
// The finished/pipeline callback ("e") may declare `Error | null |
@@ -924,32 +924,41 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
}
if (e.fn === "emitter.emitData") {
// A user emit('data', chunk) on a stream-rooted receiver: fill the
// matching payload slot of the two-slot 'data' ABI, NULL the other.
// matching payload slot of the three-slot 'data' ABI (bytes, string,
// object-mode value), NULL the others; a checked-dynamic chunk fills
// them by tag (scr_stream_emit_value).
const args = e.args.map((a) => host.emitExpr(a));
const chunkT = e.args[2]!.type;
const both =
chunkT.kind === "string"
? [`ptr null`, `ptr ${args[2]!.name}`]
: [`ptr ${args[2]!.name}`, `ptr null`];
host.declare(`declare zeroext i1 @scr_emitter_emit(ptr, ptr, ...)`);
const t = B.tmp();
B.line(
`${t} = call zeroext i1 (ptr, ptr, ...) @scr_emitter_emit(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ${both.join(", ")})`,
);
if (chunkT.kind === "dyn") {
host.declare(`declare zeroext i1 @scr_stream_emit_value(ptr, ptr, ptr)`);
B.line(
`${t} = call zeroext i1 @scr_stream_emit_value(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name})`,
);
} else {
const slots =
chunkT.kind === "string"
? [`ptr null`, `ptr ${args[2]!.name}`, `ptr null`]
: [`ptr ${args[2]!.name}`, `ptr null`, `ptr null`];
host.declare(`declare zeroext i1 @scr_emitter_emit(ptr, ptr, ...)`);
B.line(
`${t} = call zeroext i1 (ptr, ptr, ...) @scr_emitter_emit(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ${slots.join(", ")})`,
);
}
host.emitPendingCheck();
return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type };
}
if (e.fn === "emitter.onData" || e.fn === "emitter.onDataDyn") {
// The stream-'data' registration: same runtime entries as
// emitter.on/onDyn, but the DATA adapter (the two-slot payload ABI
// — scr_stream_emit_data) behind the arity-2 fixed shim.
// emitter.on/onDyn, but the DATA adapter (the three-slot payload ABI
// — scr_stream_emit_data) behind the arity-3 fixed shim.
const isDyn = e.fn === "emitter.onDataDyn";
const cbT = e.args[isDyn ? 3 : 2]!.type;
if (cbT.kind !== "func")
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} listener not a func`);
const args = e.args.map((a) => host.emitExpr(a));
const adapterFn = host.streamDataAdapter(cbT);
host.declare(`declare void @scr_ee_inv_fixed2(ptr, ptr)`);
host.declare(`declare void @scr_ee_inv_fixed3(ptr, ptr)`);
const t = B.tmp();
if (isDyn) {
host.toHeap(args[3]!);
@@ -959,7 +968,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
);
B.line(
`${t} = call ptr @scr_emitter_on_dyn(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ` +
`ptr ${args[2]!.name}, ptr ${wrapped}, ptr @scr_ee_inv_fixed2, i1 ${args[4]!.name}, i1 ${args[5]!.name})`,
`ptr ${args[2]!.name}, ptr ${wrapped}, ptr @scr_ee_inv_fixed3, i1 ${args[4]!.name}, i1 ${args[5]!.name})`,
);
} else {
host.declare(`declare ptr @scr_closure_retain_v(ptr)`);
@@ -972,7 +981,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
);
B.line(
`${t} = call ptr @scr_emitter_on_via(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ` +
`ptr ${args[2]!.name}, ptr ${wrapped}, ptr @scr_ee_inv_fixed2, i1 ${args[3]!.name}, i1 ${args[4]!.name})`,
`ptr ${args[2]!.name}, ptr ${wrapped}, ptr @scr_ee_inv_fixed3, i1 ${args[3]!.name}, i1 ${args[4]!.name})`,
);
}
const out = host.own({ name: t, type: e.type });
@@ -615,8 +615,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"crypto.generateKeySync": "scr_crypto_generate_key_sync",
"crypto.keyObjectInput": "scr_crypto_key_object_input",
"crypto.isKeyObject": "scr_crypto_is_key_object",
"webcrypto.importKey": "scr_webcrypto_import_key",
"webcrypto.generateKey": "scr_webcrypto_generate_key",
"webcrypto.importKey": "scr_webcrypto_import_key",
"webcrypto.exportKey": "scr_webcrypto_export_key",
"webcrypto.encrypt": "scr_webcrypto_encrypt",
"webcrypto.decrypt": "scr_webcrypto_decrypt",
@@ -1041,6 +1041,14 @@ export const LIB_FN_SYMS: Record<string, string> = {
"crypto.x509ValidFromStr": "scr_crypto_x509_valid_from_str",
"crypto.x509ValidTo": "scr_crypto_x509_valid_to",
"crypto.x509ValidToStr": "scr_crypto_x509_valid_to_str",
"crypto.createPrivateKey": "scr_crypto_create_private_key",
"crypto.createPublicKey": "scr_crypto_create_public_key",
"crypto.generateKeyPairSync": "scr_crypto_generate_key_pair_sync",
"crypto.generateKeyPairCb": "scr_crypto_generate_key_pair_async",
"crypto.signOneshot": "scr_crypto_sign_oneshot",
"crypto.verifyOneshot": "scr_crypto_verify_oneshot",
"crypto.createSign": "scr_crypto_create_sign",
"crypto.createVerify": "scr_crypto_create_verify",
"fs.writeFileModeSync": "scr_fs_write_file_mode",
"fs.chownSync": "scr_fs_chown",
"fs.lchownSync": "scr_fs_lchown",
@@ -1115,10 +1123,13 @@ export const LIB_FN_SYMS: Record<string, string> = {
"readable.pushStr": "scr_stream_push_str",
"readable.pushStrEnc": "scr_stream_push_str_enc",
"readable.pushEncoding": "scr_stream_set_push_encoding",
"stream.objectMode": "scr_stream_object_mode",
"stream.chunkDone": "scr_stream_chunk_done",
"readable.pushNull": "scr_stream_push_null",
"readable.pushDyn": "scr_stream_push_dyn",
"readable.unshift": "scr_stream_unshift",
"readable.unshiftStr": "scr_stream_unshift_str",
"readable.unshiftDyn": "scr_stream_unshift_dyn",
"readable.pause": "scr_stream_pause",
"readable.resume": "scr_stream_resume",
"readable.isPaused": "scr_stream_is_paused",
@@ -1586,6 +1597,7 @@ export function mockTimerSym(
}
export const USES_TIMERS_LIB_FNS = new Set<string>([
"crypto.generateKeyPairCb",
"process.emitWarning",
// Constructor-function streams defer their events on the stream ticks.
"dyn.builtinStreamClass",
@@ -1671,6 +1683,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"readable.pushDyn",
"readable.unshift",
"readable.unshiftStr",
"readable.unshiftDyn",
"readable.read",
"readable.readDyn",
"readable.setEncoding",
@@ -1702,6 +1715,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"console.write",
"writable.writeU",
"writable.writeDyn",
"writable.writeEnc",
"writable.end",
"writable.uncork",
"stream.destroy",
@@ -419,10 +419,34 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
host.emitPendingCheck();
return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type };
}
if (e.fn === "writable.writeEnc") {
// (recv, flags, chunk[, encoding][, cb]) — flags: 1 encoding, 2
// callback (MOVES). Without an encoding the argument is undefined, or
// null when the callback took its place.
const flagsArg = e.args[1]!;
if (flagsArg.kind !== "numLit")
throw new InternalCompilerError("llvm emitter bug: writable.writeEnc flags not a literal");
const flags = flagsArg.value;
const args = e.args.map((a) => host.emitExpr(a));
let at = 3;
const enc = flags & 1 ? args[at++]!.name : writeEncNull(host, flags & 2 ? "null" : "undef");
let cb = "null";
if (flags & 2) {
host.toHeap(args[at]!);
cb = args[at]!.name;
}
host.declare(`declare zeroext i1 @scr_stream_write_enc(ptr, ptr, ptr, ptr)`);
const t = B.tmp();
B.line(
`${t} = call zeroext i1 @scr_stream_write_enc(ptr ${args[0]!.name}, ptr ${args[2]!.name}, ptr ${enc}, ptr ${cb})`,
);
host.emitPendingCheck();
return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type };
}
if (e.fn === "writable.end") {
// (recv, flags[, chunk][, cb]) — flags: 1 bytes chunk, 2 string
// chunk, 8 dyn chunk (write first, Node's end(chunk) decomposition),
// 4 callback.
// (recv, flags[, chunk][, encoding][, cb]) — flags: 1 bytes chunk, 2
// string chunk, 8 dyn chunk (write first, Node's end(chunk)
// decomposition), 16 an encoding after the dyn chunk, 4 callback.
const flagsArg = e.args[1]!;
if (flagsArg.kind !== "numLit")
throw new InternalCompilerError("llvm emitter bug: writable.end flags not a literal");
@@ -432,19 +456,27 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
let chunkB = "null";
let chunkS = "null";
let chunkD: string | null = null;
let encD = "null";
if (flags & 1) chunkB = args[at++]!.name;
else if (flags & 2) chunkS = args[at++]!.name;
else if (flags & 8) chunkD = args[at++]!.name;
if (flags & 16) encD = args[at++]!.name;
else if (chunkD !== null && flags & 4) encD = writeEncNull(host, "null");
let cbName = "null";
if (flags & 4) {
host.toHeap(args[at]!);
cbName = args[at]!.name;
}
if (chunkD !== null) {
host.declare(`declare zeroext i1 @scr_stream_write_dyn(ptr, ptr, ptr)`);
// end(chunk, cb) writes with a null encoding argument (Node's end
// shifts the callback out of it)
host.declare(`declare zeroext i1 @scr_stream_write_enc(ptr, ptr, ptr, ptr)`);
B.line(
`${B.tmp()} = call zeroext i1 @scr_stream_write_dyn(ptr ${args[0]!.name}, ptr ${chunkD}, ptr null)`,
`${B.tmp()} = call zeroext i1 @scr_stream_write_enc(ptr ${args[0]!.name}, ptr ${chunkD}, ptr ${encD}, ptr null)`,
);
// a write that throws (an unknown encoding) leaves the stream as it
// was: Node's end() throws before it ends the stream
host.emitPendingCheck();
}
host.declare(`declare ptr @scr_stream_end(ptr, ptr, ptr, ptr)`);
const t = B.tmp();
@@ -771,3 +803,13 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
// adapters; the handful of union-wrapped reads use the envGet shapes.
return host.emitGenericLibCall(e);
}
/** The encoding argument a write passes when the call has none: undefined
* (a null pointer), or the null value when a callback took its place. */
function writeEncNull(host: LlvmEmitterContext, which: "null" | "undef"): string {
if (which === "undef") return "null";
host.declare(`declare ptr @scr_dyn_new_null()`);
const t = host.B.tmp();
host.B.line(`${t} = call ptr @scr_dyn_new_null()`);
return t;
}
@@ -121,6 +121,13 @@ export function emitTlsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValu
case "tls.sockOnSecureConnect":
host.toHeap(args[1]!);
return finishRuntimeCall(host, e, "scr_tls_sock_on_secure_connect", a);
case "tls.sockOnSecure":
host.toHeap(args[1]!);
return finishRuntimeCall(host, e, "scr_tls_sock_on_secure", a);
case "tls.wrapSocket":
return finishRuntimeCall(host, e, "scr_tls_wrap_socket", [a[0]!, a[1]!, ptr()]);
case "tls.wrapSocketCtx":
return finishRuntimeCall(host, e, "scr_tls_wrap_socket", a);
case "tls.sockOnSession": {
const cb = args[1]!,
type = callbackType(cb);
@@ -91,6 +91,12 @@ export interface NativeLinkFeatures {
/** crypto.createECDH and ECDH.convertKey: scr_ecdh.c plus the mbedTLS
* archive. */
ecdh: boolean;
/** Asymmetric KeyObjects, crypto.sign/verify and Sign/Verify: scr_pki.c
* plus the mbedTLS archive. */
pki: boolean;
/** Ed25519 keys and signatures with pki: scr_ed25519.c (the vendored
* libsodium Ed25519). */
ed25519: boolean;
/** perf_hooks event-loop metrics: scr_perf_loop.c. */
perfLoop: boolean;
tlsCa: boolean;
@@ -6,6 +6,10 @@ export interface CcOptions {
* LLVM backend's .ll — clang compiles IR text natively on the same
* command line, so both ride this one seat. */
cPath: string;
/** Where the program TU's bytes are read from, when that is not `cPath`: an invocation-private
* copy that a concurrent build of another program cannot replace. `cPath` stays the spelling
* clang sees (`__FILE__`, debug file names) and the one cache keys name. */
cSourcePath?: string;
/** Path of the native executable to produce. */
outPath: string;
/** Additional identity for a translation unit whose complete non-system
@@ -250,6 +254,12 @@ export interface CcOptions {
/** crypto.createECDH and ECDH.convertKey: compiles scr_ecdh.c and links
* the mbedTLS archive (shared with tls). */
ecdh?: boolean;
/** Asymmetric KeyObjects and signatures: compiles scr_pki.c and links
* the mbedTLS archive. */
pki?: boolean;
/** Ed25519 with pki: compiles scr_ed25519.c (the vendored libsodium
* Ed25519). */
ed25519?: boolean;
/** crypto.subtle importKey/sign/verify/digest: compiles scr_webcrypto.c. */
webcrypto?: boolean;
/** perf_hooks event-loop metrics: compiles scr_perf_loop.c. */
@@ -114,6 +114,7 @@ interface RuntimeUnitContext {
tlsCa: boolean;
cipher: boolean;
ecdh: boolean;
pki: boolean;
lreObjects: readonly string[];
zlibObjects: readonly string[];
tlsArchive: string | null;
@@ -297,6 +298,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"),
unit((u) => u.opts.nodeTest, "scr_test.c"),
unit((u) => u.opts.webcrypto, "scr_webcrypto.c"),
unit((u) => u.opts.webcrypto === true && u.opts.pki === true, "scr_webcrypto_pki.c"),
unit((u) => u.opts.pki === true && u.opts.ed25519 === true, "scr_ed25519.c"),
// The crypto global's object extends scr_async_dyn.c's.
unit((u) => u.opts.webcrypto && u.opts.dynAsync, "scr_webcrypto_object.c"),
// The CA-store unit rides its own gate OR the tls one: scr_tls.c
@@ -310,6 +313,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
...(u.tls ? [u.rt(join(u.rtDir, "scr_tls.c"))] : []),
...(u.cipher ? [u.rt(join(u.rtDir, "scr_cipher.c"))] : []),
...(u.ecdh ? [u.rt(join(u.rtDir, "scr_ecdh.c"))] : []),
...(u.pki ? [u.rt(join(u.rtDir, "scr_pki.c"))] : []),
...(u.cipher && u.opts.webcrypto ? [u.rt(join(u.rtDir, "scr_webcrypto_aes.c"))] : []),
u.tlsArchive,
// mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h).
@@ -378,7 +382,8 @@ export async function compileCInternal(
// Symmetric ciphers and ECDH link the same mbedTLS archive as TLS.
const cipher = opts.cipher ?? false;
const ecdh = opts.ecdh ?? false;
const mbedtls = tls || cipher || ecdh;
const pki = opts.pki ?? false;
const mbedtls = tls || cipher || ecdh || pki;
const driver = resolveCc();
const darwinDebugSymbols = needsDarwinDebugSymbols(
targetPlatform(driver),
@@ -484,7 +489,7 @@ export async function compileCInternal(
const [compiler, runtimeHash, programBytes] = await Promise.all([
resolvedTool(driver.argv[0] ?? "clang"),
runtimeFingerprint(rtDir),
readFile(opts.cPath),
readFile(opts.cSourcePath ?? opts.cPath),
]);
if (compiler !== null) {
const effectiveCompiler =
@@ -736,6 +741,7 @@ export async function compileCInternal(
tlsCa,
cipher,
ecdh,
pki,
lreObjects,
zlibObjects,
tlsArchive,
@@ -864,7 +870,12 @@ export async function compileCInternal(
);
try {
await materializeVendorPrerequisites(undefined, privateVendorRoot);
await buildExecutable((p) => p);
await buildExecutable(
(p) => p,
opts.cSourcePath === undefined
? {}
: { programPath: opts.cSourcePath, compilerVisibleSource: opts.cPath },
);
} finally {
await rm(privateVendorRoot, { recursive: true, force: true }).catch(() => undefined);
}
@@ -899,7 +910,9 @@ export async function compileCInternal(
localArtifact === null
? runtimeFingerprint(rtDir)
: Promise.resolve(localArtifact.runtimeHash),
localArtifact === null ? readFile(opts.cPath) : Promise.resolve(localArtifact.programBytes),
localArtifact === null
? readFile(opts.cSourcePath ?? opts.cPath)
: Promise.resolve(localArtifact.programBytes),
]);
compileMetadataStamp =
compileMetadataKey === null
@@ -968,7 +981,7 @@ export async function compileCInternal(
Promise.all([
ccVersion(driver.argv, toolchainEnv, true),
runtimeFingerprint(rtDir),
readFile(opts.cPath),
readFile(opts.cSourcePath ?? opts.cPath),
]));
} catch (error) {
// A version/fingerprint probe is an optimization boundary. If the compiler
+2 -2
View File
@@ -2,8 +2,8 @@
* of the link contract: a program object keeps this symbol undefined and the
* matching runtime defines it, so incompatible manual links fail before the
* program can start. */
export const RUNTIME_ABI_VERSION = 25 as const;
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v25" as const;
export const RUNTIME_ABI_VERSION = 26 as const;
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v26" as const;
/** Object consumption is useful today, but the complete scr_* surface may
* still change before 1.0. The versioned marker prevents accidental mixing;
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" },
runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" },
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
flavors: {
@@ -125,6 +125,8 @@ const features = {
tlsCa: false,
cipher: false,
ecdh: false,
pki: false,
ed25519: false,
webcrypto: false,
perfLoop: false,
};
@@ -61,6 +61,8 @@ const BASE: NativeLinkFeatures = {
tlsCa: false,
cipher: false,
ecdh: false,
pki: false,
ed25519: false,
webcrypto: false,
perfLoop: false,
};
@@ -124,7 +126,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
object_format: target.objectFormat,
minimum_os: target.minimumOs,
},
runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" },
runtime_abi: { version: 26, marker: "scr_runtime_abi_v26" },
compiler: {
command: "clang",
identity: "fixture clang",
@@ -2,7 +2,11 @@ import { everyModuleNode, everyStmtList, transformStmtList } from "../ir/travers
import type { LlvmUnsupportedError } from "./llvm/emitter.js";
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
import { type IrModule, type SrcLoc } from "../ir/ir.js";
import { moduleUsesFetch } from "../ir/runtime-features.js";
import {
PKI_LIB_FNS,
moduleUsesFetch,
moduleUsesWebCryptoAsymmetric,
} from "../ir/runtime-features.js";
/** Reflection tables expose methods that the program never calls. On WASI,
* keep their host-only implementations as explicit runtime refusals. Typed
@@ -228,6 +232,14 @@ export function moduleWasiUnavailableSurface(
};
return false;
}
if (PKI_LIB_FNS.has(node.fn)) {
found = {
surface:
"asymmetric keys and signatures (the wasm32-wasi runtime does not include the mbedTLS library)",
loc,
};
return false;
}
if (node.fn === "os.networkInterfaces") {
found = {
surface: "network-interface enumeration (WASI Preview 1 has no interface API)",
@@ -254,5 +266,12 @@ export function moduleWasiUnavailableSurface(
if (moduleUsesFetch(mod)) {
return { surface: "network-backed fetch (WASI Preview 1 has no socket API)", loc: entryLoc };
}
if (moduleUsesWebCryptoAsymmetric(mod)) {
return {
surface:
"WebCrypto ECDSA and RSA (the wasm32-wasi runtime does not include the mbedTLS library)",
loc: entryLoc,
};
}
return null;
}
@@ -67,6 +67,8 @@ export async function warmNativeCaches(
tls: false,
cipher: false,
ecdh: false,
pki: false,
ed25519: false,
webcrypto: false,
perfLoop: false,
tlsCa: false,
@@ -65,6 +65,8 @@ const native: EarlyExecutableNativeFeatures = {
tlsCa: false,
cipher: false,
ecdh: false,
pki: false,
ed25519: false,
webcrypto: false,
perfLoop: false,
};
@@ -133,8 +135,15 @@ test("early executable cache restores the emitted TU, IR, and native feature gat
const hit = await readEarlyExecutableCache(f.root, f.options);
expect(hit?.native).toEqual(native);
expect(hit?.executableRestored).toBe(false);
expect(await readFile(f.llvmPath, "utf8")).toBe("; generated llvm\n");
expect(await readFile(f.irPath, "utf8")).toContain("irVersion");
// The build compiles private copies beside the shared names and installs
// them there when it is done.
expect(hit?.llvmPath).toBe(f.llvmPath);
expect(hit?.irPath).toBe(f.irPath);
expect(hit?.llvmSourcePath).toMatch(/[/\\]\.scriptc-llvm-[^/\\]*\.ll$/);
expect(hit?.irSourcePath).toMatch(/[/\\]\.scriptc-ir-/);
expect(await readFile(hit!.llvmSourcePath!, "utf8")).toBe("; generated llvm\n");
expect(await readFile(hit!.irSourcePath!, "utf8")).toContain("irVersion");
await expect(stat(f.llvmPath)).rejects.toThrow();
expect(await readFile(alternateC, "utf8")).toBe("/* user-owned native source */\n");
});
@@ -18,6 +18,7 @@ import { compilerReleaseVersion } from "../library/sidecar.js";
import { nativeArtifactDependenciesStillMatch } from "../backend/native/artifact-stamps.js";
import { type NativeArtifactDependency } from "../backend/native/contracts.js";
import { optimizationKeyParts } from "../backend/optimization.js";
import { privateSiblingPath } from "../backend/build-cache.js";
import type { CompilerImplementationDependency } from "../library/compiler-self-identity.js";
import {
installDarwinDebugSymbols,
@@ -96,6 +97,8 @@ export interface EarlyExecutableNativeFeatures {
tls: boolean;
cipher: boolean;
ecdh: boolean;
pki: boolean;
ed25519: boolean;
perfLoop: boolean;
tlsCa: boolean;
webcrypto: boolean;
@@ -179,6 +182,15 @@ interface EarlyExecutableImplementationProof {
export interface EarlyExecutableCacheHit {
llvmPath: string;
irPath?: string;
/** When the executable was not restored: private copies of the cached LLVM text and IR beside
* llvmPath and irPath, which the build compiles from and installs at those names when it is done
* (index.ts BuildSources). A restored executable needs no compile, and its files are installed at
* llvmPath and irPath. */
llvmSourcePath?: string;
irSourcePath?: string;
/** llvmPath (irPath) already holds the cached bytes: the build leaves it as it is. */
llvmPathCurrent?: true;
irPathCurrent?: true;
/** True when the final executable was restored after native dependencies
* validated. False restores only frontend artifacts and must call compileC. */
executableRestored: boolean;
@@ -228,6 +240,8 @@ const BOOLEAN_NATIVE_KEYS = [
"tlsCa",
"cipher",
"ecdh",
"pki",
"ed25519",
"webcrypto",
"perfLoop",
] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[];
@@ -575,15 +589,6 @@ async function readExecutableCache(
(await nativeArtifactDependenciesStillMatch(stamp.nativeDependencies));
if (validateRoute !== undefined && !executableRestored) return null;
const paths = outputPaths(options, stamp.native.backend);
if (!(await fileMatches(paths.llvmPath, stamp.files.translationUnit.digest))) {
await installBytes(translationUnit, paths.llvmPath);
}
if (
ir !== null &&
stamp.files.ir !== null &&
!(await fileMatches(paths.irPath, stamp.files.ir.digest))
)
await installBytes(ir, paths.irPath);
if (executableRestored) {
try {
if (debugSymbols !== null) await installDarwinDebugSymbols(debugSymbols, options.outPath);
@@ -598,6 +603,32 @@ async function readExecutableCache(
executableRestored = false;
}
}
// A build that compiles from the cached translation unit compiles its own copy: a concurrent
// build of another program with the same entry name may replace the shared names meanwhile.
let llvmSourcePath: string | undefined;
let irSourcePath: string | undefined;
let llvmPathCurrent = false;
let irPathCurrent = false;
if (executableRestored) {
if (!(await fileMatches(paths.llvmPath, stamp.files.translationUnit.digest)))
await installBytes(translationUnit, paths.llvmPath);
if (
ir !== null &&
stamp.files.ir !== null &&
!(await fileMatches(paths.irPath, stamp.files.ir.digest))
)
await installBytes(ir, paths.irPath);
} else {
// The extension keeps clang reading the copy as LLVM IR rather than as a linker input.
llvmSourcePath = `${privateSiblingPath(paths.llvmPath, "llvm")}.ll`;
await installBytes(translationUnit, llvmSourcePath);
llvmPathCurrent = await fileMatches(paths.llvmPath, stamp.files.translationUnit.digest);
if (ir !== null && stamp.files.ir !== null) {
irSourcePath = privateSiblingPath(paths.irPath, "ir");
await installBytes(ir, irSourcePath);
irPathCurrent = await fileMatches(paths.irPath, stamp.files.ir.digest);
}
}
const now = new Date();
await Promise.all(
[
@@ -619,6 +650,10 @@ async function readExecutableCache(
? { performance: stamp.performance }
: {}),
...(ir === null ? {} : { irPath: paths.irPath }),
...(llvmSourcePath === undefined ? {} : { llvmSourcePath }),
...(irSourcePath === undefined ? {} : { irSourcePath }),
...(llvmPathCurrent ? { llvmPathCurrent: true as const } : {}),
...(irPathCurrent ? { irPathCurrent: true as const } : {}),
};
} catch {
return null;
@@ -0,0 +1,258 @@
/** The HTML entity names JSX text and attribute strings decode (HTML 4's
* 253 named character references, the set TypeScript's JSX emit decodes),
* by name. */
export const JSX_ENTITIES: ReadonlyMap<string, number> = new Map<string, number>([
["quot", 34],
["amp", 38],
["apos", 39],
["lt", 60],
["gt", 62],
["nbsp", 160],
["iexcl", 161],
["cent", 162],
["pound", 163],
["curren", 164],
["yen", 165],
["brvbar", 166],
["sect", 167],
["uml", 168],
["copy", 169],
["ordf", 170],
["laquo", 171],
["not", 172],
["shy", 173],
["reg", 174],
["macr", 175],
["deg", 176],
["plusmn", 177],
["sup2", 178],
["sup3", 179],
["acute", 180],
["micro", 181],
["para", 182],
["middot", 183],
["cedil", 184],
["sup1", 185],
["ordm", 186],
["raquo", 187],
["frac14", 188],
["frac12", 189],
["frac34", 190],
["iquest", 191],
["Agrave", 192],
["Aacute", 193],
["Acirc", 194],
["Atilde", 195],
["Auml", 196],
["Aring", 197],
["AElig", 198],
["Ccedil", 199],
["Egrave", 200],
["Eacute", 201],
["Ecirc", 202],
["Euml", 203],
["Igrave", 204],
["Iacute", 205],
["Icirc", 206],
["Iuml", 207],
["ETH", 208],
["Ntilde", 209],
["Ograve", 210],
["Oacute", 211],
["Ocirc", 212],
["Otilde", 213],
["Ouml", 214],
["times", 215],
["Oslash", 216],
["Ugrave", 217],
["Uacute", 218],
["Ucirc", 219],
["Uuml", 220],
["Yacute", 221],
["THORN", 222],
["szlig", 223],
["agrave", 224],
["aacute", 225],
["acirc", 226],
["atilde", 227],
["auml", 228],
["aring", 229],
["aelig", 230],
["ccedil", 231],
["egrave", 232],
["eacute", 233],
["ecirc", 234],
["euml", 235],
["igrave", 236],
["iacute", 237],
["icirc", 238],
["iuml", 239],
["eth", 240],
["ntilde", 241],
["ograve", 242],
["oacute", 243],
["ocirc", 244],
["otilde", 245],
["ouml", 246],
["divide", 247],
["oslash", 248],
["ugrave", 249],
["uacute", 250],
["ucirc", 251],
["uuml", 252],
["yacute", 253],
["thorn", 254],
["yuml", 255],
["OElig", 338],
["oelig", 339],
["Scaron", 352],
["scaron", 353],
["Yuml", 376],
["fnof", 402],
["circ", 710],
["tilde", 732],
["Alpha", 913],
["Beta", 914],
["Gamma", 915],
["Delta", 916],
["Epsilon", 917],
["Zeta", 918],
["Eta", 919],
["Theta", 920],
["Iota", 921],
["Kappa", 922],
["Lambda", 923],
["Mu", 924],
["Nu", 925],
["Xi", 926],
["Omicron", 927],
["Pi", 928],
["Rho", 929],
["Sigma", 931],
["Tau", 932],
["Upsilon", 933],
["Phi", 934],
["Chi", 935],
["Psi", 936],
["Omega", 937],
["alpha", 945],
["beta", 946],
["gamma", 947],
["delta", 948],
["epsilon", 949],
["zeta", 950],
["eta", 951],
["theta", 952],
["iota", 953],
["kappa", 954],
["lambda", 955],
["mu", 956],
["nu", 957],
["xi", 958],
["omicron", 959],
["pi", 960],
["rho", 961],
["sigmaf", 962],
["sigma", 963],
["tau", 964],
["upsilon", 965],
["phi", 966],
["chi", 967],
["psi", 968],
["omega", 969],
["thetasym", 977],
["upsih", 978],
["piv", 982],
["ensp", 8194],
["emsp", 8195],
["thinsp", 8201],
["zwnj", 8204],
["zwj", 8205],
["lrm", 8206],
["rlm", 8207],
["ndash", 8211],
["mdash", 8212],
["lsquo", 8216],
["rsquo", 8217],
["sbquo", 8218],
["ldquo", 8220],
["rdquo", 8221],
["bdquo", 8222],
["dagger", 8224],
["Dagger", 8225],
["bull", 8226],
["hellip", 8230],
["permil", 8240],
["prime", 8242],
["Prime", 8243],
["lsaquo", 8249],
["rsaquo", 8250],
["oline", 8254],
["frasl", 8260],
["euro", 8364],
["image", 8465],
["weierp", 8472],
["real", 8476],
["trade", 8482],
["alefsym", 8501],
["larr", 8592],
["uarr", 8593],
["rarr", 8594],
["darr", 8595],
["harr", 8596],
["crarr", 8629],
["lArr", 8656],
["uArr", 8657],
["rArr", 8658],
["dArr", 8659],
["hArr", 8660],
["forall", 8704],
["part", 8706],
["exist", 8707],
["empty", 8709],
["nabla", 8711],
["isin", 8712],
["notin", 8713],
["ni", 8715],
["prod", 8719],
["sum", 8721],
["minus", 8722],
["lowast", 8727],
["radic", 8730],
["prop", 8733],
["infin", 8734],
["ang", 8736],
["and", 8743],
["or", 8744],
["cap", 8745],
["cup", 8746],
["int", 8747],
["there4", 8756],
["sim", 8764],
["cong", 8773],
["asymp", 8776],
["ne", 8800],
["equiv", 8801],
["le", 8804],
["ge", 8805],
["sub", 8834],
["sup", 8835],
["nsub", 8836],
["sube", 8838],
["supe", 8839],
["oplus", 8853],
["otimes", 8855],
["perp", 8869],
["sdot", 8901],
["lceil", 8968],
["rceil", 8969],
["lfloor", 8970],
["rfloor", 8971],
["lang", 9001],
["rang", 9002],
["loz", 9674],
["spades", 9824],
["clubs", 9827],
["hearts", 9829],
["diams", 9830],
]);
@@ -0,0 +1,244 @@
import ts5 from "typescript5";
import { afterAll, describe, expect, it } from "vitest";
import { jsxEmitPlan, decodeEntities, jsxPragmas, jsxTextValue, lowerJsx } from "./jsx-source.js";
import { closeSourceParser, parseSourceFile } from "./ts7/source-parser-node.js";
afterAll(() => closeSourceParser());
/** Evaluates a program whose last statement is `result = ...`, with `h` and
* `Fragment` recording the calls the JSX makes. */
function evaluate(code: string): unknown {
const run = new Function(
"h",
"Fragment",
"React",
`"use strict"; let result; ${code}; return result;`,
) as (...args: unknown[]) => unknown;
const h = (tag: unknown, props: unknown, ...children: unknown[]): unknown => ({
tag: typeof tag === "function" ? `fn:${tag.name}` : tag === Fragment ? "#fragment" : tag,
props,
children,
});
const Fragment = { fragment: true };
return run(h, Fragment, { createElement: h, Fragment });
}
/** TypeScript's own classic-runtime emit of the snippet. */
function typescriptEmit(source: string, factory?: string): string {
return ts5.transpileModule(source, {
fileName: "case.tsx",
compilerOptions: {
target: ts5.ScriptTarget.ES2022,
module: ts5.ModuleKind.ESNext,
jsx: ts5.JsxEmit.React,
...(factory === undefined ? {} : { jsxFactory: factory, jsxFragmentFactory: "Fragment" }),
},
}).outputText;
}
function rewrite(source: string, factory?: string): string {
const sf = parseSourceFile("/case.tsx", source, "tsx");
const plan = jsxEmitPlan(
{
jsx: "react",
...(factory === undefined ? {} : { jsxFactory: factory, jsxFragmentFactory: "Fragment" }),
},
jsxPragmas(source),
);
return lowerJsx(sf, plan).text;
}
const cases: Record<string, string> = {
"attributes and text": `const name = "w"; const p = { id: 1 };
result = <div class="hello" data-x='1' disabled {...p} title={name}>Hello {name} &amp; &#x41; &copy;
second line
{/* comment */}
{name}{" "}<b>bold</b>
</div>;`,
fragments: `function Foo() {} Foo.Bar = function Bar() {};
result = <><i /><Foo.Bar x={1} /></>;`,
namespaced: `result = <ns:tag a:b="c" />;`,
"keys and nested maps": `const p = { id: 1 };
result = <div key="k" {...p}>{[1, 2].map((n) => <span key={n}>{n}</span>)}</div>;`,
"white space only": `result = [<div> </div>, <div>
</div>, <div> a {"b"} c </div>];`,
"spreads": `const p = { id: 1 }; const kids = [1, 2];
result = [<div {...p} />, <div {...p} {...p} />, <div>{...kids}</div>, <div {...{ a: 1, b: 2 }} c={3} />, <div {...{ __proto__: null, a: 1 }} />];`,
"attribute strings": `result = <div title="a &amp; b &unknown; &#65; &#x1F600;" alt="line
two" empty="" />;`,
"attribute values": `result = <div a={} b=<i /> c={<b>{1}</b>} d={[<p />]} />;`,
"member and this tags": `class X { Tag = function Tag() {}; render() { return <this.Tag x="1" />; } }
result = new X().render();`,
"dashed and capitalized names": `function Comp() {}
result = [<a-b />, <Comp $x="1" _y="2" z-w="3" />];`,
"nested JSX in expressions": `const items = ["a", "b"];
result = <ul>
{items.length > 1 ? <li>{items[0]}</li> : null}
{items.map((item) =>
<li
key={item}
title={item}
>
{item}
</li>)}
</ul>;`,
};
describe("lowerJsx", () => {
for (const [name, source] of Object.entries(cases)) {
it(`matches TypeScript's emit: ${name}`, () => {
const lowered = rewrite(source);
expect(evaluate(lowered)).toEqual(evaluate(typescriptEmit(source)));
expect(lowered.split("\n").length).toBe(source.split("\n").length);
});
}
it("uses jsxFactory and the pragmas", () => {
const source = `result = <><x a="1" /></>;`;
expect(evaluate(rewrite(source, "h"))).toEqual(evaluate(typescriptEmit(source, "h")));
const pragma = `/** @jsx h */\n/** @jsxFrag Fragment */\nresult = <><x /></>;`;
expect(rewrite(pragma)).toContain(`h(Fragment, null`);
expect(evaluate(rewrite(pragma))).toEqual(evaluate(typescriptEmit(pragma)));
});
it("keeps every byte outside JSX", () => {
const source = `const a = 1; // <not jsx>\nconst b = "<div>";\nresult = [a, b, <i />];\n`;
const lowered = rewrite(source);
expect(
lowered.startsWith(`const a = 1; // <not jsx>\nconst b = "<div>";\nresult = [a, b, `),
).toBe(true);
});
it("maps rewritten positions back to the source", () => {
const source = `const x = 1;\nresult = <div a={x}>{x}</div>;\nconst y = 2;\n`;
const sf = parseSourceFile("/case.tsx", source, "tsx");
const lowered = lowerJsx(sf, { runtime: "classic", factory: "h", fragment: "Fragment" });
const after = lowered.text.indexOf("const y");
expect(lowered.originalPosition(after)).toBe(source.indexOf("const y"));
const copied = lowered.text.indexOf("{ a: x }") + "{ a: ".length;
expect(lowered.originalPosition(copied)).toBe(source.indexOf("{x}>") + 1);
expect(lowered.originalPosition(lowered.text.indexOf("h("))).toBe(source.indexOf("<div"));
});
});
/** Evaluates automatic-runtime output: each runtime import becomes a
* binding of a recording function (`jsx`, `jsxs`, `jsxDEV`, `createElement`)
* or of the fragment marker, under the local name the output chose. */
function evaluateAutomatic(code: string): unknown {
const body = code
.replace(/import \{([^}]*)\} from "[^"]*";/g, (_match, list: string) =>
list
.split(",")
.map((item) => {
const [name, local] = item.trim().split(/\s+as\s+/);
return `const ${local} = runtime.${name};`;
})
.join(" "),
)
.replace(/(^|\s)export /gm, "$1");
const record =
(kind: string) =>
(...args: unknown[]): unknown => ({ kind, args: args.map(named) });
const runtime = {
jsx: record("jsx"),
jsxs: record("jsxs"),
jsxDEV: record("jsxDEV"),
createElement: record("createElement"),
Fragment: "#fragment",
};
const run = new Function("runtime", `"use strict"; let result; ${body}; return result;`) as (
runtime: unknown,
) => unknown;
return run(runtime);
}
/** Functions compare by name: each evaluation declares its own. */
function named(value: unknown): unknown {
if (typeof value === "function") return `fn:${value.name}`;
if (Array.isArray(value)) return value.map(named);
if (value !== null && typeof value === "object")
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, named(item)]));
return value;
}
function typescriptAutomatic(source: string, development: boolean): string {
return ts5.transpileModule(source, {
fileName: "/case.tsx",
compilerOptions: {
target: ts5.ScriptTarget.ES2022,
module: ts5.ModuleKind.ESNext,
jsx: development ? ts5.JsxEmit.ReactJSXDev : ts5.JsxEmit.ReactJSX,
},
}).outputText;
}
function rewriteAutomatic(source: string, development: boolean): string {
const sf = parseSourceFile("/case.tsx", source, "tsx");
const plan = jsxEmitPlan({ jsx: development ? "react-jsxdev" : "react-jsx" }, jsxPragmas(source));
return lowerJsx(sf, plan).text;
}
const automaticCases: Record<string, string> = {
...cases,
"keys": `const p = { id: 1 };
result = [<li key="a">x</li>, <li {...p} key="b" />, <li key="c" {...p} />, <li {...{ a: 1 }} key="d" />, <li {...{ ...p }} key="e" />];`,
"children shapes": `const kids = [1, 2];
result = [<p />, <p>{}</p>, <p>
</p>, <p>one</p>, <p>{1}{2}</p>, <p>{...kids}</p>, <p> </p>, <><b /></>, <></>];`,
"names in use": `const _jsx = "taken"; const _jsxs = 1;
result = [<p>{_jsx}</p>, <p>{_jsxs}{_jsx}</p>];`,
};
describe("lowerJsx with the automatic runtime", () => {
for (const [name, source] of Object.entries(automaticCases)) {
for (const development of [false, true]) {
it(`matches TypeScript's ${development ? "react-jsxdev" : "react-jsx"} emit: ${name}`, () => {
const lowered = rewriteAutomatic(`export {};\n${source}`, development);
const expected = typescriptAutomatic(`export {};\n${source}`, development);
expect(evaluateAutomatic(lowered)).toEqual(evaluateAutomatic(expected));
expect(lowered.split("\n").length).toBe(source.split("\n").length + 1);
});
}
}
it("imports the runtime functions on the first statement's line", () => {
const lowered = rewriteAutomatic(
`"use strict";\nexport const a = <><b key="k" /></>;\n`,
false,
);
expect(lowered).toBe(
`"use strict";import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; \nexport const a = _jsx(_Fragment, { children: _jsx("b", {}, "k") });\n`,
);
});
it("reads @jsxImportSource and @jsxRuntime", () => {
const pragma = `/** @jsxImportSource preact */\nexport const a = <b />;\n`;
expect(rewriteAutomatic(pragma, false)).toContain(`from "preact/jsx-runtime"`);
const classic = `/** @jsxRuntime classic */\n/** @jsx h */\nexport const a = <b />;\n`;
expect(rewriteAutomatic(classic, false)).toContain(`h("b", null)`);
});
});
describe("JSX text", () => {
it("trims lines as TypeScript does", () => {
expect(jsxTextValue(" a \n b \n ")).toBe(" a b");
expect(jsxTextValue("\n \n")).toBeUndefined();
expect(jsxTextValue(" ")).toBe(" ");
});
it("decodes entities", () => {
expect(decodeEntities("&lt;&gt;&amp;&nbsp;&#65;&#x42;&bogus;&#x110000;")).toBe(
"<>& AB&bogus;&#x110000;",
);
});
});
describe("jsxPragmas", () => {
it("reads leading multi-line comments, first @jsx and last @jsxImportSource", () => {
expect(
jsxPragmas(
"#!/usr/bin/env node\n/** @jsx h\n * @jsx g */\n// @jsxFrag F\n/* @jsxImportSource a */ /* @jsxImportSource b */\nconst x = 1; /** @jsx late */",
),
).toEqual({ jsx: "h", jsxImportSource: "b" });
});
});
@@ -0,0 +1,856 @@
/* JSX lowering at the source level, before IR lowering.
*
* A program that contains JSX is checked as written: the checker types JSX
* with the project's jsx settings and reports the errors tsc reports. Each
* file that contains JSX is then rewritten with every JSX expression
* replaced by the calls TypeScript emits for it, and the rewritten program is
* what the lowering compiles, through the same import, call and object
* literal lowering as ordinary code.
*
* The rewrite copies every byte outside JSX expressions, and copies the
* expressions inside them (attribute values, children, spreads) with their
* own JSX rewritten. It keeps every line break of a JSX expression, between
* the arguments it emits, so a position keeps its line in the rewritten
* text. */
import * as ts from "./ts7/adapter.js";
import { jsxOptionName } from "./ts7/enums.js";
import { JSX_ENTITIES } from "./jsx-entities.js";
/** The project's jsx compiler options, as the tsconfig spells them. */
export interface JsxOptions {
jsx?: string | number | undefined;
jsxFactory?: string | undefined;
jsxFragmentFactory?: string | undefined;
jsxImportSource?: string | undefined;
reactNamespace?: string | undefined;
}
/** The tsconfig name of a `jsx` option value (TS7 options carry JsxEmit
* numbers, parsed tsconfigs may carry the name). */
export function jsxEmitName(value: string | number | undefined): string | undefined {
if (typeof value === "number") return jsxOptionName(value);
return value?.toLowerCase();
}
/** The `@jsx`, `@jsxFrag`, `@jsxImportSource` and `@jsxRuntime` pragmas of
* a file's leading comments, as TypeScript reads them: multi-line comments
* only, the first `@jsx` and `@jsxFrag` and the last `@jsxImportSource` and
* `@jsxRuntime`. */
export interface JsxPragmas {
jsx?: string;
jsxFrag?: string;
jsxImportSource?: string;
jsxRuntime?: string;
}
export function jsxPragmas(text: string): JsxPragmas {
const pragmas: JsxPragmas = {};
for (const comment of leadingMultiLineComments(text)) {
const pattern = /@(\S+)(\s+(?:\S.*)?)?$/gm;
for (let match = pattern.exec(comment); match !== null; match = pattern.exec(comment)) {
const argument = match[2]?.trim().split(/\s+/)[0];
if (!argument) continue;
switch (match[1]!.toLowerCase()) {
case "jsx":
pragmas.jsx ??= argument;
break;
case "jsxfrag":
pragmas.jsxFrag ??= argument;
break;
case "jsximportsource":
pragmas.jsxImportSource = argument;
break;
case "jsxruntime":
pragmas.jsxRuntime = argument;
break;
}
}
}
return pragmas;
}
/** The `/* ... *\/` comments before a file's first token (after a shebang). */
function leadingMultiLineComments(text: string): string[] {
const comments: string[] = [];
let i = text.startsWith("#!") ? text.indexOf("\n") : 0;
if (i < 0) return comments;
while (i < text.length) {
const ch = text.charCodeAt(i);
if (isLineBreak(ch) || isWhiteSpaceSingleLine(ch)) {
i++;
} else if (text.startsWith("//", i)) {
while (i < text.length && !isLineBreak(text.charCodeAt(i))) i++;
} else if (text.startsWith("/*", i)) {
const end = text.indexOf("*/", i + 2);
const close = end < 0 ? text.length : end + 2;
comments.push(text.slice(i, close));
i = close;
} else {
break;
}
}
return comments;
}
function isLineBreak(ch: number): boolean {
return ch === 10 || ch === 13 || ch === 0x2028 || ch === 0x2029;
}
function isWhiteSpaceSingleLine(ch: number): boolean {
return (
ch === 32 ||
ch === 9 ||
ch === 11 ||
ch === 12 ||
ch === 160 ||
ch === 133 ||
ch === 5760 ||
(ch >= 8192 && ch <= 8203) ||
ch === 8239 ||
ch === 8287 ||
ch === 12288 ||
ch === 65279
);
}
/** How a file's JSX is emitted, after its pragmas. */
export type JsxEmitPlan =
| {
runtime: "classic";
/** The factory call (`h`, `React.createElement`). */
factory: string;
/** The fragment tag (`Fragment`, `React.Fragment`). */
fragment: string;
}
| {
runtime: "automatic";
/** The module `jsx-runtime` (or `jsx-dev-runtime`) is under, and that
* a `createElement` fallback imports from (`react`). */
importSource: string;
/** jsx: react-jsxdev: `jsxDEV` calls with source locations. */
development: boolean;
};
/** The plan of a file: the automatic runtime with jsx: react-jsx or
* react-jsxdev, an import source or an `@jsxRuntime automatic` pragma (an
* `@jsxRuntime classic` pragma overrides all of them), the classic runtime
* otherwise. */
export function jsxEmitPlan(options: JsxOptions, pragmas: JsxPragmas): JsxEmitPlan {
const mode = jsxEmitName(options.jsx);
const automatic =
pragmas.jsxRuntime !== "classic" &&
(mode === "react-jsx" ||
mode === "react-jsxdev" ||
options.jsxImportSource !== undefined ||
pragmas.jsxImportSource !== undefined ||
pragmas.jsxRuntime === "automatic");
if (automatic) {
return {
runtime: "automatic",
importSource: pragmas.jsxImportSource ?? options.jsxImportSource ?? "react",
development: mode === "react-jsxdev",
};
}
const namespace = options.reactNamespace ?? "React";
return {
runtime: "classic",
factory: pragmas.jsx ?? options.jsxFactory ?? `${namespace}.createElement`,
fragment: pragmas.jsxFrag ?? options.jsxFragmentFactory ?? `${namespace}.Fragment`,
};
}
/** True when the file contains a JSX expression. */
export function containsJsx(sf: ts.SourceFile): boolean {
let found = false;
const visit = (node: ts.Node): void => {
if (found) return;
if (isJsxExpressionRoot(node)) {
found = true;
return;
}
ts.forEachChild(node, visit);
};
visit(sf);
return found;
}
/** The first JSX expression of the file (for diagnostics), or null. */
export function firstJsx(sf: ts.SourceFile): ts.Node | null {
let found: ts.Node | null = null;
const visit = (node: ts.Node): void => {
if (found !== null) return;
if (isJsxExpressionRoot(node)) {
found = node;
return;
}
ts.forEachChild(node, visit);
};
visit(sf);
return found;
}
type JsxRoot = ts.JsxElement | ts.JsxSelfClosingElement | ts.JsxFragment;
function isJsxExpressionRoot(node: ts.Node): node is JsxRoot {
return ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node);
}
/** A file's text with its JSX lowered, and the way back from a position of
* that text to the source position it stands for. */
export interface LoweredJsx {
text: string;
/** A rewritten-text offset as an offset of the original text: copied text
* keeps its offset; emitted call text maps to the JSX it stands for. */
originalPosition(pos: number): number;
/** An original-text offset in the rewritten text: copied text keeps its
* offset; a position inside replaced JSX maps to the call text emitted
* for the JSX at or before it (its end, for the `end` of a span). */
rewrittenPosition(pos: number, end: boolean): number;
}
/** The file's text with its JSX replaced by the calls TypeScript emits for
* the plan, and for the automatic runtime the imports of the runtime
* functions those calls use. */
export function lowerJsx(sf: ts.SourceFile, plan: JsxEmitPlan): LoweredJsx {
return new JsxRewriter(sf, plan).file();
}
/** A run of rewritten text: `copied` text starts at `original`; emitted
* text stands for the source position `original`. */
interface Segment {
rewritten: number;
original: number;
/** The copied length, 0 for emitted text. */
copied: number;
}
class JsxRewriter {
private readonly text: string;
/** Output pieces; `cursor` is the source position whose line breaks the
* output has reached. */
private readonly pieces: string[] = [];
private readonly segments: Segment[] = [];
private length = 0;
private cursor = 0;
constructor(
private readonly sf: ts.SourceFile,
private readonly plan: JsxEmitPlan,
) {
this.text = sf.text;
}
file(): LoweredJsx {
const insertAt = this.plan.runtime === "automatic" ? this.prologueEnd() : -1;
this.copy(0, this.text.length);
if (insertAt >= 0) this.insertImports(insertAt);
const segments = this.segments;
const total = this.length;
return {
text: this.pieces.join(""),
originalPosition: (pos: number): number => {
let low = 0;
let high = segments.length - 1;
while (low < high) {
const mid = (low + high + 1) >> 1;
if (segments[mid]!.rewritten <= pos) low = mid;
else high = mid - 1;
}
const segment = segments[low];
if (segment === undefined) return pos;
return segment.copied > 0
? segment.original + Math.min(pos - segment.rewritten, segment.copied)
: segment.original;
},
rewrittenPosition: (pos: number, end: boolean): number => {
// Segments are in source order: the last one starting at or before
// `pos` holds it, or is the emitted text for the JSX holding it.
let low = 0;
let high = segments.length - 1;
while (low < high) {
const mid = (low + high + 1) >> 1;
if (segments[mid]!.original <= pos) low = mid;
else high = mid - 1;
}
const segment = segments[low];
if (segment === undefined) return pos;
if (segment.copied > 0)
return segment.rewritten + Math.min(pos - segment.original, segment.copied);
return end ? (segments[low + 1]?.rewritten ?? total) : segment.rewritten;
},
};
}
/** Local names of the automatic runtime's functions, by module and
* export, in the order of first use (TypeScript's implicit imports). */
private readonly imports = new Map<string, Map<string, string>>();
private names: Set<string> | null = null;
private fileNameLocal: string | null = null;
/** The local name bound to `name` of `module`: `_name`, or `_name_1`,
* `_name_2`, ... past an identifier the file already uses. */
private importName(name: string, module: string): string {
let names = this.imports.get(module);
if (names === undefined) {
names = new Map();
this.imports.set(module, names);
}
let local = names.get(name);
if (local === undefined) {
local = this.uniqueName(`_${name}`);
names.set(name, local);
}
return local;
}
private uniqueName(base: string): string {
if (this.names === null) {
const names = new Set<string>();
const visit = (node: ts.Node): void => {
if (ts.isIdentifier(node)) names.add(node.text);
ts.forEachChild(node, visit);
};
visit(this.sf);
this.names = names;
}
let name = base;
for (let i = 1; this.names.has(name); i++) name = `${base}_${i}`;
this.names.add(name);
return name;
}
/** Where the runtime imports go: after the leading "use strict"-style
* directives, else before the first statement, so the inserted text
* shares that statement's line. */
private prologueEnd(): number {
const statements = this.sf.statements.nodes;
let end = -1;
for (const statement of statements) {
if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression)) break;
end = statement.end;
}
if (end >= 0) return end;
const first = statements[0];
return first === undefined ? this.text.length : first.getStart(this.sf);
}
/** Inserts the imports (or, in a CommonJS file, the requires) of the
* runtime functions the file's JSX used, and react-jsxdev's file name
* constant, at `at`: imports in the reverse order of their modules' first
* use, as TypeScript inserts each after the prologue. */
private insertImports(at: number): void {
if (this.imports.size === 0 && this.fileNameLocal === null) return;
const esm = ts.isExternalModule(this.sf);
const parts: string[] = [];
for (const [module, names] of [...this.imports].reverse()) {
const specifier = JSON.stringify(module);
if (esm) {
const list = [...names].map(([name, local]) => `${name} as ${local}`).join(", ");
parts.push(`import { ${list} } from ${specifier};`);
} else if (/\.[cm]?jsx?$/.test(this.sf.fileName)) {
const list = [...names].map(([name, local]) => `${name}: ${local}`).join(", ");
parts.push(`const { ${list} } = require(${specifier});`);
}
}
if (this.fileNameLocal !== null)
parts.push(`const ${this.fileNameLocal} = ${JSON.stringify(this.sf.fileName)};`);
if (parts.length === 0) return;
const text = `${parts.join(" ")} `;
let index = 0;
let rewritten = 0;
for (; index < this.segments.length; index++) {
const segment = this.segments[index]!;
if (segment.copied > 0 && segment.original <= at && at <= segment.original + segment.copied) {
rewritten = segment.rewritten + (at - segment.original);
break;
}
}
// Split the copied segment holding `at` and shift what follows.
const holder = this.segments[index];
const joined = this.pieces.join("");
this.pieces.length = 0;
this.pieces.push(joined.slice(0, rewritten), text, joined.slice(rewritten));
const following: Segment[] = [{ rewritten, original: at, copied: 0 }];
if (holder !== undefined && holder.copied > 0) {
const head = at - holder.original;
const tail = holder.copied - head;
holder.copied = head;
if (tail > 0)
following.push({ rewritten: rewritten + text.length, original: at, copied: tail });
}
for (let i = index + 1; i < this.segments.length; i++)
this.segments[i]!.rewritten += text.length;
this.segments.splice(index + 1, 0, ...following);
if (holder !== undefined && holder.copied === 0) this.segments.splice(index, 1);
this.length += text.length;
}
/** Emits source text [start, end) unchanged. */
private copied(start: number, end: number): void {
if (end <= start) return;
this.segments.push({ rewritten: this.length, original: start, copied: end - start });
this.pieces.push(this.text.slice(start, end));
this.length += end - start;
}
/** Emits call text standing for the source position the cursor reached. */
private emit(...texts: string[]): void {
for (const text of texts) {
if (text.length === 0) continue;
const last = this.segments[this.segments.length - 1];
if (last === undefined || last.copied > 0 || last.original !== this.cursor)
this.segments.push({ rewritten: this.length, original: this.cursor, copied: 0 });
this.pieces.push(text);
this.length += text.length;
}
}
/** Copies the source range [start, end), with its JSX expressions
* replaced. */
private copy(start: number, end: number): void {
let at = start;
const visit = (node: ts.Node): void => {
if (node.end <= at || (node !== this.sf && node.getStart(this.sf) >= end)) return;
if (isJsxExpressionRoot(node)) {
const nodeStart = node.getStart(this.sf);
this.copied(at, nodeStart);
if (nodeStart > this.cursor) this.cursor = nodeStart;
this.element(node);
this.sync(node.end);
at = node.end;
return;
}
ts.forEachChild(node, visit);
};
// The cursor never moves back: a range copied after source that follows
// it (jsx()'s key argument, after the children) emits no line break
// twice.
if (start > this.cursor) this.cursor = start;
visit(this.enclosing(start, end));
this.copied(at, end);
if (end > this.cursor) this.cursor = end;
}
/** The smallest node around [start, end) whose children cover it. */
private enclosing(start: number, end: number): ts.Node {
let node: ts.Node = this.sf;
for (;;) {
let next: ts.Node | undefined;
ts.forEachChild(node, (child) => {
if (next === undefined && child.pos <= start && child.end >= end) next = child;
});
if (next === undefined || next === node) return node;
node = next;
}
}
/** Emits the line breaks of the source between the cursor and `pos`. */
private sync(pos: number): void {
for (let i = this.cursor; i < pos; i++) {
const ch = this.text.charCodeAt(i);
if (ch === 13 && this.text.charCodeAt(i + 1) === 10) {
this.cursor = i;
this.emit("\r\n");
i++;
} else if (isLineBreak(ch)) {
this.cursor = i;
this.emit(this.text[i]!);
}
}
if (pos > this.cursor) this.cursor = pos;
}
/** Copies an expression in a JSX expression's place. */
private expression(node: ts.Node): void {
const start = node.getStart(this.sf);
this.sync(start);
this.copy(start, node.end);
}
private element(node: JsxRoot): void {
const plan = this.plan;
if (plan.runtime === "classic") {
this.createElement(node, plan.factory, plan.fragment);
return;
}
const opening = ts.isJsxFragment(node)
? null
: ts.isJsxElement(node)
? node.openingElement
: node;
if (opening !== null && hasKeyAfterPropsSpread(opening.attributes)) {
// A key after a spread of props is not jsx()'s key argument: the
// element is the import source's createElement call.
this.createElement(node, this.importName("createElement", plan.importSource), "");
return;
}
this.automaticElement(node, opening, plan.importSource, plan.development);
}
/** The classic-runtime call: `factory(tag, props or null, ...children)`. */
private createElement(node: JsxRoot, factory: string, fragment: string): void {
const children = ts.isJsxSelfClosingElement(node) ? [] : node.children.nodes;
if (ts.isJsxFragment(node)) {
this.emit(`${factory}(${fragment}, null`);
} else {
const opening = ts.isJsxElement(node) ? node.openingElement : node;
this.emit(`${factory}(`);
this.tagName(opening.tagName);
this.emit(", ");
this.attributes(opening.attributes.properties.nodes, null);
}
for (const child of children) {
if (ts.isJsxText(child)) {
const value = jsxTextValue(child.text);
if (value === undefined) continue;
this.emit(", ");
this.sync(child.getStart(this.sf));
this.emit(JSON.stringify(value));
} else if (ts.isJsxExpression(child)) {
if (child.expression === undefined) continue;
this.emit(", ");
if (child.dotDotDotToken !== undefined) this.emit("...");
this.expression(child.expression);
} else if (isJsxExpressionRoot(child)) {
this.emit(", ");
this.sync(child.getStart(this.sf));
this.element(child);
}
this.sync(child.end);
}
this.sync(node.end);
this.emit(")");
}
/** The automatic-runtime call: `jsx(tag, props[, key])` with the
* children in props, `jsxs` for more than one child or a spread child, and
* react-jsxdev's `jsxDEV(tag, props, key, isStatic, source, self)`. */
private automaticElement(
node: JsxRoot,
opening: ts.JsxOpeningElement | ts.JsxSelfClosingElement | null,
importSource: string,
development: boolean,
): void {
const children: readonly ts.JsxChild[] = ts.isJsxSelfClosingElement(node)
? []
: node.children.nodes;
const semantic = children.filter(isSemanticJsxChild);
const first = semantic[0];
const isStatic =
semantic.length > 1 ||
(first !== undefined && ts.isJsxExpression(first) && first.dotDotDotToken !== undefined);
const module = `${importSource}/${development ? "jsx-dev-runtime" : "jsx-runtime"}`;
this.emit(`${this.importName(development ? "jsxDEV" : isStatic ? "jsxs" : "jsx", module)}(`);
// TypeScript's children prop: the one semantic child itself, else an
// array of every child that emits a value.
const single = semantic.length === 1 && !isStatic ? first! : null;
const listed = single === null ? children.filter((child) => jsxChildEmits(child)) : [];
const childrenProp =
children.length > 0 && (single !== null ? jsxChildEmits(single) : listed.length > 0)
? (): void => {
this.emit("children: ");
if (single !== null) {
this.child(single);
} else {
this.emit("[");
listed.forEach((child, index) => {
if (index > 0) this.emit(", ");
this.child(child);
this.sync(child.end);
});
this.emit("]");
}
}
: null;
let key: ts.JsxAttribute | undefined;
if (opening === null) {
this.emit(this.importName("Fragment", module), ", ");
if (childrenProp === null) this.emit("{}");
else this.attributes([], childrenProp);
} else {
this.tagName(opening.tagName);
this.emit(", ");
const properties = opening.attributes.properties.nodes;
key = properties.find(
(property): property is ts.JsxAttribute =>
ts.isJsxAttribute(property) &&
ts.isIdentifier(property.name) &&
property.name.text === "key",
);
const attributes = properties.filter((property) => property !== key);
if (attributes.length === 0 && childrenProp === null) this.emit("{}");
else this.attributes(attributes, childrenProp);
}
if (key !== undefined) {
this.emit(", ");
this.attributeValue(key.initializer);
}
if (development) {
if (key === undefined) this.emit(", void 0");
const { line, column } = lineAndColumn(this.text, node.pos);
this.fileNameLocal ??= this.uniqueName("_jsxFileName");
this.emit(
`, ${isStatic}, { fileName: ${this.fileNameLocal}, lineNumber: ${line}, columnNumber: ${column} }, `,
jsxSelf(node),
);
}
this.sync(node.end);
this.emit(")");
}
/** A child's value in the children prop (it emits one: jsxChildEmits). */
private child(child: ts.JsxChild): void {
if (ts.isJsxText(child)) {
this.sync(child.getStart(this.sf));
this.emit(JSON.stringify(jsxTextValue(child.text)!));
} else if (ts.isJsxExpression(child)) {
if (child.dotDotDotToken !== undefined) this.emit("...");
this.expression(child.expression!);
} else {
this.sync(child.getStart(this.sf));
this.element(child as JsxRoot);
}
}
private tagName(tag: ts.JsxTagNameExpression): void {
this.sync(tag.getStart(this.sf));
if (ts.isIdentifier(tag) && isIntrinsicJsxName(tag.text)) {
this.emit(JSON.stringify(tag.text));
} else if (ts.isJsxNamespacedName(tag)) {
this.emit(JSON.stringify(`${tag.namespace.text}:${tag.name.text}`));
} else {
this.emit(this.text.slice(tag.getStart(this.sf), tag.end));
}
this.cursor = tag.end;
}
/** The props object of `properties`, then `children` (the automatic
* runtime's children prop), or `null` for no properties and no children
* prop. */
private attributes(
properties: readonly ts.JsxAttributeLike[],
children: (() => void) | null,
): void {
if (properties.length === 0 && children === null) {
this.emit("null");
return;
}
if (properties.length === 0) {
this.emit("{ ");
children!();
this.emit(" }");
return;
}
this.emit("{ ");
let first = true;
const separate = (): void => {
if (!first) this.emit(", ");
first = false;
};
for (const attribute of properties) {
if (ts.isJsxSpreadAttribute(attribute)) {
const value = attribute.expression;
if (ts.isObjectLiteralExpression(value) && !hasProtoProperty(value)) {
// TypeScript inlines the properties of a spread object literal.
for (const property of value.properties.nodes) {
separate();
this.expression(property);
}
} else {
separate();
this.emit("...");
this.expression(value);
}
} else {
separate();
this.sync(attribute.getStart(this.sf));
this.emit(attributeName(attribute.name), ": ");
this.attributeValue(attribute.initializer);
}
this.sync(attribute.end);
}
if (children !== null) {
this.emit(", ");
children();
}
this.emit(" }");
}
private attributeValue(value: ts.JsxAttributeValue | undefined): void {
if (value === undefined) {
this.emit("true");
} else if (ts.isStringLiteral(value)) {
this.sync(value.getStart(this.sf));
this.emit(JSON.stringify(decodeEntities(value.text)));
} else if (ts.isJsxExpression(value)) {
if (value.expression === undefined) this.emit("true");
else this.expression(value.expression);
} else {
this.sync(value.getStart(this.sf));
this.element(value as JsxRoot);
}
}
}
/** A child TypeScript counts: an expression container with an expression,
* text that is not only white space spanning lines, or an element. */
function isSemanticJsxChild(child: ts.JsxChild): boolean {
if (ts.isJsxExpression(child)) return child.expression !== undefined;
if (ts.isJsxText(child)) return !child.containsOnlyTriviaWhiteSpaces;
return true;
}
/** True when the child emits a value (text that trims to nothing and empty
* expression containers emit none). */
function jsxChildEmits(child: ts.JsxChild): boolean {
if (ts.isJsxText(child)) return jsxTextValue(child.text) !== undefined;
if (ts.isJsxExpression(child)) return child.expression !== undefined;
return true;
}
/** A `key` attribute after a spread that is not an object literal free of
* spreads: TypeScript emits createElement for such an element. */
function hasKeyAfterPropsSpread(attributes: ts.JsxAttributes): boolean {
let spread = false;
for (const property of attributes.properties.nodes) {
if (
ts.isJsxSpreadAttribute(property) &&
(!ts.isObjectLiteralExpression(property.expression) ||
property.expression.properties.nodes.some((p: ts.ObjectLiteralElementLike) =>
ts.isSpreadAssignment(p),
))
) {
spread = true;
} else if (
spread &&
ts.isJsxAttribute(property) &&
ts.isIdentifier(property.name) &&
property.name.text === "key"
) {
return true;
}
}
return false;
}
/** The 1-based line and column of `pos`, counting the line breaks
* TypeScript counts (\r\n, \n, \r, U+2028, U+2029) and UTF-16 units. */
function lineAndColumn(text: string, pos: number): { line: number; column: number } {
let line = 1;
let start = 0;
for (let i = 0; i < pos; i++) {
const ch = text.charCodeAt(i);
if (ch === 13 && text.charCodeAt(i + 1) === 10) continue;
if (isLineBreak(ch)) {
line++;
start = i + 1;
}
}
return { line, column: pos - start + 1 };
}
/** react-jsxdev's `self` argument. TypeScript passes `this`; where `this`
* has no declared type (the top level of an ES module and a function that is
* not a method and declares no `this` parameter) the argument is the
* `undefined` such code observes when called without a receiver, since an
* untyped `this` does not type-check under noImplicitThis. */
function jsxSelf(node: ts.Node): string {
for (let at = node.parent; at !== undefined; at = at.parent) {
if (ts.isArrowFunction(at)) continue;
if (
ts.isMethodDeclaration(at) ||
ts.isConstructorDeclaration(at) ||
ts.isGetAccessorDeclaration(at) ||
ts.isSetAccessorDeclaration(at) ||
ts.isPropertyDeclaration(at) ||
ts.isClassStaticBlockDeclaration(at)
)
return "this";
if (ts.isFunctionDeclaration(at) || ts.isFunctionExpression(at)) {
const declaresThis = at.parameters.nodes.some(
(parameter: ts.ParameterDeclaration) =>
ts.isIdentifier(parameter.name) && parameter.name.text === "this",
);
const objectMethod =
ts.isFunctionExpression(at) &&
at.parent !== undefined &&
ts.isPropertyAssignment(at.parent);
return declaresThis || objectMethod ? "this" : "undefined";
}
if (ts.isSourceFile(at)) return ts.isExternalModule(at) ? "undefined" : "this";
}
return "undefined";
}
/** TypeScript's intrinsic-element test: a lowercase first letter or a dash. */
function isIntrinsicJsxName(name: string): boolean {
const ch = name.charCodeAt(0);
return (ch >= 97 && ch <= 122) || name.includes("-");
}
function attributeName(name: ts.JsxAttributeName): string {
if (ts.isJsxNamespacedName(name))
return JSON.stringify(`${name.namespace.text}:${name.name.text}`);
return /^[A-Z_]\w*$/i.test(name.text) ? name.text : JSON.stringify(name.text);
}
function hasProtoProperty(object: ts.ObjectLiteralExpression): boolean {
return object.properties.nodes.some(
(property: ts.ObjectLiteralElementLike) =>
ts.isPropertyAssignment(property) &&
((ts.isIdentifier(property.name) && property.name.text === "__proto__") ||
(ts.isStringLiteral(property.name) && property.name.text === "__proto__")),
);
}
/** A JSX text child's string value: each line trimmed (the first line keeps
* its leading and the last line its trailing white space), lines without
* text dropped, the rest joined with one space and entities decoded.
* Undefined when nothing remains. */
export function jsxTextValue(text: string): string | undefined {
let value: string | undefined;
let firstNonWhitespace = 0;
let lastNonWhitespace = -1;
const add = (line: string): void => {
const decoded = decodeEntities(line);
value = value === undefined ? decoded : `${value} ${decoded}`;
};
for (let i = 0; i < text.length; i++) {
const ch = text.charCodeAt(i);
if (isLineBreak(ch)) {
if (firstNonWhitespace !== -1 && lastNonWhitespace !== -1)
add(text.slice(firstNonWhitespace, lastNonWhitespace + 1));
firstNonWhitespace = -1;
} else if (!isWhiteSpaceSingleLine(ch)) {
lastNonWhitespace = i;
if (firstNonWhitespace === -1) firstNonWhitespace = i;
}
}
if (firstNonWhitespace !== -1) add(text.slice(firstNonWhitespace));
return value;
}
/** Decodes `&name;`, `&#ddd;` and `&#xhh;` references. Unknown names and
* numbers past U+10FFFF stay as written. */
export function decodeEntities(text: string): string {
return text.replace(
/&((#((\d+)|x([\da-fA-F]+)))|(\w+));/g,
(
match: string,
_all: string | undefined,
_number: string | undefined,
_digits: string | undefined,
decimal: string | undefined,
hex: string | undefined,
word: string | undefined,
) => {
const code = decimal
? parseInt(decimal, 10)
: hex
? parseInt(hex, 16)
: JSX_ENTITIES.get(word!);
return code === undefined || code > 0x10ffff ? match : String.fromCodePoint(code);
},
);
}
@@ -696,6 +696,8 @@ export function lowerCryptoModuleCall(
loc,
};
}
const pki = lowerPkiCall(lowerer, expr, bi.member, args, loc);
if (pki) return pki;
if (bi.member === "createSecretKey") {
if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement))
lowerer.noLowering(
@@ -1339,3 +1341,55 @@ export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression):
}
return false;
}
/** The asymmetric-key and signature members (scr_pki.c): checked-dynamic
* arguments, and the result converted to the call site's type
* (KeyObject, Sign and Verify are checked-dynamic handles; key pairs,
* signatures and verdicts convert to their records, Buffers and
* booleans). Null for other members. */
function lowerPkiCall(
lowerer: Lowerer,
expr: ts.CallExpression,
member: string,
args: readonly ts.Expression[],
loc: SrcLoc,
): IrExpr | null {
const forms: Readonly<Record<string, readonly [IrLibFn, number, number, string]>> = {
createPrivateKey: ["crypto.createPrivateKey", 1, 1, "createPrivateKey(key)"],
createPublicKey: ["crypto.createPublicKey", 1, 1, "createPublicKey(key)"],
generateKeyPairSync: ["crypto.generateKeyPairSync", 1, 2, "generateKeyPairSync(type, options)"],
generateKeyPair: [
"crypto.generateKeyPairCb",
2,
3,
"generateKeyPair(type, [options,] callback)",
],
sign: ["crypto.signOneshot", 3, 3, "sign(algorithm, data, key)"],
verify: ["crypto.verifyOneshot", 4, 4, "verify(algorithm, data, key, signature)"],
createSign: ["crypto.createSign", 1, 1, "createSign(algorithm)"],
createVerify: ["crypto.createVerify", 1, 1, "createVerify(algorithm)"],
};
const form = forms[member];
if (!form) return null;
const [fn, min, max, supported] = form;
if (args.length < min || args.length > max)
lowerer.noLowering(
`crypto.${member} with ${args.length} arguments`,
expr,
`${supported} is supported`,
);
// generateKeyPair(type, callback): Node reads a function in the options
// position as the callback.
const shifted = fn === "crypto.generateKeyPairCb" && args.length === 2;
const values: IrExpr[] = [];
for (let i = 0; i < max; i++) {
const node = shifted ? (i === 1 ? undefined : i === 2 ? args[1] : args[i]) : args[i];
values.push(dynArg(lowerer, node, loc));
}
if (fn === "crypto.generateKeyPairCb")
return { kind: "libCall", fn, args: values, type: VOID, loc };
const call: IrExpr = { kind: "libCall", fn, args: values, type: DYN, loc };
const result = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (result === null || result.kind === "dyn") return call;
return lowerer.coerceInto(expr, call, result);
}
@@ -4,10 +4,11 @@ import { locOf } from "../../program.js";
import { BOOL, DYN, type IrExpr } from "../../../ir/ir.js";
import { builtinImportOf } from "./module-bindings.js";
/* node:crypto's secret KeyObject is a native checked-dynamic handle
* (scr_lib.c): the KeyObject type maps to dyn, its getters and methods
* dispatch through the handle, and these helpers lower the class's uses
* that a dyn value cannot answer by itself. */
/* node:crypto's KeyObject is a native checked-dynamic handle (secret keys
* in scr_lib.c, private and public keys in scr_pki.c), as are Sign and
* Verify: the types map to dyn, their getters and methods dispatch through
* the handle, and these helpers lower the uses that a dyn value cannot
* answer by itself. */
/** Whether `node` names node:crypto's KeyObject class. */
export function isKeyObjectClass(lowerer: Lowerer, node: ts.Expression): boolean {
@@ -37,15 +38,31 @@ export function lowerKeyObjectInstanceof(
}
const KEY_OBJECT_METHODS = new Set(["export", "equals", "toCryptoKey"]);
const SIGNER_METHODS = new Set(["update", "sign", "verify"]);
/** `key.export(...)`, `key.equals(...)` and `key.toCryptoKey(...)`: the
/** Whether a value's checker type is node:crypto's Sign or Verify. */
function isSignerValue(lowerer: Lowerer, node: ts.Expression): boolean {
const type = lowerer.typeOf(node);
const sym = type.getAliasSymbol() ?? type.getSymbol();
return (
!!sym &&
(ts.symbolName(sym) === "Sign" || ts.symbolName(sym) === "Verify") &&
lowerer.isStdlibSymbol(sym)
);
}
/** `key.export(...)`, `key.equals(...)` and `key.toCryptoKey(...)`, and
* the update/sign/verify methods of Sign and Verify (scr_pki.c): the
* handle answers them as methods, so they dispatch through dynInvoke
* rather than as a member read and a call. */
export function lowerKeyObjectCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null;
const access = call.expression;
if (access.questionDotToken || !KEY_OBJECT_METHODS.has(access.name.text)) return null;
if (!isKeyObjectValue(lowerer, access.expression)) return null;
if (access.questionDotToken) return null;
const method = access.name.text;
const keyMethod = KEY_OBJECT_METHODS.has(method) && isKeyObjectValue(lowerer, access.expression);
if (!keyMethod && !(SIGNER_METHODS.has(method) && isSignerValue(lowerer, access.expression)))
return null;
if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
return {
kind: "dynInvoke",
@@ -7,8 +7,9 @@ import { stdlibGlobalNameOf } from "../surfaces.js";
import { WEBCRYPTO_DYN_TYPES } from "../../type-mapper.js";
import { isWebCryptoReceiver } from "./crypto.js";
/* WebCrypto's secret-key slice (scr_webcrypto.c, with encrypt and
* decrypt in scr_webcrypto_aes.c): `crypto.subtle`'s `importKey`,
/* WebCrypto (scr_webcrypto.c, with encrypt and decrypt in
* scr_webcrypto_aes.c and the ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS
* algorithms in scr_webcrypto_pki.c): `crypto.subtle`'s `importKey`,
* `generateKey`, `exportKey`, `encrypt`, `decrypt`, `wrapKey`,
* `unwrapKey`, `deriveBits`, `deriveKey`, `sign`, `verify` and `digest`
* called on the crypto global (also through
@@ -20,8 +21,8 @@ import { isWebCryptoReceiver } from "./crypto.js";
* are handle reads. Other SubtleCrypto methods are refused by name. */
const SUBTLE_LOWERED: ReadonlyMap<string, IrLibFn> = new Map([
["importKey", "webcrypto.importKey"],
["generateKey", "webcrypto.generateKey"],
["importKey", "webcrypto.importKey"],
["exportKey", "webcrypto.exportKey"],
["encrypt", "webcrypto.encrypt"],
["decrypt", "webcrypto.decrypt"],
@@ -35,7 +36,7 @@ const SUBTLE_LOWERED: ReadonlyMap<string, IrLibFn> = new Map([
]);
const SUBTLE_HINT =
"the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, wrapKey and unwrapKey with AES-KW or those ciphers, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle";
"the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle";
/** Whether `node` is a SubtleCrypto object: `subtle` on a WebCrypto
* Crypto object (the global, globalThis.crypto or node:crypto's
@@ -101,6 +101,8 @@ export class ClassProtocols {
const sites: {
source: ts.Expression;
target: () => ts.Type | undefined;
/** The node whose contextual type `target` answers. */
contextual?: ts.Expression;
assertion?: true;
}[] = [];
/** Expressions whose values are observed as whole objects. */
@@ -122,7 +124,8 @@ export class ClassProtocols {
const callbackParams: { fn: ts.SignatureDeclaration; index: number; name: ts.Node }[] = [];
const contextual = (expr: ts.Expression) => () => this.checker.getContextualType(expr);
const flow = (expr: ts.Expression | undefined): void => {
if (expr !== undefined) sites.push({ source: expr, target: contextual(expr) });
if (expr !== undefined)
sites.push({ source: expr, target: contextual(expr), contextual: expr });
};
const write = (target: ts.Expression): void => {
while (ts.isParenthesizedExpression(target) || ts.isNonNullExpression(target))
@@ -329,7 +332,11 @@ export class ClassProtocols {
facts.loose.push({ node: looseArguments[i]!, call: looseCalls[i]!, symbols });
}
this.facts = facts;
const pending: { sourceType: ts.Type; target: () => ts.Type | undefined }[] = [];
const pending: {
sourceType: ts.Type;
target: () => ts.Type | undefined;
contextual?: ts.Expression;
}[] = [];
/** Views a value of unknown origin is asserted to and that expose a
* method of a class the view accepts (see methodViewSources). */
const methodViews = new Set<ts.Symbol>();
@@ -352,7 +359,11 @@ export class ClassProtocols {
}
continue;
}
pending.push({ sourceType, target: site.target });
pending.push(
site.contextual === undefined
? { sourceType, target: site.target }
: { sourceType, target: site.target, contextual: site.contextual },
);
}
for (const { fn, index, name } of callbackParams) {
attempt(() => {
@@ -377,6 +388,19 @@ export class ClassProtocols {
// flows and origins are complete.
const before = new Set(marked);
const targets = new Map<number, ts.Type | undefined>();
// The fixpoint asks the target of each site whose source carries
// identity: one batch asks those of the sites that carry it now, and a
// site that starts to as marks grow asks its own.
attempt(() =>
this.checker.prefetchContextualTypes(
pending.flatMap((entry) =>
entry.contextual !== undefined &&
attempt(() => this.carriesIdentity(entry.sourceType, marked)) === true
? [entry.contextual]
: [],
),
),
);
let changed = true;
while (changed) {
changed = false;
@@ -985,11 +985,41 @@ export function runtimeOptionalReceiverRead(
loc,
};
}
// An array hole in a slot with more than two arms fails the member
// access with Node's TypeError before the checked extraction runs.
const holeGuarded = (extracted: IrExpr): IrExpr => {
if (local.type.kind !== "union") return extracted;
const holeTag = lowerer.armTag(local.type.unionId, UNDEFINED_T);
if (holeTag < 0) return extracted;
return {
kind: "ternary",
cond: {
kind: "unionIsTag",
unionId: local.type.unionId,
tag: holeTag,
negated: false,
value: varRef(local.id, local.type, loc),
type: BOOL,
loc,
},
then: nodeThrowExpr(
1,
"",
runtimeOptionalReceiverMessage(expr, property),
extracted.type,
loc,
),
else_: extracted,
type: extracted.type,
loc,
};
};
if (narrowed.kind === "object") {
const view = checkedClassAssertion(lowerer, varRef(local.id, local.type, loc), narrowed, loc);
// Preserve the member-specific missing-value error on the two-arm
// path below; mixed storage also needs to validate its concrete class.
if (view && (lowerer.unions.get(local.type.unionId)?.arms.length ?? 0) > 2) return view;
if (view && (lowerer.unions.get(local.type.unionId)?.arms.length ?? 0) > 2)
return holeGuarded(view);
}
if (narrowed.kind === "record" && lowerer.armTag(local.type.unionId, narrowed) < 0) {
const hosted = hostedStorageView(lowerer, varRef(local.id, local.type, loc), narrowed, loc);
@@ -1014,7 +1044,9 @@ export function runtimeOptionalReceiverRead(
"a direct read from a runtime-optional capture with multiple value arms",
);
}
return lowerer.checkedNarrowCall(helper, varRef(local.id, local.type, loc), narrowed, loc);
return holeGuarded(
lowerer.checkedNarrowCall(helper, varRef(local.id, local.type, loc), narrowed, loc),
);
}
// A predicate can strengthen a record's fields or narrow a base class
// to a subclass without changing the slot's stored value arm. Extract
@@ -1029,10 +1061,7 @@ export function runtimeOptionalReceiverRead(
"a runtime-optional receiver without a representable value arm",
);
}
const message =
property === null
? `${expr.text} is not a function`
: `Cannot read properties of undefined (reading '${property}')`;
const message = runtimeOptionalReceiverMessage(expr, property);
return lowerer.maybeNarrow(
{
kind: "ternary",
@@ -1061,6 +1090,14 @@ export function runtimeOptionalReceiverRead(
);
}
/** Node's TypeError message for a member read (`property`) or a call
* (`property` null) through an undefined runtime-optional receiver. */
function runtimeOptionalReceiverMessage(expr: ts.Identifier, property: string | null): string {
return property === null
? `${expr.text} is not a function`
: `Cannot read properties of undefined (reading '${property}')`;
}
export function runtimeOptionalTrueIds(lowerer: Lowerer, node: ts.Expression): IrLocal[] {
return runtimeOptionalGuardIds(lowerer, node, true);
}
@@ -36,6 +36,7 @@ import { tryLowerExpression } from "./try-lower-expression.js";
import { fenceSymbolFieldCopy } from "../symbol-fields.js";
import { planUnionRetag } from "../union-retag.js";
import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js";
import { builtinGlobalValue } from "./property-access.js";
/** `{ a: 1, b: "x" }` → recordLit. The record type comes from the
* contextual type when tsc has one (annotated declarations, arguments,
@@ -394,9 +395,11 @@ export function lowerDynObjectLiteral(
const lowerValue = (): IrExpr =>
ts.isMethodDeclaration(prop)
? lowerer.lowerLambda(prop, true)
: !boxValue && ts.isObjectLiteralExpression(valueExpr)
? lowerer.lowerExprExpecting(valueExpr, DYN)
: lowerer.lowerExpr(valueExpr as ts.Expression);
: ts.isShorthandPropertyAssignment(prop) && shorthandBuiltinValue(lowerer, prop)
? shorthandBuiltinValue(lowerer, prop)!
: !boxValue && ts.isObjectLiteralExpression(valueExpr)
? lowerer.lowerExprExpecting(valueExpr, DYN)
: lowerer.lowerExpr(valueExpr as ts.Expression);
raw = fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ?? lowerValue();
} catch (err) {
// A PURE member read a JS file cannot lower (a namespace object
@@ -3157,6 +3160,20 @@ function lowerDeclaredSpreadMerge(
/** The value of a shorthand property (`{ x }`): the binding `x` refers to.
* The property name's own symbol is the PROPERTY, so resolution goes
* through getShorthandAssignmentValueSymbol. */
/** `{ TypeError }`: a stdlib global in shorthand position reads as the value
* its name reads as elsewhere (the interned constructor or function value),
* or null when the global has no such value. The shorthand's name carries
* the property's symbol, so the identifier path cannot resolve it. */
function shorthandBuiltinValue(
lowerer: Lowerer,
prop: ts.ShorthandPropertyAssignment,
): IrExpr | null {
const symbol = lowerer.checker.getShorthandAssignmentValueSymbol(prop);
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
const js = isJsSourceFile(ts.getSourceFileOfNode(prop));
return builtinGlobalValue(ts.symbolName(symbol), locOf(prop.name), js);
}
export function lowerShorthandValue(
lowerer: Lowerer,
prop: ts.ShorthandPropertyAssignment,
@@ -3173,6 +3190,8 @@ export function lowerShorthandValue(
const callable = lowerer.lowerBuiltinCallableValue(builtin, loc);
if (callable) return callable;
}
const builtinValue = shorthandBuiltinValue(lowerer, prop);
if (builtinValue) return builtinValue;
if (symbol) {
if (lowerer.ctx.selfSymbol === symbol) {
return { kind: "selfRef", type: lowerer.ctx.selfType!, loc };
@@ -4090,10 +4090,26 @@ function lowerStreamObjectProperty(
return lowerStreamProperty(lowerer, expr, info);
}
/** The error constructors among GLOBAL_CONSTRUCTOR_VALUES. Their statics
* (`Error.stackTraceLimit`, `Error.captureStackTrace`) keep their own
* lowerings, so fenceBuiltinConstructorStaticRead leaves them alone. */
const ERROR_CONSTRUCTOR_VALUES: ReadonlySet<string> = new Set([
"Error",
"TypeError",
"RangeError",
"SyntaxError",
"ReferenceError",
"EvalError",
"URIError",
"AggregateError",
]);
/** The builtin constructors JavaScript sources read as real function
* values rather than identity tokens: the runtime interns one function per
* constructor, and dynamic values answer `.constructor` with the same one,
* so `o.constructor === Object` is an exact identity test. */
* so `o.constructor === Object` and `err.constructor === TypeError` are
* exact identity tests, and `err.constructor.name` is the constructor's
* name. */
const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet<string> = new Set([
"Object",
"Function",
@@ -4104,6 +4120,7 @@ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet<string> = new Set([
"Promise",
"WeakMap",
"WeakSet",
...ERROR_CONSTRUCTOR_VALUES,
]);
/** The builtin global functions JavaScript sources read as interned
@@ -4154,8 +4171,18 @@ export function builtinGlobalValue(canonical: string, loc: SrcLoc, js = false):
const BUILTIN_VALUE_MEMBERS: ReadonlySet<string> = new Set(["name", "length", "constructor"]);
/** The Object statics whose answers for an interned builtin function value
* match Node's (identity, and the empty enumerable own-key lists). */
const BUILTIN_VALUE_REFLECTION: ReadonlySet<string> = new Set(["is", "keys", "values", "entries"]);
* match Node's (identity, the empty enumerable own-key lists, and
* extensibility: freezing, sealing and preventExtensions are fenced, so the
* values stay extensible). */
const BUILTIN_VALUE_REFLECTION: ReadonlySet<string> = new Set([
"is",
"keys",
"values",
"entries",
"isExtensible",
"isFrozen",
"isSealed",
]);
/** The value `builtinGlobalValue` gives the stdlib global `expr` read at its
* use site: null when the use reads the global's members (they keep their
@@ -4246,6 +4273,7 @@ function fenceBuiltinConstructorStaticRead(
symbol !== undefined &&
ts.symbolName(symbol).endsWith("Constructor") &&
GLOBAL_CONSTRUCTOR_VALUES.has(ts.symbolName(symbol).slice(0, -"Constructor".length)) &&
!ERROR_CONSTRUCTOR_VALUES.has(ts.symbolName(symbol).slice(0, -"Constructor".length)) &&
lowerer.isStdlibSymbol(symbol)
)
lowerer.stdlibMemberFence(access);
@@ -5,15 +5,20 @@ import * as ts from "../ts7/adapter.js";
import { bodyReadsArguments } from "../arguments-usage.js";
import type { Lowerer } from "./lowerer.js";
import {
BOOL,
DYN,
type IrExpr,
type IrFunction,
type IrParam,
type IrStmt,
type IrType,
type SrcLoc,
STRING,
VOID,
isUnitType,
typeEquals,
} from "../../ir/ir.js";
import { helperLoc, nodeThrowExpr } from "../../ir/build.js";
import { isJsSourceFile, locOf } from "../program.js";
import { typeKey } from "../type-mapper.js";
import { functionCanReturnUndefined } from "../function-completion.js";
@@ -24,7 +29,7 @@ import { nsPathPrefix } from "./lower-namespaces.js";
import { declSymbolOf } from "./lower-modules.js";
import { npmStaticPackageOfPath } from "../npm-static.js";
import { rejectStaticThis } from "./static-this.js";
import { jsBindingHasOpenWrites } from "./lower-stmts.js";
import { isVarDeclared, jsBindingHasOpenWrites } from "./lower-stmts.js";
import {
type ParamShape,
paramShapes,
@@ -93,6 +98,17 @@ export interface GenericFnInfo {
* declaration makes a new function object in JavaScript, while the
* compiled instances are shared module functions, so only calls lower. */
functionScoped?: true;
/** The declaration of a generic arrow/function-expression binding. */
bindingDecl?: ts.VariableDeclaration;
/** A module-scope binding that code can use before its declaration
* runs: the BOOL global its declaration sets, and whether it is a
* `let`/`const` (a temporal dead zone) or a `var` (undefined). */
presence?: { flagId: string; lexical: boolean };
/** genericValueForwarder's lifted functions, by instance name. */
valueForwarders?: Map<string, string>;
/** runsAfterDeclaration's answers for the function declarations whose
* calls it placed. */
ordering?: Map<ts.Node, boolean>;
/** IMPLICIT-ANY monomorphization (npm-static JS): parallel to
* decl.parameters — the param's own symbol when the slot is a BINDABLE
* implicit-any parameter (untyped, identifier-named, never written in
@@ -266,7 +282,14 @@ export function lowerGenericCall(
? implicitCallInstance(lowerer, expr, info)
: genericCallInstance(lowerer, expr, info);
const args = lowerer.completeArgs(expr.arguments.nodes, instance.params, loc, expr);
return { kind: "call", callee: instance.name, args, type: instance.returnType, loc };
const call: IrExpr & { kind: "call" } = {
kind: "call",
callee: instance.name,
args,
type: instance.returnType,
loc,
};
return guardEarlyBindingCall(lowerer, info, expr, call);
}
/** The instance a CALL of a generic function-like names: resolved
@@ -1361,6 +1384,110 @@ export function lowerGenericFnValue(
}
}
if (!target) fenceUnpinned();
return guardEarlyBindingValue(
lowerer,
info,
ref,
pinnedGenericClosure(lowerer, ref, info, target, fenceUnpinned),
);
}
/** The value a generic binding declared inside a function stores when a
* nested function read it before the declaration lowered (`const read =
* () => id; const id: (x: number) => number = <T>(x: T): T => x`): such
* a read resolves to an ordinary TDZ local, so the declaration assigns it
* the instance its concrete declared signature pins. Each evaluation of
* the declaration makes a new function object, as in JavaScript: the
* value is a closure over a forwarder to the instance, not the instance's
* interned closure. */
export function genericBindingStoredValue(
lowerer: Lowerer,
decl: ts.VariableDeclaration,
info: GenericFnInfo,
): IrExpr {
const name = decl.name as ts.Identifier;
const fenceUnpinned = (): never =>
lowerer.unsupported(
"SC1090",
name,
`generic function bindings read before their declaration without a concrete declared signature (annotate '${info.baseName}' with one, or declare it before its uses)`,
);
const sigs = lowerer.checker.getCallSignatures(lowerer.typeOf(name));
if (sigs.length !== 1) fenceUnpinned();
const pinned = pinnedGenericClosure(lowerer, name, info, sigs[0]!, fenceUnpinned);
const async = ts.getModifiers(info.decl)?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword);
if (pinned.kind !== "closure" || pinned.type.kind !== "func" || pinned.type.rest || async)
return lowerer.unsupported(
"SC1090",
name,
`async or rest-parameter generic function bindings read before their declaration (each evaluation of '${info.baseName}' makes a new function object)`,
);
return {
...pinned,
fnName: genericValueForwarder(lowerer, info, pinned.fnName, pinned.type, pinned.loc),
};
}
/** A lifted function that calls the instance `instance` (of type `type`)
* with its own arguments. Lifted functions get a new closure object per
* evaluation; module functions, which instances are, get one interned
* closure. */
function genericValueForwarder(
lowerer: Lowerer,
info: GenericFnInfo,
instance: string,
type: IrType & { kind: "func" },
at: SrcLoc,
): string {
info.valueForwarders ??= new Map();
const existing = info.valueForwarders.get(instance);
if (existing !== undefined) return existing;
const name = `${instance}%value`;
const loc = helperLoc(at);
const params: IrParam[] = type.params.map((paramType, i) => ({
localId: `%arg${i}.0`,
name: `%arg${i}`,
type: paramType,
}));
const call: IrExpr = {
kind: "call",
callee: instance,
args: params.map((p): IrExpr => ({ kind: "varRef", localId: p.localId, type: p.type, loc })),
type: type.ret,
loc,
};
lowerer.liftedFns.push({
name,
params,
returnType: type.ret,
locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
captures: [],
...(ts.isFunctionExpression(info.decl) ? { ownsPrototype: true } : {}),
body:
type.ret.kind === "void"
? [
{ kind: "exprStmt", expr: call, loc },
{ kind: "return", value: null, loc },
]
: [{ kind: "return", value: call, loc }],
loc,
});
lowerer.noteEdge(name);
info.valueForwarders.set(instance, name);
return name;
}
/** The closure of the instance of `info` that `target` pins, for a value
* read at `ref`. `fenceUnpinned` reports a signature that leaves a type
* parameter unbound. */
function pinnedGenericClosure(
lowerer: Lowerer,
ref: ts.Expression,
info: GenericFnInfo,
target: ts.Signature,
fenceUnpinned: () => never,
): IrExpr {
const loc = locOf(ref);
const bindings = new Map<ts.Symbol, IrType>();
unifySignatureBindings(lowerer, info, target, bindings);
const hofBinding = runtimeOptionalHofGenericBinding(lowerer, ref, info);
@@ -2693,9 +2820,18 @@ export function bindingGenericFnInfoOf(
typeParams,
instances: new Map(),
...(functionScoped ? { functionScoped: true as const } : {}),
bindingDecl: decl,
};
lowerer.bindingGenericFns.set(fnNode, info);
lowerer.genericFnsBySymbol.set(sym, info);
// Module-scope uses resolve to the instances from the start, so those
// that can run first need the flag. Inside a function the declaration
// registers before any later use lowers, and earlier uses read a TDZ
// box (genericBindingStoredValue).
if (!functionScoped) {
const presence = bindingPresenceOf(lowerer, info, decl, sym);
if (presence !== undefined) info.presence = presence;
}
if (ts.isFunctionExpression(fnNode) && fnNode.name !== undefined) {
const inner = lowerer.checker.getSymbolAtLocation(fnNode.name);
if (inner) lowerer.genericFnsBySymbol.set(inner, info);
@@ -2703,6 +2839,261 @@ export function bindingGenericFnInfoOf(
return info;
}
/** Whether `use`, a reference to the binding `info` declares, can only
* run after the declaration has run: it follows the declaration in its
* statement list (closures count where they are created), sits inside the
* initializer itself, or is inside a function declaration whose every
* reference does. References from other modules run after this module's
* body, and export specifiers only export. A use the check cannot place
* answers false. */
export function runsAfterDeclaration(lowerer: Lowerer, info: GenericFnInfo, use: ts.Node): boolean {
const decl = info.bindingDecl;
if (decl === undefined) return true;
info.ordering ??= new Map();
return useRunsAfter(lowerer, decl, use, info.ordering, new Set());
}
function useRunsAfter(
lowerer: Lowerer,
decl: ts.VariableDeclaration,
use: ts.Node,
memo: Map<ts.Node, boolean>,
inProgress: Set<ts.Node>,
): boolean {
if (ts.getSourceFileOfNode(use) !== ts.getSourceFileOfNode(decl)) return true;
const init = decl.initializer;
if (init !== undefined && use.pos >= init.pos && use.end <= init.end) return true;
const stmt = decl.parent?.parent;
const list = stmt?.parent;
if (stmt === undefined || list === undefined) return false;
for (let n: ts.Node = use; n.parent !== undefined; n = n.parent) {
if (n.parent === list) return n === stmt ? use.pos >= decl.end : n.pos >= stmt.end;
if (ts.isFunctionDeclaration(n)) return functionRunsAfter(lowerer, decl, n, memo, inProgress);
}
return false;
}
/** Whether every call of the function declaration `fn` runs after `decl`
* has run. A function in progress counts as running after: its calls of
* itself, directly or through other functions, add no earlier caller.
* Only answers that depend on no function in progress are memoized. */
function functionRunsAfter(
lowerer: Lowerer,
decl: ts.VariableDeclaration,
fn: ts.FunctionDeclaration,
memo: Map<ts.Node, boolean>,
inProgress: Set<ts.Node>,
): boolean {
const known = memo.get(fn);
if (known !== undefined) return known;
if (inProgress.has(fn)) return true;
// `export default function () {}`: only importers call it, after this
// module's body has run.
if (fn.name === undefined) return true;
inProgress.add(fn);
let result = true;
try {
const symbol = lowerer.checker.getSymbolAtLocation(fn.name);
if (symbol === undefined) result = false;
else {
for (const ref of bindingReferences(lowerer, ts.getSourceFileOfNode(fn), symbol, fn.name)) {
if (ref.pos >= fn.pos && ref.end <= fn.end) continue;
if (!useRunsAfter(lowerer, decl, ref, memo, inProgress)) {
result = false;
break;
}
}
}
} finally {
inProgress.delete(fn);
}
if (inProgress.size === 0) memo.set(fn, result);
return result;
}
/** The value references of `symbol` in `file` other than `name` (its
* declaration name): identifiers spelled like it that resolve to it,
* outside type positions and export specifiers. */
function bindingReferences(
lowerer: Lowerer,
file: ts.SourceFile,
symbol: ts.Symbol,
name: ts.Identifier,
): ts.Identifier[] {
let byName = lowerer.bindingIdentifiers.get(file);
if (byName === undefined) {
byName = new Map();
lowerer.bindingIdentifiers.set(file, byName);
}
let idents = byName.get(name.text);
if (idents === undefined) {
const found: ts.Identifier[] = [];
ts.walkPreorder(file, (node) => {
if (ts.isIdentifier(node) && node.text === name.text) found.push(node);
return undefined;
});
lowerer.checker.prefetchSymbolNodesExact(found);
byName.set(name.text, found);
idents = found;
}
return idents.filter((ident) => {
if (ident === name) return false;
const parent = ident.parent;
if (parent === undefined || ts.isExportSpecifier(parent) || ts.isExportAssignment(parent))
return false;
let resolved = lowerer.checker.getSymbolAtLocation(ident);
if (ts.isShorthandPropertyAssignment(parent) && parent.name === ident)
resolved = lowerer.checker.getShorthandAssignmentValueSymbol(parent) ?? resolved;
if (resolved !== symbol) return false;
for (let p: ts.Node | undefined = parent; p !== undefined; p = p.parent) {
if (ts.isTypeNode(p) && p.kind !== ts.SyntaxKind.ExpressionWithTypeArguments) return false;
if (ts.isStatement(p) || ts.isFunctionLike(p)) break;
}
return true;
});
}
/** The presence flag of a module-scope generic binding that code can use
* before its declaration runs (a hoisted function or an earlier closure
* called during module evaluation), or undefined when every use runs
* after it. The declaration statement sets the flag. */
function bindingPresenceOf(
lowerer: Lowerer,
info: GenericFnInfo,
decl: ts.VariableDeclaration,
symbol: ts.Symbol,
): GenericFnInfo["presence"] {
const name = decl.name as ts.Identifier;
const early = bindingReferences(lowerer, ts.getSourceFileOfNode(decl), symbol, name).some(
(ref) => !runsAfterDeclaration(lowerer, info, ref),
);
if (!early) return undefined;
const flagId = `%g.${info.qualifiedName}%initialized`;
lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true });
return { flagId, lexical: !isVarDeclared(decl) };
}
/** The statement that records a generic binding's declaration as run,
* for a binding with a presence flag. */
export function genericBindingPresenceStore(
info: GenericFnInfo,
decl: ts.VariableDeclaration,
): IrStmt | null {
if (info.presence === undefined) return null;
const loc = locOf(decl);
return {
kind: "assign",
localId: info.presence.flagId,
value: { kind: "boolLit", value: true, type: BOOL, loc },
loc,
};
}
/** A call of a generic binding where the binding may not be initialized
* yet. A `let`/`const` binding in its temporal dead zone throws Node's
* ReferenceError before the arguments run; an unassigned `var` is
* undefined, so the arguments run and the call throws "is not a
* function". Calls that run after the declaration stay direct. */
function guardEarlyBindingCall(
lowerer: Lowerer,
info: GenericFnInfo,
expr: ts.CallExpression,
call: IrExpr & { kind: "call" },
): IrExpr {
if (info.bindingDecl === undefined || runsAfterDeclaration(lowerer, info, expr)) return call;
const loc = call.loc;
const presence = requirePresence(lowerer, info, expr.expression);
const flag: IrExpr = { kind: "varRef", localId: presence.flagId, type: BOOL, loc };
if (presence.lexical)
return {
kind: "seqExpr",
stmts: [tdzCheck(flag, info.baseName, loc)],
result: call,
type: call.type,
loc,
};
// Arguments first, in order, then the check: Node evaluates them
// before it finds the callee is not callable.
const stmts: IrStmt[] = [];
const args = call.args.map((arg): IrExpr => {
const id = `%earlyCallArg.${lowerer.ctx.locals.length}`;
lowerer.ctx.locals.push({ id, name: "%earlyCallArg", type: arg.type, mutable: false });
stmts.push({ kind: "varDecl", localId: id, init: arg, loc });
return { kind: "varRef", localId: id, type: arg.type, loc };
});
return {
kind: "seqExpr",
stmts,
result: {
kind: "ternary",
cond: flag,
then: { ...call, args },
else_: nodeThrowExpr(1, "", `${info.baseName} is not a function`, call.type, loc),
type: call.type,
loc,
},
type: call.type,
loc,
};
}
/** A generic binding read as a value where it may not be initialized
* yet: a `let`/`const` binding checks its temporal dead zone first. */
function guardEarlyBindingValue(
lowerer: Lowerer,
info: GenericFnInfo,
ref: ts.Expression,
value: IrExpr,
): IrExpr {
if (info.bindingDecl === undefined || runsAfterDeclaration(lowerer, info, ref)) return value;
const presence = requirePresence(lowerer, info, ref);
if (!presence.lexical)
lowerer.unsupported(
"SC1090",
ref,
`generic 'var' bindings read as values before their declaration runs (Node reads undefined there; declare '${info.baseName}' with const)`,
);
const loc = value.loc;
const flag: IrExpr = { kind: "varRef", localId: presence.flagId, type: BOOL, loc };
return {
kind: "seqExpr",
stmts: [tdzCheck(flag, info.baseName, loc)],
result: value,
type: value.type,
loc,
};
}
/** The presence flag a use that can run before the declaration checks.
* Only module-scope bindings have one: a binding inside a function would
* need a per-call flag that its closures share. */
function requirePresence(
lowerer: Lowerer,
info: GenericFnInfo,
blame: ts.Node,
): NonNullable<GenericFnInfo["presence"]> {
if (info.presence !== undefined) return info.presence;
return lowerer.unsupported(
"SC1090",
blame,
`generic arrow/function-expression bindings declared inside functions that code can call before their declaration runs (the binding '${info.baseName}' is a 'var' in a branch or block, or is called from a place that can run first — declare it with const before its uses)`,
);
}
function tdzCheck(flag: IrExpr, name: string, loc: SrcLoc): IrStmt {
return {
kind: "exprStmt",
expr: {
kind: "intrinsic",
name: "module.tdzCheck",
args: [flag, { kind: "strLit", value: name, type: STRING, loc }],
type: VOID,
loc,
},
loc,
};
}
/** `const h = id` — a binding ALIASING a generic function (a top-level
* declaration, a registered generic binding, or another alias — resolved
* left to right in declaration order). The alias registers the SAME info
@@ -428,7 +428,9 @@ function lowerNodeReadable(
},
{
kind: "if",
cond: { kind: "dynTest", test: "undefined", value: chunkRef, type: BOOL, loc },
// the end is the done marker: an object-mode stream can yield
// undefined
cond: { kind: "libCall", fn: "stream.chunkDone", args: [chunkRef], type: BOOL, loc },
then: [{ kind: "break", loc }],
else_: null,
loc,
@@ -7439,6 +7439,35 @@ function lowerObjectStaticCall(
if (call.questionDotToken || access.questionDotToken) return null;
if (!lowerer.isStdlibGlobal(access.expression, "Object")) return null;
const member = access.name.text;
// `Object.getOwnPropertyDescriptor(Error, "stackTraceLimit")` (Effect's
// isStackTraceLimitWritable): Error's own writable, enumerable,
// configurable data property holding the current limit. The interned
// Error value models no other statics (builtinGlobalValueAt).
const [first, second] = call.arguments.nodes;
if (
member === "getOwnPropertyDescriptor" &&
call.arguments.nodes.length === 2 &&
lowerer.isStdlibGlobal(first!, "Error") &&
ts.isStringLiteralLike(second!) &&
second.text === "stackTraceLimit"
) {
const loc = locOf(call);
const dyn = (value: IrExpr): IrExpr => ({ kind: "dynFrom", value, type: DYN, loc });
const yes = dyn({ kind: "boolLit", value: true, type: BOOL, loc });
const limit: IrExpr = { kind: "libCall", fn: "error.stackLimitGet", args: [], type: F64, loc };
return {
kind: "dynObjLit",
fields: [
{ key: { kind: "strLit", value: "value", type: STRING, loc }, value: dyn(limit) },
...["writable", "enumerable", "configurable"].map((key) => ({
key: { kind: "strLit" as const, value: key, type: STRING, loc },
value: yes,
})),
],
type: DYN,
loc,
};
}
if (
(member === "getOwnPropertyNames" ||
member === "getOwnPropertySymbols" ||
@@ -158,7 +158,7 @@ import {
classSymbolKeyOfSymbol,
symbolFieldDisplayName,
} from "./symbol-fields.js";
import { lowerHttpAgentNew, lowerHttpServerNew } from "./lower-server.js";
import { lowerHttpAgentNew, lowerHttpServerNew, lowerTlsSocketNew } from "./lower-server.js";
import {
ambientNsRootOf,
ambientUndefReadType,
@@ -9836,6 +9836,12 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
const httpServer = lowerHttpServerNew(lowerer, expr);
if (httpServer) return httpServer;
}
// `new tls.TLSSocket(socket, options)` — the server-mode wrap of an open
// socket (lower-server).
{
const tlsSocket = lowerTlsSocketNew(lowerer, expr);
if (tlsSocket) return tlsSocket;
}
// `new http.Agent(opts?)` / `new https.Agent(opts?)` — the Agent
// handle (lower-server): getName/destroy/counters through the dyn
// handle ops, requests thread it via the agent option.
@@ -1458,10 +1458,11 @@ export function lowerEmitterMethodCall(
const receiver = lowerReceiver();
const once = member === "once" || member === "prependOnceListener";
const prepend = member === "prependListener" || member === "prependOnceListener";
// Stream 'data' rides its own two-slot payload ABI (bytes + string —
// encoded streams deliver strings; scr_stream_emit_data): listeners
// on readable-sided receivers register through DATA thunks that
// unwrap the declared side (typed) or box by tag (dyn).
// Stream 'data' rides its own three-slot payload ABI (bytes, string —
// encoded streams deliver strings — and an object-mode value;
// scr_stream_emit_data): listeners on readable-sided receivers
// register through DATA thunks that unwrap the declared side, convert
// to the declared type, or take the value (dyn).
const sides = streamSidesOf(lowerer, info);
if (name === "data" && (sides === "r" || sides === "rw")) {
const dynFlavor = dynListenerFlavor(lowerer, args[1]!);
@@ -1500,16 +1501,25 @@ export function lowerEmitterMethodCall(
(lowerer.unions.get(p.unionId)?.arms ?? []).every(
(arm) => (arm.kind === "bytes" && arm.elem === "u8") || arm.kind === "string",
);
// Any other chunk type converts from the chunk's value (an
// object-mode stream's value, or the Buffer or string) when the
// listener runs, or throws a TypeError there.
if (
p !== undefined &&
!(p.kind === "bytes" && p.elem === "u8") &&
p.kind !== "string" &&
!chunkUnion
!chunkUnion &&
(p.kind === "func" ||
!canConvertToDyn(
p,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
))
) {
lowerer.noLowering(
`a 'data' listener whose chunk parameter is '${lowerer.fmt(p)}'`,
args[1]!,
"chunks are Buffers — or strings once setEncoding/the encoding option applies",
"declare the chunk as a type that converts from a checked-dynamic value",
);
}
if (!registering) {
@@ -1654,8 +1664,10 @@ export function lowerEmitterMethodCall(
loc,
);
}
// Stream 'data' rides the two-slot payload ABI: a user emit fills
// the chunk's slot (bytes or string), NULLs the other.
// Stream 'data' rides the three-slot payload ABI: a user emit fills
// the chunk's slot (bytes or string) and NULLs the others; any other
// value is emitted as a checked-dynamic value, filling the slots by
// its tag.
const emitSides = streamSidesOf(lowerer, info);
if (name === "data" && (emitSides === "r" || emitSides === "rw")) {
if (args.length !== 2) {
@@ -1665,16 +1677,27 @@ export function lowerEmitterMethodCall(
"the event carries one chunk (a Buffer or string)",
);
}
const chunk = lowerer.lowerExpr(args[1]!);
let chunk = lowerer.lowerExpr(args[1]!);
if (
!(chunk.type.kind === "bytes" && chunk.type.elem === "u8") &&
chunk.type.kind !== "string"
) {
lowerer.noLowering(
`emit('data') with a '${lowerer.fmt(chunk.type)}' chunk`,
args[1]!,
"chunks are Buffers or strings",
);
if (
chunk.type.kind === "func" ||
(chunk.type.kind !== "dyn" &&
!canConvertToDyn(
chunk.type,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
))
) {
lowerer.noLowering(
`emit('data') with a '${lowerer.fmt(chunk.type)}' chunk`,
args[1]!,
"the chunk is a value that converts to a checked-dynamic value",
);
}
chunk = lowerer.coerceToExpected(chunk, DYN);
}
return {
kind: "libCall",
@@ -2939,6 +2939,26 @@ function lowerNetSocketMethodCall(
loc,
};
}
if (event === "secure") {
// The handshake's completion on a TLS socket (after secureConnect on
// a client); plain sockets never fire it.
const { cb } = lowerCallbackArg(
lowerer,
args[1]!,
"secure listeners",
0,
() => false,
"use ()",
[],
);
return {
kind: "libCall",
fn: "tls.sockOnSecure",
args: [receiver, cb, once],
type: VOID,
loc,
};
}
if (event === "session") {
// The received-ticket event: fires once per TLS client socket with
// the serialized session (a Buffer); plain sockets never fire it.
@@ -2962,7 +2982,7 @@ function lowerNetSocketMethodCall(
lowerer.noLowering(
`socket.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
args[0]!,
'"data", "end", "close", "error", "connect", "lookup", "ready", "timeout", "readable", "secureConnect", and "session" are the supported socket events (as literals)',
'"data", "end", "close", "error", "connect", "lookup", "ready", "timeout", "readable", "secureConnect", "secure", and "session" are the supported socket events (as literals)',
);
}
lowerer.noLowering(
@@ -4676,6 +4696,125 @@ export function lowerHttpServerNew(lowerer: Lowerer, expr: ts.NewExpression): Ir
return lowerHttpCreateServerForms(lowerer, expr, "new http.Server", locOf(expr));
}
/** `new tls.TLSSocket(socket, options)` in server mode: a new TLS socket
* takes over the socket's connection and runs the server handshake
* (scr_tls_wrap_socket). The options record reads at runtime, the
* tls.connect precedent. A literal's secureContext member is a
* SecureContext handle, which the checked-dynamic record cannot hold, so
* it passes as its own argument; members after it must be free of side
* effects, because it is read after them. Node never starts the handshake
* of a client-mode TLSSocket, so a literal without isServer: true fences
* here. */
export function lowerTlsSocketNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
const callee = expr.expression;
const isTlsSocket =
(ts.isPropertyAccessExpression(callee) &&
callee.name.text === "TLSSocket" &&
lowerer.builtinMemberOf(callee)?.module === "tls") ||
(ts.isIdentifier(callee) &&
lowerer.builtinImportOf(callee)?.module === "tls" &&
lowerer.builtinImportOf(callee)?.member === "TLSSocket");
if (!isTlsSocket) return null;
const loc = locOf(expr);
const HINT =
"the supported forms are new tls.TLSSocket(socket, { isServer: true, cert, key }) and new tls.TLSSocket(socket, { isServer: true, secureContext })";
const args = expr.arguments?.nodes ?? [];
if (args.length !== 2 || args.some(ts.isSpreadElement)) {
lowerer.noLowering(`new tls.TLSSocket with ${args.length} argument(s)`, expr, HINT);
}
const socket = lowerer.lowerExpr(args[0]!);
if (socket.type.kind !== "netSocket") {
lowerer.noLowering(
`new tls.TLSSocket over a '${lowerer.fmt(socket.type)}' value`,
args[0]!,
"the wrapped value must be a net.Socket (a server connection or an http CONNECT socket)",
);
}
let optsNode: ts.Expression = args[1]!;
while (ts.isParenthesizedExpression(optsNode)) optsNode = optsNode.expression;
if (!ts.isObjectLiteralExpression(optsNode)) {
const o = lowerer.lowerExpr(optsNode);
const opts: IrExpr =
o.type.kind === "dyn"
? o
: canConvertToDyn(
o.type,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
)
? { kind: "dynFrom", value: o, type: DYN, loc }
: lowerer.noLowering(
`new tls.TLSSocket with a '${lowerer.fmt(o.type)}' options record`,
optsNode,
`${HINT} — a secureContext member lowers in an options object literal`,
);
return { kind: "libCall", fn: "tls.wrapSocket", args: [socket, opts], type: NETSOCKET_T, loc };
}
const props = optsNode.properties.nodes;
const nameOf = (p: ts.ObjectLiteralElementLike): string | null =>
(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))
? p.name.text
: null;
const isServer = props.find((p) => nameOf(p) === "isServer");
if (
isServer === undefined ||
(ts.isPropertyAssignment(isServer) && isServer.initializer.kind === ts.SyntaxKind.FalseKeyword)
) {
lowerer.noLowering(
"new tls.TLSSocket in client mode",
optsNode,
"Node starts no handshake for a client-mode TLSSocket — pass isServer: true for the server side; tls.connect makes client sockets",
);
}
const ctxIndex = props.findIndex((p) => nameOf(p) === "secureContext");
if (ctxIndex < 0) {
const opts = lowerDynObjectLiteral(lowerer, optsNode);
return { kind: "libCall", fn: "tls.wrapSocket", args: [socket, opts], type: NETSOCKET_T, loc };
}
const ctxProp = props[ctxIndex]!;
const ctxNode = ts.isShorthandPropertyAssignment(ctxProp)
? ctxProp.name
: (ctxProp as ts.PropertyAssignment).initializer;
for (const p of props.slice(ctxIndex + 1)) {
const pure =
ts.isShorthandPropertyAssignment(p) ||
(ts.isPropertyAssignment(p) && sideEffectFreeOptionValue(p.initializer));
if (!pure) {
lowerer.noLowering(
"a new tls.TLSSocket options literal with side effects after secureContext",
p,
"secureContext is read after the other members — move it to the end of the object",
);
}
}
let ctx: IrExpr | null = null;
const opts = lowerDynObjectLiteral(lowerer, optsNode, (node, value) => {
if (node !== ctxNode) return lowerer.coerceToExpected(value, DYN);
if (value.type.kind !== "secureCtx") {
lowerer.noLowering(
`a '${lowerer.fmt(value.type)}' secureContext`,
node,
"pass a tls.createSecureContext({ cert, key }) result",
);
}
ctx = value;
return {
kind: "dynFrom",
value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
type: DYN,
loc,
};
});
return {
kind: "libCall",
fn: "tls.wrapSocketCtx",
args: [socket, opts, ctx!],
type: NETSOCKET_T,
loc,
};
}
/** Module-function calls on http import bindings: createServer([options][,
* handler]) and the call-form Server (Node's constructor works without
* `new` — test/parallel's http.Server(fn) spelling). The handler takes
@@ -4985,14 +5124,14 @@ function lowerTlsServerOptionsOrDyn(
* connect(options[, cb]) and connect(port[, host][, options][, cb]) with
* a RUNTIME options record (the checked-dynamic JS lane's shape — a dyn
* value, or a record that converts): the runtime walk implements
* port/host/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout and throws the catchable
* port/host/socket/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout and throws the catchable
* fence for other documented members (the divergence-66 stance). The
* callback (and 'secureConnect'/'connect' listeners) fires
* post-handshake — Node's secureConnect timing. */
function lowerTlsConnectCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
const args = expr.arguments.nodes;
const FORMS_HINT =
"the supported forms are connect(options[, cb]) and connect(port[, host][, options][, cb]) — options implement port/host/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout";
"the supported forms are connect(options[, cb]) and connect(port[, host][, options][, cb]) — options implement port/host/socket/rejectUnauthorized/ca/servername/ALPNProtocols/minVersion/maxVersion/timeout";
if (args.length < 1 || args.length > 4 || args.some(ts.isSpreadElement)) {
lowerer.noLowering(`tls.connect with ${args.length} arguments`, expr, FORMS_HINT);
}
@@ -5043,7 +5182,22 @@ function lowerTlsConnectCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Src
// The options value rides whole to the runtime walk: dyn passes
// directly; a typed record that converts boxes into the checked-dynamic tree.
let opts: IrExpr;
if (optsNode !== null) {
let literal: ts.Expression | null = optsNode;
while (literal !== null && ts.isParenthesizedExpression(literal)) literal = literal.expression;
if (
literal !== null &&
ts.isObjectLiteralExpression(literal) &&
literal.properties.nodes.some(
(p) =>
(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
ts.isIdentifier(p.name) &&
p.name.text === "socket",
)
) {
// A socket member is a handle the record form cannot box: the literal
// builds the checked-dynamic object member by member.
opts = lowerDynObjectLiteral(lowerer, literal);
} else if (optsNode !== null) {
const o = lowerer.lowerExpr(optsNode);
if (o.type.kind === "dyn") {
opts = o;
@@ -139,6 +139,8 @@ import {
bindingGenericFnAliasInfoOf,
bindingGenericFnInfoOf,
bindingGenericFnNodeOf,
genericBindingPresenceStore,
genericBindingStoredValue,
implicitLocalFnInfoOf,
implicitLocalFnNodeOf,
implicitMethodCallInfersReturn,
@@ -3889,6 +3891,9 @@ function stdlibGlobalTokenDestructure(
if (decl.initializer === undefined || !isJsSourceFile(ts.getSourceFileOfNode(decl))) return null;
const globalName = stdlibGlobalNameOf(lowerer, decl.initializer);
if (globalName === null) return null;
// `workerData` is a value (the data the worker was started with), not an
// API surface: its members destructure as any untyped value's do.
if (globalName === "workerData") return null;
if (!ts.isObjectBindingPattern(decl.name)) {
lowerer.unsupported(
"SC1031",
@@ -5113,8 +5118,19 @@ export function lowerVarDecl(
{
const gfnNode = bindingGenericFnNodeOf(decl) ?? bindingContextualGenericFnNodeOf(lowerer, decl);
if (gfnNode) {
bindingGenericFnInfoOf(lowerer, decl, gfnNode);
return null;
const info = bindingGenericFnInfoOf(lowerer, decl, gfnNode);
// A nested function that lowered before this declaration read the
// binding as an ordinary TDZ box (predeclareForwardCapture); the
// declaration initializes it, so those reads see the function once
// it runs and a ReferenceError before.
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
const pre = symbol ? lowerer.ctx.tdzPredeclared.get(symbol) : undefined;
if (pre) {
lowerer.ctx.tdzPredeclared.delete(symbol!);
const value = genericBindingStoredValue(lowerer, decl, info);
return { kind: "assign", localId: pre.id, value, initializes: true, loc: locOf(decl) };
}
return genericBindingPresenceStore(info, decl);
}
}
@@ -308,6 +308,21 @@ function lowerStreamCallback(
// boxes each position by kind (the completion callback arrives as a
// callable dyn whose glue dispatches to the runtime's *_done).
if (funcType.params[i]!.kind === "dyn") continue;
// A write or transform chunk of another type converts from the
// chunk's value when the callback runs (an object-mode side's value,
// or the Buffer), or throws a TypeError there; an encoding of another
// type (`string | null | undefined`) from the encoding argument's.
if (
(i === 0 || i === 1) &&
(which === "write" || which === "transform") &&
funcType.params[i]!.kind !== "func" &&
canConvertToDyn(
funcType.params[i]!,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
)
)
continue;
if (!typeEquals(funcType.params[i]!, fullTuple[i]!)) {
lowerer.noLowering(
`a '${which}' callback whose parameter ${i} is '${lowerer.fmt(funcType.params[i]!)}' where Node passes '${lowerer.fmt(fullTuple[i]!)}'`,
@@ -404,11 +419,52 @@ interface StreamOptions {
* right after construction (Node's constructors call addAbortSignal
* once the state exists), or null when absent. */
signal: IrExpr | null;
/** The objectMode option's sides (a Duplex's readableObjectMode and
* writableObjectMode set one side): applied with stream.objectMode
* right after construction. */
readableObject: boolean;
writableObject: boolean;
/** Present callbacks in canonical order with their flag bits. */
flags: number;
cbs: IrExpr[];
}
/** The objectMode, readableObjectMode and writableObjectMode members of a
* stream options literal (boolean literals; the sided keys count only on
* a Duplex, Transform or PassThrough, as in Node). */
function streamObjectModes(
lowerer: Lowerer,
ctorName: string,
optsLit: ts.ObjectLiteralExpression | undefined,
): { readable: boolean; writable: boolean } {
const out = { readable: false, writable: false };
const duplexish = ctorName !== "Readable" && ctorName !== "Writable";
for (const prop of optsLit?.properties?.nodes ?? []) {
if (!ts.isPropertyAssignment(prop)) continue;
const name =
ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ? prop.name.text : null;
if (name !== "objectMode" && name !== "readableObjectMode" && name !== "writableObjectMode")
continue;
const value = prop.initializer;
if (value.kind === ts.SyntaxKind.FalseKeyword) continue;
if (value.kind !== ts.SyntaxKind.TrueKeyword) {
lowerer.noLowering(
`${ctorName} with a runtime '${name}' value`,
prop,
`write '${name}: true' or '${name}: false'`,
);
}
if (name === "objectMode") {
out.readable = true;
out.writable = true;
} else if (duplexish) {
if (name === "readableObjectMode") out.readable = true;
else out.writable = true;
}
}
return out;
}
/** The full Node parameter tuples of the option callbacks, given the
* concrete stream's `this` type. The trailing completion callbacks keep
* whatever func type the checker contextually gave the user's parameter
@@ -657,6 +713,8 @@ function parseStreamOptions(
badEncoding: null,
pushEncoding: null,
signal: null,
readableObject: false,
writableObject: false,
flags: 0,
cbs: [],
};
@@ -669,6 +727,18 @@ function parseStreamOptions(
"the options must be an object literal, inline or through const-initialized bindings (the callbacks lower as compile-time closures)",
);
}
// Read before the callbacks lower: an object-mode readable side keeps a
// completion callback's `data: any` (its values are not only strings and
// Buffers). An object-mode side's high-water mark defaults to 16.
const objectModes = streamObjectModes(
lowerer,
ctorName,
optsLit !== undefined && ts.isObjectLiteralExpression(optsLit) ? optsLit : undefined,
);
out.readableObject = objectModes.readable;
out.writableObject = objectModes.writable;
if (out.readableObject) out.hwmR = numLit(16, loc);
if (out.writableObject) out.hwmW = numLit(16, loc);
const tuples = cbTuples(lowerer);
const present = new Map<string, IrExpr>();
for (const prop of optsLit?.properties?.nodes ?? []) {
@@ -715,7 +785,7 @@ function parseStreamOptions(
continue;
}
const full = tuples[name]!.map((t) => (t === "fn" ? null : t));
const pinned = pinAnyDataCompletion(lowerer, name, fnNode);
const pinned = out.readableObject ? null : pinAnyDataCompletion(lowerer, name, fnNode);
try {
// The completion-callback tail: typed by the checker contextually;
// accept the user's declared func type as the tuple's entry so the
@@ -776,16 +846,9 @@ function parseStreamOptions(
break;
case "objectMode":
case "readableObjectMode":
case "writableObjectMode": {
// `objectMode: false` is the byte default — admit the literal.
if (value.kind === ts.SyntaxKind.FalseKeyword) break;
lowerer.noLowering(
`${ctorName} in objectMode`,
prop,
"object-mode streams have no static chunk type yet — chunks are Buffers (utf8 strings convert at push/write)",
);
case "writableObjectMode":
// read up front (streamObjectModes)
break;
}
case "decodeStrings": {
if (value.kind === ts.SyntaxKind.TrueKeyword) break; // Node's default
lowerer.noLowering(
@@ -942,6 +1005,8 @@ function streamCtorArgs(
badEncoding: string | null;
pushEncoding: string | null;
signal: IrExpr | null;
readableObject: boolean;
writableObject: boolean;
} {
const shape = streamCtorShape(cls);
const o = parseStreamOptions(
@@ -971,6 +1036,8 @@ function streamCtorArgs(
badEncoding: o.badEncoding,
pushEncoding: o.pushEncoding,
signal: o.signal,
readableObject: o.readableObject,
writableObject: o.writableObject,
};
}
@@ -1015,6 +1082,17 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C
);
}
let built: IrExpr = { kind: "libCall", fn, args: parsed.args, type: thisType, loc };
if (parsed.readableObject || parsed.writableObject) {
// { objectMode }: applied before any other follow-up (answers the
// receiver +1).
built = {
kind: "libCall",
fn: "stream.objectMode",
args: [built, boolLit(parsed.readableObject, loc), boolLit(parsed.writableObject, loc)],
type: thisType,
loc,
};
}
if (parsed.pushEncoding !== null) {
// { defaultEncoding }: chain the push-side decode default (the
// setEncoding chaining shape — answers the receiver +1).
@@ -1133,6 +1211,22 @@ function streamMethodWrapper(
);
}
} else if (!typeEquals(got.type, want)) {
// A write or transform encoding of another type (`string | null |
// undefined`) converts from the encoding argument's value: an
// object-mode write passes undefined, null or the name.
if (
i === 1 &&
(which === "write" || which === "transform") &&
got.type.kind !== "func" &&
canConvertToDyn(
got.type,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
)
) {
paramTypes.push(got.type);
continue;
}
// A declared supertype of what Node passes (`chunk: Buffer | string`)
// is sound: the wrapper takes Node's value and widens it into the
// declared parameter.
@@ -1140,6 +1234,22 @@ function streamMethodWrapper(
paramTypes.push(want);
continue;
}
// A write or transform chunk of another type converts from the
// chunk's value when the method runs (an object-mode side's value,
// or the Buffer), or throws a TypeError there.
if (
i === 0 &&
(which === "write" || which === "transform") &&
got.type.kind !== "func" &&
canConvertToDyn(
got.type,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
)
) {
paramTypes.push(got.type);
continue;
}
lowerer.noLowering(
`a '${methodName}' method whose parameter ${i} is '${lowerer.fmt(got.type)}' where Node passes '${lowerer.fmt(want)}'`,
blame,
@@ -1301,6 +1411,19 @@ export function lowerStreamSuperCall(
loc,
},
];
if (parsed.readableObject || parsed.writableObject) {
out.push({
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "stream.objectMode",
args: [thisRef(), boolLit(parsed.readableObject, loc), boolLit(parsed.writableObject, loc)],
type: thisType,
loc,
},
loc,
});
}
if (parsed.pushEncoding !== null) {
out.push({
kind: "exprStmt",
@@ -1602,7 +1725,17 @@ export function lowerStreamStaticCall(
}
const anySrc =
srcT?.kind === "dyn" ||
(srcT?.kind === "set" && typedElem(srcT.elem) !== null) ||
// a Set or Map (entry arrays) of other values: the checked-dynamic
// view iterates the collection itself
((srcT?.kind === "set" || srcT?.kind === "map") &&
canConvertToDyn(
srcT,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
)) ||
// an array of other values: the object-mode entries come from its
// checked-dynamic copy
(srcT?.kind === "array" && fromGenChunk(lowerer, srcT.elem) && typedElem(srcT.elem) === null) ||
(srcNode !== undefined &&
srcT === null &&
(lowerer.typeOf(srcNode).flags & ts.TypeFlags.Any) !== 0);
@@ -1623,20 +1756,24 @@ export function lowerStreamStaticCall(
lowerer.noLowering(
`Readable.from over a '${srcT ? lowerer.fmt(srcT) : "?"}' source`,
srcNode ?? call,
"the supported sources are a string or Buffer, and arrays, Sets, generators and class iterables of strings or Buffers",
"the supported sources are a string or Buffer, and arrays, Sets, Maps, generators and class iterables of values that convert to checked-dynamic values",
);
}
/** Whether a generator's yields are stream chunks: strings, Buffers, a
* union of them (null included: the runtime destroys the stream with
* ERR_STREAM_NULL_VALUES), or checked-dynamic values (checked when
* pushed). */
/** Whether a generator's yields can be stream chunks: values that box
* into checked-dynamic values (null included: the runtime destroys the
* stream with ERR_STREAM_NULL_VALUES). An object-mode stream takes any of
* them; a byte-mode one checks them when they are pushed or written. */
function fromGenChunk(lowerer: Lowerer, t: IrType): boolean {
const chunk = (a: IrType): boolean =>
a.kind === "string" || (a.kind === "bytes" && a.elem === "u8") || a.kind === "nullT";
if (t.kind === "dyn" || (chunk(t) && t.kind !== "nullT")) return true;
if (t.kind !== "union") return false;
return lowerer.unions.get(t.unionId)?.arms.every(chunk) ?? false;
if (t.kind === "nullT" || t.kind === "void" || t.kind === "func") return false;
return (
t.kind === "dyn" ||
canConvertToDyn(
t,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
)
);
}
/** The helpers Readable.from pulls a compiled generator through, as
@@ -3587,14 +3724,6 @@ function lowerStreamOperator(
throw new InternalCompilerError(`unknown stream operator ${member}`);
}
/** The literal 'buffer' encoding on write()/end(): Node 24 takes it for a
* Buffer chunk and throws ERR_UNKNOWN_ENCODING for a string chunk. */
function isBufferEncoding(lowerer: Lowerer, arg: ts.Expression | undefined): boolean {
if (arg === undefined) return false;
const t = lowerer.typeOf(arg);
return t.isStringLiteralType() && t.value === "buffer";
}
/** The one supported encoding argument: utf8 (Node's default), as a
* compile-time literal or absent. */
function checkUtf8Encoding(lowerer: Lowerer, member: string, arg: ts.Expression | undefined): void {
@@ -3925,10 +4054,24 @@ export function lowerStreamMethodCall(
if (probed?.type.kind === "dyn") return "dyn";
if (probed?.type.kind === "nullT") return "null";
}
// Any other value is a checked-dynamic chunk: one entry on an
// object-mode side, Node's chunk type error on a byte-mode one
// (undefined, which a byte-mode push ignores, included).
if (t?.kind === "void" || t?.kind === "undefinedT") return "dyn";
if (
t !== null &&
t.kind !== "func" &&
canConvertToDyn(
t,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
)
)
return "dyn";
lowerer.noLowering(
`${member} with a '${t ? lowerer.fmt(t) : "?"}' chunk`,
node,
"chunks are Buffers or utf8 strings — or unions of those with null (objectMode is not lowered)",
"chunks are values that convert to checked-dynamic values: Buffers, strings, numbers, records, arrays and class instances",
);
};
@@ -3973,11 +4116,17 @@ export function lowerStreamMethodCall(
checkUtf8Encoding(lowerer, member, args[1]);
const kind = chunkKind(args[0]!);
if ((kind === "null" || kind === "union" || kind === "dyn") && member === "unshift") {
lowerer.noLowering(
kind === "null" ? "unshift(null)" : "unshift with a nullable or dynamic chunk",
call,
"EOF signals through push(null)",
);
// unshift(value): an object-mode stream's value at the front, null
// the end of the stream, as Node's readableAddChunk takes them
const receiver = lowerer.lowerExpr(access.expression);
const chunk = lowerer.lowerExprExpecting(args[0]!, DYN);
return {
kind: "libCall",
fn: "readable.unshiftDyn",
args: [receiver, chunk],
type: VOID,
loc,
};
}
const receiver = lowerer.lowerExpr(access.expression);
if (kind === "null") {
@@ -4237,9 +4386,54 @@ export function lowerStreamMethodCall(
}
const kind = chunkKind(args[0]!);
if (kind === "null") lowerer.noLowering("write(null)", call, "end() finishes a writable");
const lowerWriteCb = (cbArg: ts.Expression): IrExpr => {
const cb = callbackDeclaresParameters(cbArg)
? null
: lowerer.lowerExprExpecting(cbArg, funcOf([], VOID));
if (cb === null || cb.type.kind !== "func" || cb.type.params.length > 0) {
lowerer.noLowering(
"write completion callbacks taking the error argument",
cbArg,
"() => void callbacks are supported (listen for 'error' to observe failures)",
);
}
return cb!;
};
// write(chunk, encoding[, cb]), and write(value, cb): the encoding
// argument reaches an object-mode side's _write as passed, null when
// the callback took its place (a byte-mode side encodes a string
// chunk with it).
const writeEnc = (encArg: ts.Expression | undefined, cbArg: ts.Expression | undefined) => {
const receiver = lowerer.lowerExpr(access.expression);
const chunk = lowerer.lowerExprExpecting(args[0]!, DYN);
const tail: IrExpr[] = [];
if (encArg !== undefined) tail.push(lowerer.lowerExprExpecting(encArg, DYN));
if (cbArg !== undefined) tail.push(lowerWriteCb(cbArg));
const flags = (encArg !== undefined ? 1 : 0) | (cbArg !== undefined ? 2 : 0);
return {
kind: "libCall",
fn: "writable.writeEnc",
args: [receiver, numLit(flags, loc), chunk, ...tail],
type: BOOL,
loc,
} satisfies IrExpr;
};
if (kind === "dyn") {
if (args.length > 1)
lowerer.noLowering("write with a dynamic chunk and more arguments", call);
if (args.length === 3) return writeEnc(args[1], args[2]);
if (args.length === 2) {
// Node's write takes a function there as the callback, anything
// else as the encoding
const t = lowerer.mapTypeOf(lowerer.typeOf(args[1]!));
if (t?.kind === "func") return writeEnc(undefined, args[1]);
if (t === null || t === undefined || t.kind === "dyn") {
lowerer.noLowering(
"write with a dynamic second argument",
args[1]!,
"pass an encoding string or a completion callback",
);
}
return writeEnc(args[1], undefined);
}
const receiver = lowerer.lowerExpr(access.expression);
const chunk = lowerer.lowerExprExpecting(args[0]!, DYN);
return { kind: "libCall", fn: "writable.writeDyn", args: [receiver, chunk], type: BOOL, loc };
@@ -4263,19 +4457,15 @@ export function lowerStreamMethodCall(
return { kind: "libCall", fn, args: [receiver, chunk], type: BOOL, loc };
}
// Disambiguate (chunk, cb?) vs (chunk, encoding, cb?) by the second
// argument's static type.
// argument's static type. A write with an encoding runs through
// writeEnc, whose runtime checks and applies it as Node's _write does.
let cbArg: ts.Expression | undefined;
if (args.length >= 2 && isBufferEncoding(lowerer, args[1])) {
if (kind !== "bytes")
return nodeThrowExpr(1, "ERR_UNKNOWN_ENCODING", "Unknown encoding: buffer", BOOL, loc);
cbArg = args[2];
} else if (args.length === 2) {
if (args.length === 3) return writeEnc(args[1], args[2]);
if (args.length === 2) {
const t = lowerer.mapTypeOf(lowerer.typeOf(args[1]!));
if (t?.kind === "string") checkUtf8Encoding(lowerer, "write", args[1]);
else cbArg = args[1];
} else if (args.length === 3) {
checkUtf8Encoding(lowerer, "write", args[1]);
cbArg = args[2];
if (t?.kind === "string" || t?.kind === "nullT" || t?.kind === "undefinedT")
return writeEnc(args[1], undefined);
cbArg = args[1];
}
const receiver = lowerer.lowerExpr(access.expression);
const chunk =
@@ -4284,16 +4474,7 @@ export function lowerStreamMethodCall(
: lowerer.lowerExprExpecting(args[0]!, STRING);
const fn = kind === "bytes" ? "writable.write" : "writable.writeStr";
if (cbArg !== undefined) {
const cb = callbackDeclaresParameters(cbArg)
? null
: lowerer.lowerExprExpecting(cbArg, funcOf([], VOID));
if (cb === null || cb.type.kind !== "func" || cb.type.params.length > 0) {
lowerer.noLowering(
"write completion callbacks taking the error argument",
cbArg,
"() => void callbacks are supported (listen for 'error' to observe failures)",
);
}
const cb = lowerWriteCb(cbArg);
return { kind: "libCall", fn, args: [receiver, chunk, cb], type: BOOL, loc };
}
return { kind: "libCall", fn, args: [receiver, chunk], type: BOOL, loc };
@@ -4305,34 +4486,29 @@ export function lowerStreamMethodCall(
// Forms: (), (cb), (chunk), (chunk, cb), (chunk, encoding, cb).
let chunkNode: ts.Expression | undefined;
let cbArg: ts.Expression | undefined;
let encNode: ts.Expression | undefined;
if (args.length === 1) {
const t = lowerer.mapTypeOf(lowerer.typeOf(args[0]!));
if (t?.kind === "func") cbArg = args[0];
else chunkNode = args[0];
} else if (args.length >= 2 && isBufferEncoding(lowerer, args[1])) {
chunkNode = args[0];
const endKind = chunkKind(args[0]!);
if (endKind === "string") {
const endT = lowerer.mapTypeOf(lowerer.typeOf(call)) ?? DYN;
return nodeThrowExpr(1, "ERR_UNKNOWN_ENCODING", "Unknown encoding: buffer", endT, loc);
}
if (endKind !== "bytes") checkUtf8Encoding(lowerer, "end", args[1]);
cbArg = args[2];
} else if (args.length === 2) {
chunkNode = args[0];
const t = lowerer.mapTypeOf(lowerer.typeOf(args[1]!));
if (t?.kind === "string") checkUtf8Encoding(lowerer, "end", args[1]);
if (t?.kind === "string" || t?.kind === "nullT" || t?.kind === "undefinedT")
encNode = args[1];
else cbArg = args[1];
} else if (args.length === 3) {
chunkNode = args[0];
checkUtf8Encoding(lowerer, "end", args[1]);
encNode = args[1];
cbArg = args[2];
}
const receiver = lowerer.lowerExpr(access.expression);
let flags = 0;
const tail: IrExpr[] = [];
if (chunkNode !== undefined) {
const kind = chunkKind(chunkNode);
// end(chunk, encoding[, cb]) writes the chunk with the encoding
// argument as passed (flags 16), through the dyn write
const kind = encNode !== undefined ? "dyn" : chunkKind(chunkNode);
if (kind === "null" || kind === "union") lowerer.noLowering("end(null)", call);
flags |= kind === "bytes" ? 1 : kind === "dyn" ? 8 : 2;
tail.push(
@@ -4342,6 +4518,10 @@ export function lowerStreamMethodCall(
? lowerer.lowerExprExpecting(chunkNode, DYN)
: lowerer.lowerExprExpecting(chunkNode, STRING),
);
if (encNode !== undefined) {
flags |= 16;
tail.push(lowerer.lowerExprExpecting(encNode, DYN));
}
}
if (cbArg !== undefined) {
const cb = callbackDeclaresParameters(cbArg)
@@ -4604,8 +4784,7 @@ export function lowerStreamProperty(
return propCall(BOOL);
}
if (name === "writableObjectMode" && writableSide) {
// objectMode construction fences, so the answer is compile-time false.
return boolLit(false, loc);
return propCall(BOOL);
}
if (name === "allowHalfOpen" && sides === "rw") {
return propCall(BOOL);
@@ -1914,6 +1914,9 @@ export class Lowerer {
* pinned values resolve through genericFnOf exactly like top-level
* generic function declarations (bindingGenericFnInfoOf). */
readonly bindingGenericFns = new Map<ts.Node, GenericFnInfo>();
/** Identifiers by spelling, per file, for the generic bindings' use
* ordering (runsAfterDeclaration). */
readonly bindingIdentifiers = new Map<ts.SourceFile, Map<string, ts.Identifier[]>>();
/** Per-symbol result of the never-reassigned file scan
* (bindingNeverReassigned — object-literal generic-method receivers). */
readonly neverReassignedCache = new Map<ts.Symbol, boolean>();
@@ -1553,6 +1553,16 @@ export const BUILTIN_MODULE_FNS: Record<
createSecretKey: { fn: "crypto.createSecretKey", params: [DYN, DYN], result: DYN },
generateKeySync: { fn: "crypto.generateKeySync", params: [DYN, DYN], result: DYN },
generateKey: { fn: "crypto.generateKeyCb", params: [], result: VOID },
// Asymmetric keys and signatures (lowerPkiCall): checked-dynamic
// arguments, results converted to the call site's type.
createPrivateKey: { fn: "crypto.createPrivateKey", params: [DYN], result: DYN },
createPublicKey: { fn: "crypto.createPublicKey", params: [DYN], result: DYN },
createSign: { fn: "crypto.createSign", params: [DYN], result: DYN },
createVerify: { fn: "crypto.createVerify", params: [DYN], result: DYN },
generateKeyPairSync: { fn: "crypto.generateKeyPairSync", params: [], result: DYN },
generateKeyPair: { fn: "crypto.generateKeyPairCb", params: [], result: VOID },
sign: { fn: "crypto.signOneshot", params: [], result: DYN },
verify: { fn: "crypto.verifyOneshot", params: [], result: DYN },
pbkdf2Sync: {
fn: "crypto.pbkdf2",
params: [BYTES_U8, BYTES_U8, F64, F64, STRING],
@@ -2211,8 +2221,8 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
kind: "stdlib",
name: "globalThis.crypto.subtle",
fns: [
"webcrypto.importKey",
"webcrypto.generateKey",
"webcrypto.importKey",
"webcrypto.exportKey",
"webcrypto.encrypt",
"webcrypto.decrypt",
@@ -2224,7 +2234,7 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
"webcrypto.verify",
"webcrypto.digest",
],
note: "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, wrapKey and unwrapKey with AES-KW or those ciphers, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused",
note: "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused",
},
{
id: "stdlib.webcrypto.CryptoKey",
@@ -3070,14 +3080,6 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record<
crypto: {
...Object.fromEntries(
[
"generateKeyPair",
"generateKeyPairSync",
"createPrivateKey",
"createPublicKey",
"createSign",
"createVerify",
"sign",
"verify",
"createDiffieHellman",
"createDiffieHellmanGroup",
"getDiffieHellman",
@@ -3088,9 +3090,9 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record<
"privateDecrypt",
].map((m) => [
m,
"asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and " +
"public and private KeyObjects — neither exists in the static runtime, so no faithful lowering " +
"can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics",
"Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is " +
"RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, " +
"createSign, createVerify, and createECDH",
]),
),
...Object.fromEntries(
+120 -4
View File
@@ -140,6 +140,15 @@ import {
} from "./type-only-imports.js";
import { moduleSourceCandidates } from "./module-source-candidates.js";
import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js";
import {
containsJsx,
firstJsx,
jsxEmitName,
jsxEmitPlan,
jsxPragmas,
lowerJsx,
type LoweredJsx,
} from "./jsx-source.js";
const BASE_OPTIONS: ts.Ts7CompilerOptions = {
strict: true,
@@ -377,6 +386,18 @@ export interface LoadResult {
* checkPreflight. */
startupCrash?: StartupCrash | null;
configDiags: ScrDiagnostic[];
/** The files whose JSX the program lowers (jsx-source.ts), by file name.
* The program holds their rewritten text; positions in diagnostics refer
* to it. */
jsxSources?: ReadonlyMap<string, LoweredJsx>;
/** With JSX sources: the program over the original text, whose TypeScript
* errors are the program's, and its project world when it has errors. */
tscWorld?: {
program: ts.Program;
/** The program's pre-emit diagnostics, computed once. */
diagnostics: readonly ts.Diagnostic[];
projectWorld: ts.Program | null;
};
/** How scriptc's type world differs from the project's own tsc setup —
* used only to label the TypeScript errors those differences cause. */
typeWorld?: TypeWorldDifferences;
@@ -778,6 +799,47 @@ function moduleSpecifierKind7(specifier: ts.StringLiteralLike): "import" | "requ
: "import";
}
/** The program's files with JSX, lowered with the project's jsx settings
* and each file's pragmas, or the refusal of a setting that emits no calls.
* TypeScript files (.ts, .mts, .cts) cannot hold JSX; without a jsx setting
* the checker already rejects any JSX (TS17004). */
function lowerProgramJsx7(
program: ts.Program,
options: ts.Ts7CompilerOptions,
): { sources: Map<string, LoweredJsx>; refusal: ScrDiagnostic | null } {
const sources = new Map<string, LoweredJsx>();
const mode = jsxEmitName(options.jsx as string | number | undefined);
if (mode === undefined) return { sources, refusal: null };
for (const sf of program.getImplementationSourceFiles()) {
if (sf.isDeclarationFile || /\.[mc]?ts$/.test(sf.fileName) || !containsJsx(sf)) continue;
const at = firstJsx(sf)!;
const loc = { file: sf.fileName, start: ts.tokenStartOfNode(at), end: at.end };
if (mode === "preserve" || mode === "react-native") {
return {
sources,
refusal: {
code: "SC1090",
message: `JSX with "jsx": "${mode}" is not supported: that setting keeps JSX in the output instead of lowering it to calls`,
loc,
hint: 'set "jsx" to "react", "react-jsx" or "react-jsxdev" in the project tsconfig',
},
};
}
const plan = jsxEmitPlan(
{
jsx: mode,
jsxFactory: options.jsxFactory as string | undefined,
jsxFragmentFactory: options.jsxFragmentFactory as string | undefined,
jsxImportSource: options.jsxImportSource as string | undefined,
reactNamespace: options.reactNamespace as string | undefined,
},
jsxPragmas(sf.text),
);
sources.set(sf.fileName, lowerJsx(sf, plan));
}
return { sources, refusal: null };
}
function loadProgram7(
host: ts.Ts7Host,
entryPath: string,
@@ -859,6 +921,26 @@ function loadProgram7(
};
program = host.createProgram([...programRoots, overridesDtsPath()], options);
}
// JSX: the program above checks the files as written; the program the
// lowering compiles holds them with their JSX lowered.
const jsx = lowerProgramJsx7(program, options);
let tscWorld: LoadResult["tscWorld"];
if (jsx.refusal !== null) {
config.diags.push(jsx.refusal);
} else if (jsx.sources.size > 0) {
const original = program;
// The project world must read the original text too, so it is built
// before the rewritten files are served, and only when it is needed.
const diagnostics = ts.getPreEmitDiagnostics(original);
const errors = diagnostics.some((d) => d.category === ts.DiagnosticCategory.Error);
tscWorld = {
program: original,
diagnostics,
projectWorld: errors ? host.createProgram(programRoots, options) : null,
};
for (const [file, lowered] of jsx.sources) host.addVirtualFile(file, lowered.text);
program = host.createProgram([...programRoots, overridesDtsPath()], options);
}
const entry = program.getSourceFile(entryPath);
if (!entry) throw new Error(`could not load ${entryPath}`);
const externalTypeSpecifiersByFile = externalTypeFileClosure7(program, externalTypes);
@@ -876,10 +958,14 @@ function loadProgram7(
},
externalTypes,
externalTypeSpecifiersByFile,
...(tscWorld === undefined ? {} : { jsxSources: jsx.sources }),
...(tscWorld === undefined ? {} : { tscWorld }),
projectWorld: () => (projectWorld ??= host.createProgram(programRoots, options)),
disposeAll: () => {
try {
projectWorld?.dispose();
tscWorld?.projectWorld?.dispose();
tscWorld?.program.dispose();
} finally {
program.dispose();
}
@@ -4665,8 +4751,8 @@ function preflight7(load: LoadResult): {
const sf = p.getSourceFile(d.fileName);
return sf !== undefined && isCheckedJsSourceFile(sf, projectCheckJs);
};
const errorsOf = (p: ts.Program): ts.Diagnostic[] => {
const all = ts.getPreEmitDiagnostics(p);
const errorsOf = (p: ts.Program): ts.Diagnostic[] => keptErrors(p, ts.getPreEmitDiagnostics(p));
const keptErrors = (p: ts.Program, all: readonly ts.Diagnostic[]): ts.Diagnostic[] => {
// First pass: every comment-side 2300's (file, name) — the partners
// the second pass forgives.
const commentDup = new Set<string>();
@@ -4706,9 +4792,39 @@ function preflight7(load: LoadResult): {
// instead (the SC1100 checked-cast family). Dirty there → the project-
// world errors are the ones reported: they are reproducible with the
// project's own tsc, which is what "fix type errors first" asks for.
const tscErrors = errorsOf(program);
// With JSX sources, the program over the original text answers the tsc
// gate; its positions move into the rewritten text the program holds.
// Errors only the rewritten text has come from the calls the JSX lowers
// to (a factory whose signature does not accept them).
const tscWorld = load.tscWorld;
const tscErrors =
tscWorld === undefined ? errorsOf(program) : keptErrors(tscWorld.program, tscWorld.diagnostics);
if (tscErrors.length > 0) {
for (const d of errorsOf(load.projectWorld())) diags.push(toPassthrough(d));
const world =
tscWorld === undefined ? load.projectWorld() : (tscWorld.projectWorld ?? tscWorld.program);
for (const d of errorsOf(world)) {
const diag = toPassthrough(d);
const lowered = tscWorld === undefined ? undefined : load.jsxSources?.get(diag.loc.file);
if (lowered !== undefined) {
diag.loc = {
...diag.loc,
start: lowered.rewrittenPosition(diag.loc.start, false),
end: lowered.rewrittenPosition(diag.loc.end, true),
};
}
diags.push(diag);
}
} else if (load.jsxSources !== undefined) {
for (const file of load.jsxSources.keys()) {
const sf = program.getSourceFile(file);
if (sf === undefined) continue;
const own = [...program.getSyntacticDiagnostics(sf), ...program.getSemanticDiagnostics(sf)];
for (const d of keptErrors(program, own)) {
const diag = toPassthrough(d);
diag.message = `${diag.message} (in the call this JSX lowers to)`;
diags.push(diag);
}
}
}
// Preflight and lowering share this CheckerFacade. Claim executable
+70 -2
View File
@@ -45,7 +45,7 @@ import type {
TypePredicate,
TypeReference,
} from "./semantic-types.js";
import type { SemanticChecker as Checker } from "./semantic-checker.js";
import { NODE_BATCH_QUERIES, type SemanticChecker as Checker } from "./semantic-checker.js";
import { symbolNameOf, type SemanticProject as Project } from "./semantic-model.js";
import { isTypeNode, walkPreorder } from "./ast.js";
import { SignatureKind, SyntaxKind, TypeFlags, type SymbolFlags } from "./enums.js";
@@ -79,6 +79,30 @@ function withPanicFence<I, O>(
}
}
/** Answers for `items` in chunks, each answer given to `store`. A chunk
* that throws is split until the throwing items are alone; those get no
* answer, so their direct query throws again where it is made. */
function batchAnswers<I, O>(
items: readonly I[],
fetch: (chunk: readonly I[]) => readonly (O | undefined)[],
store: (item: I, answer: O | undefined) => void,
): void {
const answer = (chunk: readonly I[]): void => {
let answers: readonly (O | undefined)[];
try {
answers = fetch(chunk);
} catch {
if (chunk.length === 1) return;
const mid = chunk.length >> 1;
answer(chunk.slice(0, mid));
answer(chunk.slice(mid));
return;
}
chunk.forEach((item, index) => store(item, answers[index]));
};
for (let i = 0; i < items.length; i += BATCH_CHUNK) answer(items.slice(i, i + BATCH_CHUNK));
}
function chunked<T, R>(items: readonly T[], fetch: (chunk: readonly T[]) => readonly R[]): R[] {
const out: R[] = [];
for (let i = 0; i < items.length; i += BATCH_CHUNK) {
@@ -322,7 +346,51 @@ export class CheckerFacade {
prefetchRoots(roots: readonly Node[]): void {
this.ensureActive();
this.markManaged(roots);
this.prefetchNodes(collectNodes(roots, "reachable"));
const nodes = collectNodes(roots, "reachable");
this.prefetchNodes(nodes);
if (!NODE_BATCH_QUERIES) return;
// Lowering a call resolves its signature (reconcileOverloadReturn).
this.prefetchResolvedSignatures(
nodes.filter(
(node) =>
node.kind === SyntaxKind.CallExpression ||
node.kind === SyntaxKind.TaggedTemplateExpression,
),
);
// Lowering an array literal or a conditional asks its contextual type.
this.prefetchContextualTypes(
nodes.filter(
(node) =>
node.kind === SyntaxKind.ArrayLiteralExpression ||
node.kind === SyntaxKind.ConditionalExpression,
),
);
}
/** Batches the contextual type of each node, for an analysis that asks it
* of many nodes (class-protocols.ts). A query that fails stays out of the
* memo, so a later getContextualType asks again as it would have. */
prefetchContextualTypes(nodes: readonly Node[]): void {
if (!NODE_BATCH_QUERIES) return;
this.ensureActive();
const missing = [...new Set(nodes)].filter((node) => !this.cache.contextualType.has(node));
batchAnswers(
missing,
(chunk) => this.raw.getContextualTypes(chunk),
(node, type) => this.cache.contextualType.set(node, type),
);
}
/** Batches the resolved signature of each call (getResolvedSignature). */
prefetchResolvedSignatures(nodes: readonly Node[]): void {
if (!NODE_BATCH_QUERIES) return;
this.ensureActive();
const missing = [...new Set(nodes)].filter((node) => !this.cache.resolvedSignature.has(node));
batchAnswers(
missing,
(chunk) => this.raw.getResolvedSignatures(chunk),
(node, signature) => this.cache.resolvedSignature.set(node, signature),
);
}
/** Batches symbol queries for every identifier under roots without the
@@ -1315,3 +1315,25 @@ export enum LanguageVariant {
Standard = 0,
JSX = 1
}
export enum JsxEmit {
None = 0,
Preserve = 1,
ReactNative = 2,
React = 3,
ReactJSX = 4,
ReactJSXDev = 5
}
/** Numeric lookup with the SDK's aliases and undefined for unknown values. */
export function jsxEmitName(value: number): string | undefined {
switch (value) {
case 0: return "None";
case 1: return "Preserve";
case 2: return "ReactNative";
case 3: return "React";
case 4: return "ReactJSX";
case 5: return "ReactJSXDev";
default: return undefined;
}
}
@@ -1,4 +1,25 @@
/** Runtime enums generated from the pinned TypeScript 7 declarations.
* Their nominal names and values match the SDK; TypeScript 5 remains a
* separate world. No package loader runs on the compiler's native path. */
import { jsxEmitName } from "./enums.generated.js";
export * from "./enums.generated.js";
/** The tsconfig spelling of a `jsx` option value: "preserve", "react-native",
* "react", "react-jsx" or "react-jsxdev". */
export function jsxOptionName(value: number): string | undefined {
switch (jsxEmitName(value)) {
case "Preserve":
return "preserve";
case "ReactNative":
return "react-native";
case "React":
return "react";
case "ReactJSX":
return "react-jsx";
case "ReactJSXDev":
return "react-jsxdev";
default:
return undefined;
}
}
@@ -30,6 +30,7 @@ import type {
} from "./session.js";
import { CheckerFacade } from "./checker.js";
import {
jsxOptionName,
moduleDetectionKindName,
moduleKindName,
moduleResolutionKindName,
@@ -82,6 +83,9 @@ function serializeOptions(options: Ts7CompilerOptions): Record<string, unknown>
case "moduleDetection":
out[key] = moduleDetectionKindName(value as number)?.toLowerCase() ?? value;
break;
case "jsx":
out[key] = typeof value === "number" ? (jsxOptionName(value) ?? value) : value;
break;
case "lib":
// 5.9.3 spells lib entries "lib.es2025.d.ts"; tsconfig wants "es2025".
out[key] = (value as string[]).map((lib) =>
@@ -96,6 +96,13 @@ export interface SemanticCompletion {
symbol: SemanticSymbol | undefined;
}
/** Whether lowering asks the checker for contextual types and resolved
* signatures in batches (`getContextualTypes`, `getResolvedSignatures`).
* Off until scriptc depends on a tsc-ts release that answers them: the
* batch prefetches (checker.ts) then do nothing and lowering asks node by
* node. */
export const NODE_BATCH_QUERIES = false;
/** The pinned checker's request surface over native objects. Every query
* converges on the same snapshot/project registries, including batch and
* lazy-object queries. No SDK object is constructed or consulted here. */
@@ -201,6 +208,10 @@ export class SemanticChecker {
getContextualType(node: AstNode): SemanticType | undefined {
return this.type("getContextualType", { location: node.id });
}
/** getContextualType for each node. */
getContextualTypes(nodes: readonly AstNode[]): (SemanticType | undefined)[] {
return this.optionalTypes("getContextualTypes", { locations: nodes.map((node) => node.id) });
}
getTypeFromTypeNode(node: AstNode): SemanticType | undefined {
return this.type("getTypeFromTypeNode", { location: node.id });
}
@@ -213,6 +224,12 @@ export class SemanticChecker {
getResolvedSignature(node: AstNode): SemanticSignature | undefined {
return this.signature("getResolvedSignature", { location: node.id });
}
/** getResolvedSignature for each node. */
getResolvedSignatures(nodes: readonly AstNode[]): (SemanticSignature | undefined)[] {
return this.project.requestSignatures("getResolvedSignatures", {
locations: nodes.map((node) => node.id),
});
}
getSignatureFromDeclaration(node: AstNode): SemanticSignature | undefined {
return this.signature("getSignatureFromDeclaration", { location: node.id });
}
@@ -27,6 +27,13 @@ export const ADOPTED_OPTIONS = [
// SC1012 "not supported yet" beats a raw TS1259 at the same site.
"esModuleInterop",
"allowSyntheticDefaultImports",
// JSX: the checker types JSX as the project's tsc does, and the JSX
// lowering (jsx-source.ts) emits the calls that setting selects.
"jsx",
"jsxFactory",
"jsxFragmentFactory",
"jsxImportSource",
"reactNamespace",
] as const;
export const JS_RELAXED_TSC_CODES: ReadonlySet<number> = new Set([
+99 -3
View File
@@ -147,6 +147,15 @@ export const WEBCRYPTO_DYN_TYPES: ReadonlySet<string> = new Set([
"KeyAlgorithm",
"HmacImportParams",
"HmacKeyAlgorithm",
"CryptoKeyPair",
"EcKeyAlgorithm",
"RsaHashedKeyAlgorithm",
"EcKeyGenParams",
"EcKeyImportParams",
"EcdsaParams",
"RsaHashedKeyGenParams",
"RsaHashedImportParams",
"RsaPssParams",
"HmacKeyGenParams",
"AesKeyGenParams",
"AesKeyAlgorithm",
@@ -159,6 +168,28 @@ export const WEBCRYPTO_DYN_TYPES: ReadonlySet<string> = new Set([
"AesDerivedKeyParams",
]);
/** node:crypto's key and signature option interfaces: the key functions,
* KeyObject.export and sign/verify take them as checked-dynamic arguments
* (scr_lib.c, scr_pki.c). */
export const CRYPTO_KEY_OPTION_DYN_TYPES: ReadonlySet<string> = new Set([
"AsymmetricKeyExportOptions",
"ECKeyPairKeyObjectOptions",
"ECKeyPairOptions",
"ED25519KeyPairOptions",
"JsonWebKeyInput",
"JwkKeyExportOptions",
"KeyExportOptions",
"PrivateKeyInput",
"PublicKeyInput",
"RSAKeyPairKeyObjectOptions",
"RSAKeyPairOptions",
"SignKeyObjectInput",
"SignPrivateKeyInput",
"SigningOptions",
"VerifyKeyObjectInput",
"VerifyPublicKeyInput",
]);
export const FETCH_AMBIENT_TYPES = [
"Response",
"Request",
@@ -2987,9 +3018,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
) {
return DYN;
}
// node:crypto's KeyObject: secret keys are native handles (scr_lib.c)
// whose getters and methods dispatch through the handle. The JsonWebKey
// that export() returns is a plain checked-dynamic object.
// node:crypto's KeyObject: secret keys (scr_lib.c) and private and
// public keys (scr_pki.c) are native handles whose getters and methods
// dispatch through the handle. The JsonWebKey that export() returns is a
// plain checked-dynamic object.
if (
((psym !== undefined && ts.symbolName(psym) === "KeyObject") ||
(psym !== undefined && ts.symbolName(psym) === "JsonWebKey")) &&
@@ -3001,6 +3033,35 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
) {
return DYN;
}
// node:crypto's key and signature options are plain checked-dynamic
// objects, so a value asserted to one (`opts as
// crypto.AsymmetricKeyExportOptions<"pem">`) reaches the call unchanged.
if (
psym !== undefined &&
CRYPTO_KEY_OPTION_DYN_TYPES.has(ts.symbolName(psym)) &&
psym.declarations.some(
(d) =>
ts.isInterfaceDeclaration(d) &&
ctx.isStdlibFile(ts.getSourceFileOfNode(d)) &&
isDeclaredInAmbientModule(d, "crypto"),
)
) {
return DYN;
}
// node:crypto's Sign and Verify (createSign/createVerify): native
// handles over the digest state (scr_pki.c).
if (
psym !== undefined &&
(ts.symbolName(psym) === "Sign" || ts.symbolName(psym) === "Verify") &&
psym.declarations.some(
(d) =>
ts.isClassDeclaration(d) &&
ctx.isStdlibFile(ts.getSourceFileOfNode(d)) &&
isDeclaredInAmbientModule(d, "crypto"),
)
) {
return DYN;
}
if (
psym &&
(FETCH_AMBIENT_TYPES as readonly string[]).includes(ts.symbolName(psym)) &&
@@ -4267,6 +4328,41 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
};
}
}
// crypto.generateKeyPairSync's results: KeyPairKeyObjectResult and
// KeyPairSyncResult<T1, T2> as { publicKey, privateKey } records over the
// mapped property types (KeyObjects, PEM strings or DER Buffers), in
// Node's key order.
if (
psym !== undefined &&
(ts.symbolName(psym) === "KeyPairKeyObjectResult" ||
ts.symbolName(psym) === "KeyPairSyncResult") &&
psym.declarations.some(
(d) =>
ts.isInterfaceDeclaration(d) &&
ctx.isStdlibFile(ts.getSourceFileOfNode(d)) &&
isDeclaredInAmbientModule(d, "crypto"),
)
) {
const field = (name: string): IrType | null => {
const prop = ctx.checker.getPropertyOfType(type, name);
return prop ? mapType(ctx.checker.getTypeOfSymbol(prop), ctx) : null;
};
const publicKey = field("publicKey");
const privateKey = field("privateKey");
if (!publicKey || !privateKey) return null;
return {
kind: "record",
shapeId: ctx.shapes.intern(
[
{ name: "privateKey", type: privateKey },
{ name: "publicKey", type: publicKey },
],
false,
undefined,
["publicKey", "privateKey"],
),
};
}
// crypto.X509Certificate — the Dirent-style data record: THREE visible
// fields (fingerprint and the validFrom/validTo validity window, all
// computed at construction by the new-expression lowering). Module-
+188 -53
View File
@@ -49,7 +49,8 @@ import { frontendProcessExecArgv } from "./frontend-process-args.js";
import { type ChildProcess, spawn } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
import { copyFile, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
@@ -524,6 +525,9 @@ async function compileExecutableNative(
onArtifactReady?: NonNullable<Parameters<typeof compileExternalC>[0]["onArtifactReady"]>,
programPartitions: readonly string[] = [],
profileGenerate = false,
/** The kept LLVM text's shared name, when `llvmPath` is the build's private copy of it
* (BuildSources): the spelling clang sees and native cache keys name. */
llvmVisiblePath?: string,
): Promise<void> {
const programIsObject = /\.(?:o|obj)$/.test(llvmPath);
const runtimePackTarget = programIsObject && !sanitize ? nativeCodegenTarget() : null;
@@ -563,7 +567,9 @@ async function compileExecutableNative(
if (programIsObject)
throw new InternalCompilerError("program objects must link against a precompiled runtime pack");
await compileExternalC({
cPath: llvmPath,
...(llvmVisiblePath === undefined || llvmVisiblePath === llvmPath
? { cPath: llvmPath }
: { cPath: llvmVisiblePath, cSourcePath: llvmPath }),
outPath,
cacheIdentity: "scriptc-generated-v1",
...optimizationField(features.optimization),
@@ -624,6 +630,8 @@ async function compileExecutableNative(
tlsCa: features.tlsCa,
cipher: features.cipher,
ecdh: features.ecdh,
pki: features.pki,
ed25519: features.ed25519,
webcrypto: features.webcrypto,
perfLoop: features.perfLoop,
...(onArtifactReady === undefined ? {} : { onArtifactReady }),
@@ -729,8 +737,12 @@ function runtimePackDiagnostic(error: RuntimePackError, entryPath: string): ScrD
interface PreparedExecutable {
llvmSource: LlvmInput;
/** The kept LLVM text beside the executable, installed when the build ends. */
llvmPath: string;
/** This build's private copy of the LLVM text (BuildSources). */
llvmSourcePath: string;
irPath: string | undefined;
irSourcePath: string | undefined;
nativeFeatures: EarlyExecutableNativeFeatures;
programSplit: ReturnType<typeof splitLlvmProgram>;
sourceTexts: Map<string, string>;
@@ -841,6 +853,8 @@ async function prepareExecutableInput(
await mkdir(opts.outDir, { recursive: true });
const llvmPath = defaultSourcePaths.llvm;
// The extension keeps clang reading the copy as LLVM IR rather than as a linker input.
const llvmSourcePath = `${privateSiblingPath(llvmPath, "llvm")}.ll`;
const backend = "llvm" as const;
const useRuntimePack =
opts.nativeProgramObject === true || usesPrecompiledRuntimePack(opts, backend);
@@ -863,12 +877,14 @@ async function prepareExecutableInput(
return { ok: false, diagnostics: [llvmRefusalDiag(err, entryPath)], sourceTexts };
}
timing("llvm-emit");
await writeLlvmSource(llvmPath, llvmSource);
await writeLlvmSource(llvmSourcePath, llvmSource);
timing("llvm-write");
let irPath: string | undefined;
let irSourcePath: string | undefined;
if (opts.emitIr) {
irPath = defaultSourcePaths.ir;
await writeTextParts(irPath, serializeModuleParts(mod));
irSourcePath = privateSiblingPath(irPath, "ir");
await writeTextParts(irSourcePath, serializeModuleParts(mod));
}
const nativeFeatures = executableNativeFeatures(mod, backend, opts.optimization ?? "release");
@@ -882,7 +898,87 @@ async function prepareExecutableInput(
? splitLlvmProgram(llvmSource)
: null;
timing("llvm-split");
return { llvmSource, llvmPath, irPath, nativeFeatures, programSplit, sourceTexts };
return {
llvmSource,
llvmPath,
llvmSourcePath,
irPath,
irSourcePath,
nativeFeatures,
programSplit,
sourceTexts,
};
}
/**
* The kept LLVM text (`<entry stem>.ll`) and IR (`<entry stem>.ir.json`) sit beside the executable,
* so programs whose entries share a name and are built into one output directory share them. Each
* build therefore writes, compiles and caches private copies (privateSiblingPath) and moves them to
* the shared names only when it is done (rename replaces a file in one step): a concurrent build of
* another program cannot replace the text a build compiles or the translation unit it caches.
* Builds that compile with the C driver install them before the native build instead
* (stageDriverBuildSources).
*/
interface BuildSources {
llvmPath: string;
llvmSourcePath: string;
irPath?: string | undefined;
irSourcePath?: string | undefined;
/** The shared name already holds the private copy's bytes (a cache hit's restored files): it is
* left as it is, with its time stamps. */
llvmPathCurrent?: boolean | undefined;
irPathCurrent?: boolean | undefined;
}
/** Moves a build's private copies to the shared names, or drops them when that fails. */
async function installBuildSources(sources: BuildSources): Promise<void> {
const install = async (
source: string | undefined,
destination: string | undefined,
current: boolean | undefined,
) => {
if (source === undefined || destination === undefined || source === destination) return;
if (current === true) await rm(source, { force: true });
else await rename(source, destination).catch(() => rm(source, { force: true }));
};
await Promise.all([
install(sources.llvmSourcePath, sources.llvmPath, sources.llvmPathCurrent),
install(sources.irSourcePath, sources.irPath, sources.irPathCurrent),
]);
}
/**
* A build that compiles the LLVM text with the C driver (no precompiled runtime pack) records the
* output directory in its native cache proofs, so that directory has to be in its final state before
* the native build starts. The build therefore installs the shared names first and compiles and
* caches copies in a private directory outside the output directory. Returns that directory and the
* copies.
*/
async function stageDriverBuildSources(
sources: BuildSources,
): Promise<{ dir: string; sources: BuildSources }> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-program-"));
const llvmSourcePath = join(dir, basename(sources.llvmPath));
const irSourcePath =
sources.irPath === undefined || sources.irSourcePath === undefined
? undefined
: join(dir, basename(sources.irPath));
try {
await Promise.all([
copyFile(sources.llvmSourcePath, llvmSourcePath),
irSourcePath === undefined || sources.irSourcePath === undefined
? undefined
: copyFile(sources.irSourcePath, irSourcePath),
]);
} catch (err) {
await rm(dir, { recursive: true, force: true });
throw err;
}
await installBuildSources(sources);
return {
dir,
sources: { llvmPath: sources.llvmPath, llvmSourcePath, irPath: sources.irPath, irSourcePath },
};
}
/**
@@ -976,10 +1072,11 @@ type FrontendProcessResult =
| {
kind: "prepared";
llvmPath: string;
/** A private copy of the LLVM text, which the native helper reads (`nativeInputPath`). */
/** The build's private copy of the LLVM text (BuildSources), which the native helper reads. */
nativeInputPath: string;
llvmBytes: number;
irPath: string | undefined;
irSourcePath: string | undefined;
nativeFeatures: EarlyExecutableNativeFeatures;
sourceTexts: [string, string][];
warnings: ScrDiagnostic[];
@@ -1047,7 +1144,9 @@ async function prepareExecutableInputInChild(
return {
llvmSource: { file: result.nativeInputPath, bytes: result.llvmBytes },
llvmPath: result.llvmPath,
llvmSourcePath: result.nativeInputPath,
irPath: result.irPath,
irSourcePath: result.irSourcePath,
nativeFeatures: result.nativeFeatures,
programSplit: null,
sourceTexts,
@@ -1085,18 +1184,15 @@ export async function runFrontendProcess(requestPath: string, resultPath: string
);
const frontend = frontendInputs.snapshot();
if (!("ok" in prepared)) {
// The public .ll file may be rewritten by a concurrent build of another entry with the
// same output stem before the native helper reads it (see emitNativeArtifact).
if (isLlvmFile(prepared.llvmSource))
throw new InternalCompilerError("in-process preparation returned LLVM text as a file");
const nativeInputPath = privateSiblingPath(prepared.llvmPath, "frontend-llvm");
await writeLlvmSource(nativeInputPath, prepared.llvmSource, 0o600);
result = {
kind: "prepared",
llvmPath: prepared.llvmPath,
nativeInputPath,
nativeInputPath: prepared.llvmSourcePath,
llvmBytes: llvmInputBytes(prepared.llvmSource),
irPath: prepared.irPath,
irSourcePath: prepared.irSourcePath,
nativeFeatures: prepared.nativeFeatures,
sourceTexts: [...prepared.sourceTexts],
warnings: warnings.warnings,
@@ -1450,37 +1546,54 @@ async function compileTracked(
...(earlyHit.irPath === undefined ? {} : { irPath: earlyHit.irPath }),
};
}
let nativeInputPath = earlyHit.llvmPath;
// The build's private copies of the cached LLVM text and IR (BuildSources).
const hitSources: BuildSources = {
llvmPath: earlyHit.llvmPath,
llvmSourcePath: earlyHit.llvmSourcePath ?? earlyHit.llvmPath,
irPath: earlyHit.irPath,
irSourcePath: earlyHit.irSourcePath,
llvmPathCurrent: earlyHit.llvmPathCurrent,
irPathCurrent: earlyHit.irPathCurrent,
};
// What the native build compiles and the cache publishes: hitSources, or their staged copies.
let nativeSources = hitSources;
let stagedDir: string | null = null;
let nativeInputPath = hitSources.llvmSourcePath;
let nativeProgramObject: NativeProgramObject | null = null;
const useRuntimePack =
opts.nativeProgramObject === true ||
usesPrecompiledRuntimePack(opts, earlyHit.native.backend);
if (useRuntimePack) {
if (earlyHit.native.backend !== "llvm") {
throw new InternalCompilerError(
"native program-object cache hit restored a non-LLVM translation unit",
);
}
try {
nativeProgramObject = await emitNativeProgramObject(
entryPath,
opts,
// A large program's LLVM exceeds the engine's string limit as one
// string; the emitter takes it in parts, as on a cold build.
await readTextParts(earlyHit.llvmPath),
profile,
);
nativeInputPath = nativeProgramObject.linkPath;
} catch (err) {
if (!(err instanceof NativeCodegenError)) throw err;
return {
ok: false,
diagnostics: [nativeCodegenDiag(err.diagnosticCode, err.message, entryPath)],
sourceTexts: new Map(),
};
}
}
try {
if (useRuntimePack) {
if (earlyHit.native.backend !== "llvm") {
throw new InternalCompilerError(
"native program-object cache hit restored a non-LLVM translation unit",
);
}
try {
nativeProgramObject = await emitNativeProgramObject(
entryPath,
opts,
// A large program's LLVM exceeds the engine's string limit as one
// string; the emitter takes it in parts, as on a cold build.
await readTextParts(hitSources.llvmSourcePath),
profile,
);
nativeInputPath = nativeProgramObject.linkPath;
} catch (err) {
if (!(err instanceof NativeCodegenError)) throw err;
return {
ok: false,
diagnostics: [nativeCodegenDiag(err.diagnosticCode, err.message, entryPath)],
sourceTexts: new Map(),
};
}
} else {
const staged = await stageDriverBuildSources(hitSources);
stagedDir = staged.dir;
nativeSources = staged.sources;
nativeInputPath = nativeSources.llvmSourcePath;
}
await compileExecutableNative(
earlyHit.native,
nativeInputPath,
@@ -1496,6 +1609,10 @@ async function compileTracked(
: async ({ dependencies }) => {
await publishEarlyExecutableCache(cacheRoot, executableCacheOptions, {
...earlyHit,
llvmPath: nativeSources.llvmSourcePath,
...(nativeSources.irSourcePath === undefined
? {}
: { irPath: nativeSources.irSourcePath }),
executableRestored: true,
nativeDependencies: dependencies,
frontend: earlyHit.frontend,
@@ -1503,6 +1620,7 @@ async function compileTracked(
},
nativeProgramObject?.partitionPaths,
profile?.generate !== undefined,
hitSources.llvmPath,
);
if (nativeProgramObject !== null && opts.nativeProgramObject === true) {
await rename(nativeProgramObject.linkPath, nativeProgramObject.artifactPath);
@@ -1532,6 +1650,8 @@ async function compileTracked(
nativeProgramObject.partitionPaths,
);
}
if (stagedDir === null) await installBuildSources(hitSources);
else await rm(stagedDir, { recursive: true, force: true });
}
await pruneBuildCache(cacheRoot);
return {
@@ -1564,7 +1684,17 @@ async function compileTracked(
)
: await prepareExecutableInput(entryPath, opts, ffi, buildPlatform, timing, warningSink);
if ("ok" in prepared) return prepared;
const { llvmSource, llvmPath, irPath, nativeFeatures, programSplit, sourceTexts } = prepared;
const buildSources: BuildSources = {
llvmPath: prepared.llvmPath,
llvmSourcePath: prepared.llvmSourcePath,
irPath: prepared.irPath,
irSourcePath: prepared.irSourcePath,
};
const { llvmSource, llvmPath, irPath } = prepared;
const { nativeFeatures, programSplit, sourceTexts } = prepared;
// What the native build compiles and the cache publishes: buildSources, or their staged copies.
let { llvmSourcePath, irSourcePath } = buildSources;
let stagedDir: string | null = null;
const backend = "llvm" as const;
await mkdir(dirname(opts.outPath), { recursive: true });
if (earlyCacheOptions === null) {
@@ -1596,10 +1726,14 @@ async function compileTracked(
sourceTexts,
};
}
} else {
const staged = await stageDriverBuildSources(buildSources);
stagedDir = staged.dir;
({ llvmSourcePath, irSourcePath } = staged.sources);
}
await compileExecutableNative(
nativeFeatures,
nativeProgramObject?.linkPath ?? llvmPath,
nativeProgramObject?.linkPath ?? llvmSourcePath,
opts.outPath,
opts.sanitize ?? false,
ffi,
@@ -1611,23 +1745,34 @@ async function compileTracked(
? undefined
: async ({ dependencies }) => {
await publishEarlyExecutableCache(cacheRoot, executableCacheOptions, {
llvmPath,
llvmPath: llvmSourcePath,
native: nativeFeatures,
executableRestored: true,
nativeDependencies: dependencies,
frontend: frontendInputs.snapshot(),
...(irPath === undefined ? {} : { irPath }),
...(irSourcePath === undefined ? {} : { irPath: irSourcePath }),
...performanceWarnings(warningSink),
});
publishedExecutable = true;
},
nativeProgramObject?.partitionPaths,
profile?.generate !== undefined,
llvmPath,
);
timing("native-link");
if (nativeProgramObject !== null && opts.nativeProgramObject === true) {
await rename(nativeProgramObject.linkPath, nativeProgramObject.artifactPath);
}
if (!publishedExecutable) {
await publishEarlyExecutableCache(cacheRoot, executableCacheOptions, {
llvmPath: llvmSourcePath,
native: nativeFeatures,
executableRestored: false,
frontend: frontendInputs.snapshot(),
...(irSourcePath === undefined ? {} : { irPath: irSourcePath }),
...performanceWarnings(warningSink),
}).catch(() => undefined);
}
} catch (err) {
if (err instanceof RuntimePackError) {
return { ok: false, diagnostics: [runtimePackDiagnostic(err, entryPath)], sourceTexts };
@@ -1649,18 +1794,8 @@ async function compileTracked(
nativeProgramObject.partitionPaths,
);
}
// The frontend process's private copy of the LLVM text.
if (isLlvmFile(llvmSource)) await rm(llvmSource.file, { force: true });
}
if (!publishedExecutable) {
await publishEarlyExecutableCache(cacheRoot, executableCacheOptions, {
llvmPath,
native: nativeFeatures,
executableRestored: false,
frontend: frontendInputs.snapshot(),
...(irPath === undefined ? {} : { irPath }),
...performanceWarnings(warningSink),
}).catch(() => undefined);
if (stagedDir === null) await installBuildSources(buildSources);
else await rm(stagedDir, { recursive: true, force: true });
}
await pruneBuildCache(cacheRoot);
timing("complete");
@@ -230,6 +230,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"http2.createSecureServerDynCb",
"tls.connect",
"tls.connectCb",
"tls.wrapSocket",
"tls.wrapSocketCtx",
// The CA-store surface: an unknown getCACertificates type throws
// Node's ERR_INVALID_ARG_VALUE TypeError; setDefaultCACertificates
// throws Node's ERR_CRYPTO_OPERATION_FAILED on a certificate-free
@@ -358,6 +360,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"readable.pushU",
"readable.pushDyn",
"readable.unshift",
"readable.unshiftDyn",
"readable.unshiftStr",
"readable.read",
"readable.readDyn",
@@ -370,6 +373,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"writable.writeStr",
"writable.writeU",
"writable.writeDyn",
"writable.writeEnc",
"writable.end",
"writable.uncork",
"stream.destroy",
@@ -985,4 +989,12 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"crypto.x509ValidFromStr",
"crypto.x509ValidTo",
"crypto.x509ValidToStr",
"crypto.createPrivateKey",
"crypto.createPublicKey",
"crypto.generateKeyPairSync",
"crypto.generateKeyPairCb",
"crypto.signOneshot",
"crypto.verifyOneshot",
"crypto.createSign",
"crypto.createVerify",
]);
+41 -2
View File
@@ -2182,6 +2182,20 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
result: VOID,
},
"tls.sockOnSession": { argTypes: [NETSOCKET_T, null, BOOL], result: VOID },
/** The 'secure' event: the handshake completed (after secureConnect on
* a client). Plain sockets never fire it. */
"tls.sockOnSecure": {
argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }, BOOL],
result: VOID,
},
/** new tls.TLSSocket(socket, options) in server mode: a new TLS socket
* takes over the socket's connection (wrapSocketCtx passes a literal's
* secureContext member apart from the runtime options record). The
* record reads at runtime: isServer, cert, key, ALPNProtocols,
* minVersion and maxVersion are implemented, the client mode and other
* documented members throw the runtime fence. MAY THROW. */
"tls.wrapSocket": { argTypes: [NETSOCKET_T, DYN], result: NETSOCKET_T },
"tls.wrapSocketCtx": { argTypes: [NETSOCKET_T, DYN, SECURECTX_T], result: NETSOCKET_T },
// createSecureContext({ cert, key }) mints the opaque SNI-answer handle.
/** tls.createSecureContext({ cert, key }) — parses the PEM pair into an
* opaque SecureContext handle (secureCtx kind) for SNI callbacks to
@@ -2857,8 +2871,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** WebCrypto (scr_webcrypto.c): each method takes its argument vector as
* one dyn array and answers a promise that argument and validation
* failures reject. CryptoKey values are native handles. */
"webcrypto.importKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"webcrypto.generateKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"webcrypto.importKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"webcrypto.exportKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"webcrypto.encrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"webcrypto.decrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
@@ -3877,7 +3891,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* readable.unpipe (recv[, dst]) → recv +1.
* writable.write/writeStr: (recv, chunk[, cb]) → below-hwm bool (cb
* MOVES when present, called after the user write completes).
* writable.end: (recv, flags[, chunk][, cb]) → recv +1. cork/uncork are
* writable.writeEnc: (recv, flags, chunk[, encoding][, cb]) — flags 1
* an encoding argument, 2 a callback (the encoding is then null, as
* Node's write shifts it).
* writable.end: (recv, flags[, chunk][, encoding][, cb]) → recv +1;
* flags 16: an encoding argument follows the dyn chunk. cork/uncork are
* void. stream.destroy/destroyErr: (recv[, err]) → recv +1.
* stream.prop: (recv, name-literal) → the flag/number the name asks
* for; stream.errored → Error|null union; readable.flowing →
@@ -4016,6 +4034,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"readable.pushU": { argTypes: [null, null], result: BOOL },
"readable.pushDyn": { argTypes: [null, DYN], result: BOOL },
"readable.unshift": { argTypes: [null, BYTES_U8], result: VOID },
"readable.unshiftDyn": { argTypes: [null, DYN], result: VOID },
"readable.unshiftStr": { argTypes: [null, STRING], result: VOID },
"stream.onDyn": { argTypes: [null, STRING, DYN, BOOL, BOOL], result: VOID },
"readable.readDyn": { argTypes: [null, DYN], result: DYN },
@@ -4027,6 +4046,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* Buffer.from(chunk, enc)); both carry the CANONICAL literal. */
"readable.pushStrEnc": { argTypes: [null, STRING, STRING], result: BOOL },
"readable.pushEncoding": { argTypes: [null, STRING], result: VOID },
/** The objectMode option's sides (readable, writable), applied right
* after construction; answers the receiver (+1, the chaining shape). */
"stream.objectMode": { argTypes: [null, BOOL, BOOL], result: VOID },
/** Whether a readable.nextChunkDyn step answered the end (the done
* marker; an object-mode stream can yield undefined). */
"stream.chunkDone": { argTypes: [DYN], result: BOOL },
/** for-await over a readable (the desugared loop's per-pass promise):
* +1 promise of the next chunk — buffered content, the EOF sentinel
* (empty Buffer / dyn undefined), or a rejection with the stream's
@@ -4080,6 +4105,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"writable.writeStr": { argTypes: [null, STRING], result: BOOL },
"writable.writeU": { argTypes: [null, null], result: BOOL },
"writable.writeDyn": { argTypes: [null, DYN], result: BOOL },
"writable.writeEnc": { argTypes: [null, F64, DYN], result: BOOL },
"writable.end": { argTypes: [null, F64], result: VOID },
"writable.cork": { argTypes: [null], result: VOID },
"writable.uncork": { argTypes: [null], result: VOID },
@@ -4665,6 +4691,19 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* body passes its undefined result on into a Buffer slot. Owned (+1);
* never throws. */
"bytes.absent": { argTypes: [], result: BYTES_U8 },
/** node:crypto asymmetric keys and signatures (scr_pki.c): checked-
* dynamic arguments, borrowed; KeyObject, Sign and Verify handles,
* Buffers and booleans as checked-dynamic results (+1). Node's argument
* and decoder errors throw (may-throw); generateKeyPairCb's callback
* receives (null, publicKey, privateKey) in a later macrotask. */
"crypto.createPrivateKey": { argTypes: [DYN], result: DYN },
"crypto.createPublicKey": { argTypes: [DYN], result: DYN },
"crypto.generateKeyPairSync": { argTypes: [DYN, DYN], result: DYN },
"crypto.generateKeyPairCb": { argTypes: [DYN, DYN, DYN], result: VOID },
"crypto.signOneshot": { argTypes: [DYN, 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 },
/** The Buffer brand of a Uint8Array source onto a fresh Uint8Array
* result (map and filter of a Buffer return a Buffer). */
"bytes.copyBrand": { argTypes: [BYTES_U8, BYTES_U8], result: VOID },
@@ -216,6 +216,8 @@ const ABORT_SIGNAL_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
/** Entry points that link the stream unit (with STREAM_LIB_PREFIXES). */
const STREAM_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"child.stdioStream",
"stream.chunkDone",
"stream.objectMode",
"console.bind",
"crypto.hashStream",
"crypto.hashStreamPiped",
@@ -624,6 +626,63 @@ const rule = (r: RuntimeFeatureRule): RuntimeFeatureRule => r;
/* The HTTP, HTTPS, HTTP/2 and TLS surfaces, which link the net unit. */
const NET_PREFIXES: readonly string[] = ["net.", "http.", "tls.", "https.", "http2."];
/** The scr_pki.c entry points (the pki unit). */
export const PKI_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"crypto.createPrivateKey",
"crypto.createPublicKey",
"crypto.createSign",
"crypto.createVerify",
"crypto.generateKeyPairCb",
"crypto.generateKeyPairSync",
"crypto.signOneshot",
"crypto.verifyOneshot",
]);
/** The WebCrypto algorithms scr_webcrypto_pki.c implements over the pki
* unit (lowercased). */
const WEBCRYPTO_ASYMMETRIC_NAMES: ReadonlySet<string> = new Set([
"ecdsa",
"ed25519",
"rsa-pss",
"rsassa-pkcs1-v1_5",
]);
/** Whether a string literal in the module names Ed25519, without regard
* to case ("ed25519" for node:crypto, "Ed25519" for WebCrypto): the
* Ed25519 unit links then, alongside the pki unit. */
export function moduleNamesEd25519(mod: IrModule): boolean {
let named = false;
everyModuleNode(mod, {
expr: (node) => {
if (node.kind === "strLit" && node.value.toLowerCase() === "ed25519") named = true;
return !named;
},
stmt: () => !named,
type: () => true,
});
return named;
}
/** Whether the module calls WebCrypto and a string literal in it names one
* of the asymmetric algorithms (matched without regard to case, as
* normalizeAlgorithm matches names): scr_webcrypto_pki.c and the pki unit
* link then. */
export function moduleUsesWebCryptoAsymmetric(mod: IrModule): boolean {
let webcrypto = false;
let named = false;
everyModuleNode(mod, {
expr: (node) => {
if (node.kind === "libCall" && node.fn.startsWith("webcrypto.")) webcrypto = true;
if (node.kind === "strLit" && WEBCRYPTO_ASYMMETRIC_NAMES.has(node.value.toLowerCase()))
named = true;
return !(webcrypto && named);
},
stmt: () => !(webcrypto && named),
type: () => true,
});
return webcrypto && named;
}
/** One rule per runtime feature, sorted by name. A new feature is an
* entry here and its line in noRuntimeFeatures; RuntimeFeatures, the scan
* and its early exit derive from this table. The lists above keep one
@@ -840,6 +899,12 @@ const RUNTIME_FEATURE_RULES = {
/** perf_hooks event-loop metrics (eventLoopUtilization,
* monitorEventLoopDelay): scr_perf_loop.c. */
perfLoop: rule({ fns: ["perf.idleTime"], prefixes: ["perf.loop", "perf.eld"] }),
/** scr_pki.c over the mbedTLS archive: asymmetric KeyObjects and
* signatures. */
pki: rule({ initial: moduleUsesWebCryptoAsymmetric, fns: [...PKI_LIB_FNS] }),
/** scr_ed25519.c (the vendored libsodium Ed25519), linked with pki: a
* string literal in the module names Ed25519. */
ed25519: rule({ initial: moduleNamesEd25519 }),
processEvents: rule({ fns: [...PROCESS_EVENT_LIB_FNS, "util.styleText"], types: ["procStream"] }),
/** scr_url_legacy.c parses and stringifies queries. */
qs: rule({ fns: [...QS_LIB_FNS, ...LEGACY_URL_LIB_FNS] }),
@@ -947,6 +1012,8 @@ function noRuntimeFeatures(): RuntimeFeatures {
nodeTest: false,
parseArgs: false,
perfLoop: false,
pki: false,
ed25519: false,
processEvents: false,
qs: false,
regex: false,
+7 -2
View File
@@ -4109,6 +4109,7 @@ function validateFunction(
e.fn === "writable.write" ||
e.fn === "writable.writeStr" ||
e.fn === "writable.writeU" ||
e.fn === "writable.writeEnc" ||
e.fn === "writable.end" ||
e.fn === "readable.unpipe" ||
e.fn === "rl.create" ||
@@ -5492,6 +5493,7 @@ function validateFunction(
e.fn.startsWith("passthrough.") ||
e.fn === "stream.destroy" ||
e.fn === "stream.destroyErr" ||
e.fn === "stream.objectMode" ||
e.fn === "stream.iteratorClose" ||
e.fn === "stream.prop" ||
e.fn === "stream.errored" ||
@@ -5782,6 +5784,7 @@ function validateFunction(
e.fn === "writable.end" ||
e.fn === "readable.setEncoding" ||
e.fn === "readable.pushEncoding" ||
e.fn === "stream.objectMode" ||
e.fn === "stream.destroy" ||
e.fn === "stream.destroyErr" ||
e.fn === "stream.iteratorClose"
@@ -5898,8 +5901,10 @@ function validateFunction(
const chunkT = e.args[2]?.type;
const ok =
chunkT !== undefined &&
((chunkT.kind === "bytes" && chunkT.elem === "u8") || chunkT.kind === "string");
if (!ok) err(`libCall emitter.emitData chunk must be bytes<u8> or string`, e.loc);
((chunkT.kind === "bytes" && chunkT.elem === "u8") ||
chunkT.kind === "string" ||
chunkT.kind === "dyn");
if (!ok) err(`libCall emitter.emitData chunk must be bytes<u8>, string or dyn`, e.loc);
if (e.type.kind !== "bool") err(`libCall emitter.emitData must be bool`, e.loc);
return true;
}
@@ -52,6 +52,13 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [
["tls.", "the node:tls surface"],
["crypto.cipherNew", "crypto.createCipheriv and createDecipheriv"],
["crypto.ecdh", "crypto.createECDH and ECDH.convertKey"],
["crypto.createPrivateKey", "asymmetric KeyObjects, generateKeyPair, sign and verify"],
["crypto.createPublicKey", "asymmetric KeyObjects, generateKeyPair, sign and verify"],
["crypto.createSign", "asymmetric KeyObjects, generateKeyPair, sign and verify"],
["crypto.createVerify", "asymmetric KeyObjects, generateKeyPair, sign and verify"],
["crypto.generateKeyPair", "asymmetric KeyObjects, generateKeyPair, sign and verify"],
["crypto.signOneshot", "asymmetric KeyObjects, generateKeyPair, sign and verify"],
["crypto.verifyOneshot", "asymmetric KeyObjects, generateKeyPair, sign and verify"],
["webcrypto.", "WebCrypto (crypto.subtle)"],
["fs.watch", "fs.watch"],
["watcher.", "fs.watch"],
@@ -159,6 +166,7 @@ export function moduleLibAsyncSurface(mod: IrModule): { surface: string; loc: Sr
[features.fsWatch, "fs.watch"],
[features.stream, "the node:stream surface"],
[features.tls, "the node:tls surface"],
[features.pki, "asymmetric KeyObjects, generateKeyPair, sign and verify"],
[features.fetch, "fetch"],
[features.eventTarget, "the EventTarget surface"],
[features.nodeTest, "the node:test surface"],
+2
View File
@@ -110,6 +110,8 @@ async function main(): Promise<number> {
tls: false,
cipher: false,
ecdh: false,
pki: false,
ed25519: false,
webcrypto: false,
perfLoop: false,
tlsCa: false,
+2
View File
@@ -161,6 +161,8 @@ function validFeatures(value: unknown): value is NativeLinkFeatures {
"tlsCa",
"cipher",
"ecdh",
"pki",
"ed25519",
"webcrypto",
"perfLoop",
].every((name) => typeof features[name] === "boolean");
@@ -120,6 +120,8 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
tlsCa: false,
cipher: false,
ecdh: false,
pki: false,
ed25519: false,
webcrypto: false,
perfLoop: false,
nativeFetch: false,
+17 -33
View File
@@ -498,7 +498,7 @@
"name": "crypto.createDiffieHellman",
"status": "unsupported",
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
"note": "Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, and createECDH"
},
{
"id": "node-builtin.crypto.createDiffieHellmanGroup",
@@ -506,7 +506,7 @@
"name": "crypto.createDiffieHellmanGroup",
"status": "unsupported",
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
"note": "Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, and createECDH"
},
{
"id": "node-builtin.crypto.createECDH",
@@ -530,17 +530,13 @@
"id": "node-builtin.crypto.createPrivateKey",
"kind": "node-builtin",
"name": "crypto.createPrivateKey",
"status": "unsupported",
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
"status": "static"
},
{
"id": "node-builtin.crypto.createPublicKey",
"kind": "node-builtin",
"name": "crypto.createPublicKey",
"status": "unsupported",
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
"status": "static"
},
{
"id": "node-builtin.crypto.createSecretKey",
@@ -552,17 +548,13 @@
"id": "node-builtin.crypto.createSign",
"kind": "node-builtin",
"name": "crypto.createSign",
"status": "unsupported",
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
"status": "static"
},
{
"id": "node-builtin.crypto.createVerify",
"kind": "node-builtin",
"name": "crypto.createVerify",
"status": "unsupported",
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
"status": "static"
},
{
"id": "node-builtin.crypto.diffieHellman",
@@ -570,7 +562,7 @@
"name": "crypto.diffieHellman",
"status": "unsupported",
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
"note": "Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, and createECDH"
},
{
"id": "node-builtin.crypto.generateKey",
@@ -582,17 +574,13 @@
"id": "node-builtin.crypto.generateKeyPair",
"kind": "node-builtin",
"name": "crypto.generateKeyPair",
"status": "unsupported",
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
"status": "static"
},
{
"id": "node-builtin.crypto.generateKeyPairSync",
"kind": "node-builtin",
"name": "crypto.generateKeyPairSync",
"status": "unsupported",
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
"status": "static"
},
{
"id": "node-builtin.crypto.generateKeySync",
@@ -614,7 +602,7 @@
"name": "crypto.getDiffieHellman",
"status": "unsupported",
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
"note": "Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, and createECDH"
},
{
"id": "node-builtin.crypto.getRandomValues",
@@ -658,7 +646,7 @@
"name": "crypto.privateDecrypt",
"status": "unsupported",
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
"note": "Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, and createECDH"
},
{
"id": "node-builtin.crypto.privateEncrypt",
@@ -666,7 +654,7 @@
"name": "crypto.privateEncrypt",
"status": "unsupported",
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
"note": "Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, and createECDH"
},
{
"id": "node-builtin.crypto.publicDecrypt",
@@ -674,7 +662,7 @@
"name": "crypto.publicDecrypt",
"status": "unsupported",
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
"note": "Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, and createECDH"
},
{
"id": "node-builtin.crypto.publicEncrypt",
@@ -682,7 +670,7 @@
"name": "crypto.publicEncrypt",
"status": "unsupported",
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
"note": "Diffie-Hellman and RSA encryption have no lowering — the asymmetric crypto surface is RSA and EC KeyObjects (generateKeyPair, createPrivateKey, createPublicKey), sign, verify, createSign, createVerify, and createECDH"
},
{
"id": "node-builtin.crypto.randomBytes",
@@ -744,9 +732,7 @@
"id": "node-builtin.crypto.sign",
"kind": "node-builtin",
"name": "crypto.sign",
"status": "unsupported",
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
"status": "static"
},
{
"id": "node-builtin.crypto.timingSafeEqual",
@@ -758,9 +744,7 @@
"id": "node-builtin.crypto.verify",
"kind": "node-builtin",
"name": "crypto.verify",
"status": "unsupported",
"code": "SC2020",
"note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics"
"status": "static"
},
{
"id": "node-builtin.dgram",
@@ -5718,7 +5702,7 @@
"kind": "stdlib",
"name": "globalThis.crypto.subtle",
"status": "static",
"note": "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, wrapKey and unwrapKey with AES-KW or those ciphers, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused"
"note": "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, wrapKey and unwrapKey of secret keys with AES-KW or those ciphers, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, generateKey, importKey, exportKey, sign and verify for ECDSA, Ed25519, RSASSA-PKCS1-v1_5 and RSA-PSS, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused"
},
{
"id": "syntax.async-generators",
@@ -222,7 +222,8 @@ test("bodies that suspend only at their own awaits compile to stackless coroutin
});
const framedSources = {
// Entering a store makes the frame isolate the AsyncLocalStorage context.
// Entering a store needs no frame: the body runs in its caller's context,
// as Node runs an async body's synchronous prefix.
context: `
import { AsyncLocalStorage } from "node:async_hooks";
const store = new AsyncLocalStorage<string>();
@@ -238,7 +239,7 @@ async function leaf(x: number): Promise<number> {
}`,
};
test("fiberless bodies keep their frame where it isolates a context or a stack", async () => {
test("fiberless bodies keep their frame only where it isolates a stack", async () => {
for (const [name, prefix] of Object.entries(framedSources)) {
const llvm = await emit(`${prefix}
async function slow(x: number): Promise<number> {
@@ -262,9 +263,12 @@ async function main(): Promise<void> {
void main();
`);
for (const symbol of ["sc_ai_leaf", "sc_aw_leaf", "sc_as_leaf"]) {
expect(definition(llvm, symbol), `${name} ${symbol}`).toContain(
"call void @scr_async_inline_enter(ptr %frame)",
);
const body = definition(llvm, symbol);
if (name === "stack")
expect(body, `${name} ${symbol}`).toContain(
"call void @scr_async_inline_enter(ptr %frame)",
);
else expect(body, `${name} ${symbol}`).not.toContain("@scr_async_inline_enter");
}
}
});
@@ -378,7 +378,7 @@ done:
expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"]));
expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"]));
expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]);
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v25");
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v26");
const clangExe = join(dir, "clang-program");
const linkDriver = join(dir, "clang-driver.c");
@@ -712,7 +712,7 @@ done:
.split("\n");
expect(undefinedSymbols).toContain("_sf_scale");
expect(undefinedSymbols).toContain("_sf_callback_mix");
expect(undefinedSymbols).toContain("_scr_runtime_abi_v25");
expect(undefinedSymbols).toContain("_scr_runtime_abi_v26");
});
test("helper and clang object paths retain the same outbound FFI C ABI", async () => {
@@ -0,0 +1,11 @@
{
"order": [
"<repo>/tests/corpus/6822-jsx-classic-runtime/html.ts",
"<repo>/tests/corpus/6822-jsx-classic-runtime/list.jsx",
"<repo>/tests/corpus/6822-jsx-classic-runtime/page.tsx",
"<repo>/tests/corpus/6822-jsx-classic-runtime/factory.ts",
"<repo>/tests/corpus/6822-jsx-classic-runtime/pragma.tsx",
"<repo>/tests/corpus/6822-jsx-classic-runtime/main.ts"
],
"diags": []
}
@@ -0,0 +1,12 @@
{
"order": [
"<repo>/tests/corpus/6824-jsx-automatic-runtime/runtime/jsx-runtime.ts",
"<repo>/tests/corpus/6824-jsx-automatic-runtime/alt/jsx-runtime.ts",
"<repo>/tests/corpus/6824-jsx-automatic-runtime/alt.tsx",
"<repo>/tests/corpus/6824-jsx-automatic-runtime/runtime/index.ts",
"<repo>/tests/corpus/6824-jsx-automatic-runtime/page.tsx",
"<repo>/tests/corpus/6824-jsx-automatic-runtime/render.ts",
"<repo>/tests/corpus/6824-jsx-automatic-runtime/main.ts"
],
"diags": []
}
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/tests/corpus/6825-jsx-automatic-development/runtime/jsx-dev-runtime.ts",
"<repo>/tests/corpus/6825-jsx-automatic-development/view.tsx",
"<repo>/tests/corpus/6825-jsx-automatic-development/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6870-error-constructor-values.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6871-worker-data-destructuring.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7080-crypto-ed25519.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7081-webcrypto-ed25519.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7082-crypto-key-objects.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7083-crypto-sign-verify.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7084-crypto-key-pairs.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7085-webcrypto-ecdsa-rsa.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7086-webcrypto-ecdsa-rsa-errors.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7087-tls-socket-wrap.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7088-tls-socket-connect-tunnel.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7089-http-host-header-tunnels.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7094-tls-socket-failed-handshakes.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7106-async-prefix-enter-with.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7107-for-await-context.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7108-generator-step-context.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7180-generic-local-binding-early-uses.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7181-generic-module-binding-tdz.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7182-generic-binding-direct-calls.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7240-objectmode-streams.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7241-objectmode-streams.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7242-objectmode-dynamic.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7243-for-await-close-order.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7244-objectmode-subclasses.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7245-writable-callback-order.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7280-named-reads-hole-receivers.ts"
],
"diags": []
}
@@ -0,0 +1,17 @@
{
"order": [
"<repo>/tests/diagnostics/jsx-factory-mismatch/view.tsx",
"<repo>/tests/diagnostics/jsx-factory-mismatch/main.ts"
],
"diags": [
{
"code": "SC0001",
"message": "Argument of type 'number' is not assignable to parameter of type 'string'. (in the call this JSX lowers to)",
"loc": {
"file": "<repo>/tests/diagnostics/jsx-factory-mismatch/view.tsx",
"start": 337,
"end": 342
}
}
]
}

Some files were not shown because too many files have changed in this diff Show More