mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-04 08:04:37 +08:00
Merge main
This commit is contained in:
@@ -8,6 +8,7 @@ node_modules/
|
||||
!tests/fixtures/npm/node_modules/
|
||||
!tests/corpus/*/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/
|
||||
!tests/coverage-fixtures/wasm/node_modules/
|
||||
!tests/fixtures/npm-static/nested/inner/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/*/node_modules/
|
||||
!tests/fixtures/npm/store/*/node_modules/
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"packages/compiler/src/backend/llvm/**",
|
||||
"packages/compiler/src/backend/loop-polls.test.ts",
|
||||
"packages/compiler/src/backend/may-throw.test.ts",
|
||||
"packages/compiler/src/backend/stack-observations.test.ts",
|
||||
"packages/compiler/src/ir/**",
|
||||
"packages/compiler/src/library/*.test.ts",
|
||||
"packages/compiler/test/**",
|
||||
|
||||
@@ -133,6 +133,9 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
|
||||
<dt><code>--explain-dynamic[=<name>]</code></dt>
|
||||
<dd>Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as <code>x</code>, <code>Point</code>, or <code>Point.x</code>, show only the matching units. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
|
||||
|
||||
<dt><code>--wasm <file.wasm></code></dt>
|
||||
<dd>Coverage only. Report a WebAssembly module the program loads from a path its sources do not spell out. The report already lists modules whose paths the compiler resolves, modules inlined as base64 literals, and <code>.wasm</code> files shipped by packages that use WebAssembly; a <code>scriptc.wasm</code> array in <code>package.json</code> adds files the same way as this flag. Each module is listed with its size, whether it validates, and the WebAssembly proposals it uses. scriptc does not compile WebAssembly modules to native code yet. Repeatable.</dd>
|
||||
|
||||
<dt><code>--sanitize</code></dt>
|
||||
<dd>Build an executable with AddressSanitizer and a reference-count audit at exit, the same checks scriptc's own test suite uses. Requires a C compiler and is available only for host builds. Not supported with <code>--emit=asm</code> or <code>--emit=obj</code>.</dd>
|
||||
|
||||
|
||||
@@ -120,6 +120,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/builtin-apis/synchronous-iteration.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/iterator-helpers.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/regular-expressions.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/object-freezing.mdx</include>
|
||||
@@ -238,6 +240,10 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/native-addons.mdx</include>
|
||||
|
||||
## WebAssembly modules
|
||||
|
||||
<include>../limitations/webassembly-modules.mdx</include>
|
||||
|
||||
## WASI target
|
||||
|
||||
<include>../limitations/wasi-target.mdx</include>
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Crypto:** Hashing, HMAC, PBKDF2, and HKDF support every digest `crypto.getHashes()` lists in Node 24, with OpenSSL's aliases and object identifiers. Hash and Hmac accept every Buffer encoding and any TypedArray or DataView, `createHash` and `hash.copy` take `outputLength`, and `crypto.hash` takes its options object; HMAC, PBKDF2, and HKDF reject the SHAKE and KMAC digests with Node's errors. Hash and Hmac values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, and listened to, and they report Node's errors after the stream flushes. `copy()` of a Hash whose stream flushed, and stream use of a value after its stream closed, 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. `randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, and GCM modes and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers, cipher streams, and wasm32-wasi builds are refused. Decrypting chacha20-poly1305 without an authentication tag fails in `final()`; Node 24.22 returns unverified plaintext. After `update()` fails, for example past the AES-GCM limit of 2^36 - 32 bytes per message, every later call on that cipher throws; Node still lets `final()` run. An output encoding that is a function or an instance of a class defining `toString` throws a coded SC2020 error. Public-key operations remain unsupported. Native derivations finish before their deferred callbacks, so computation blocks the caller.
|
||||
- **Crypto:** Hashing, HMAC, PBKDF2, and HKDF support every digest `crypto.getHashes()` lists in Node 24, with OpenSSL's aliases and object identifiers. Hash and Hmac accept every Buffer encoding and any TypedArray or DataView, `createHash` and `hash.copy` take `outputLength`, and `crypto.hash` takes its options object; HMAC, PBKDF2, and HKDF reject the SHAKE and KMAC digests with Node's errors. Hash and Hmac values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, and listened to, and they report Node's errors after the stream flushes. `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. `randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, and GCM modes and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers, cipher streams, and wasm32-wasi builds are refused. Decrypting chacha20-poly1305 without an authentication tag fails in `final()`; Node 24.22 returns unverified plaintext. After `update()` fails, for example past the AES-GCM limit of 2^36 - 32 bytes per message, every later call on that cipher throws; Node still lets `final()` run. An output encoding that is a function or an instance of a class defining `toString` throws a coded SC2020 error. Public-key operations remain unsupported. Native derivations finish before their deferred callbacks, so computation blocks the caller.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. `flatMap` callbacks return arrays or generators. Iterator helpers on string iterators, `Iterator.from`, the `Iterator` global, calling `return()` on a helper object in TypeScript, printing helper objects, and helper calls on untyped iterators in JavaScript are unsupported.
|
||||
@@ -1 +1 @@
|
||||
- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, symbols, server handles, and unions of these reference types by identity. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map<unknown, unknown>` and `Set<unknown>`, and promises requiring payload adapters, cannot pass into these slots. `Promise<unknown>` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. Detached collection methods and iterator helper methods such as `.map()` and `.take()` are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported.
|
||||
- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, symbols, server handles, and unions of these reference types by identity. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map<unknown, unknown>` and `Set<unknown>`, and promises requiring payload adapters, cannot pass into these slots. `Promise<unknown>` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. They also support the iterator helper methods, such as `.map()` and `.take()`. Detached collection methods are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
Compiled programs can't load WebAssembly modules yet. Code that only mentions the `WebAssembly` types type-checks, including packages whose declarations name them, and `typeof WebAssembly` answers as it does in Node.js. Every use of the API itself is rejected: `WebAssembly.compile`, `instantiate` and `validate`, `new WebAssembly.Module` and the other constructors, `instanceof` checks, and reads of `globalThis.WebAssembly`. TypeScript sources fail to build with SC2020 at each use, and JavaScript sources throw that diagnostic when the statement runs.
|
||||
|
||||
`scriptc coverage` lists the modules a program would load, with their size, whether they validate, and the WebAssembly proposals they use. It finds modules at paths the compiler resolves, modules inlined as base64 literals, `.wasm` files shipped by packages that use WebAssembly, and files named with [`--wasm`](/docs/cli) or a `scriptc.wasm` array in `package.json`.
|
||||
@@ -2,6 +2,6 @@
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"artifactVersion": "2f8d15fdac84f8c4852a",
|
||||
"artifactVersion": "ee72820d45a79aa154f0",
|
||||
"rowCount": 3880
|
||||
}
|
||||
|
||||
@@ -330,8 +330,8 @@
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 12,
|
||||
"refused": 0,
|
||||
"not-implemented": 80,
|
||||
"refused": 1,
|
||||
"not-implemented": 79,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -36590,9 +36590,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"tiers": {
|
||||
"static": {
|
||||
"audit-partial": 1164,
|
||||
"verify-gap": 2163,
|
||||
"replace-refusal": 40,
|
||||
"verify-gap": 2162,
|
||||
"replace-refusal": 41,
|
||||
"classify": 2,
|
||||
"implement": 85
|
||||
}
|
||||
@@ -6736,6 +6736,8 @@
|
||||
"source": "compiler-dedicated:crypto.createHash",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -6774,6 +6776,8 @@
|
||||
"source": "compiler-dedicated:crypto.createHmac",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -7371,6 +7375,8 @@
|
||||
"source": "compiler-dedicated:crypto.hash",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -7494,6 +7500,8 @@
|
||||
"source": "compiler-dedicated:crypto.pbkdf2",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -7532,6 +7540,8 @@
|
||||
"source": "compiler-dedicated:crypto.pbkdf2Sync",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -9012,6 +9022,8 @@
|
||||
"source": "compiler-dedicated:Hash",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -9050,6 +9062,8 @@
|
||||
"source": "compiler-dedicated:hash.copy",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -9088,6 +9102,8 @@
|
||||
"source": "compiler-dedicated:hash.digest",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -9126,6 +9142,8 @@
|
||||
"source": "compiler-dedicated:hash.update",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -9164,6 +9182,8 @@
|
||||
"source": "compiler-dedicated:Hmac",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -9202,6 +9222,8 @@
|
||||
"source": "compiler-dedicated:hmac.digest",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -9240,6 +9262,8 @@
|
||||
"source": "compiler-dedicated:hmac.update",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -34159,11 +34183,11 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:globals",
|
||||
"status": "refused",
|
||||
"action": "replace-refusal",
|
||||
"verification": "explicit-refusal",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:unsupported:stdlib.webassembly",
|
||||
"tests": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,8 +342,8 @@
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 12,
|
||||
"refused": 0,
|
||||
"not-implemented": 80,
|
||||
"refused": 1,
|
||||
"not-implemented": 79,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -19861,6 +19861,8 @@
|
||||
"tests/corpus/5760-crypto-hash-stream.ts",
|
||||
"tests/corpus/5761-crypto-hash-stream-states.ts",
|
||||
"tests/corpus/5762-crypto-hash-stream.cjs",
|
||||
"tests/corpus/5768-crypto-hash-unknown.ts",
|
||||
"tests/corpus/5769-crypto-hash-unknown.cjs",
|
||||
"tests/corpus/6204-crypto-get-random-values.ts",
|
||||
"tests/corpus/6205-crypto-get-random-values.js",
|
||||
"tests/corpus/node24-crypto-kdf-errors.ts",
|
||||
@@ -20404,6 +20406,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.createHash",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -20441,6 +20445,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.createHmac",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -21046,6 +21052,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.hash",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -21164,6 +21172,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.pbkdf2",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -21201,6 +21211,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.pbkdf2Sync",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -23101,6 +23113,8 @@
|
||||
"evidence": "compiler-dedicated:Hash",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -23138,6 +23152,8 @@
|
||||
"evidence": "compiler-dedicated:hash.copy",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -23175,6 +23191,8 @@
|
||||
"evidence": "compiler-dedicated:hash.digest",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -23212,6 +23230,8 @@
|
||||
"evidence": "compiler-dedicated:hash.update",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -23249,6 +23269,8 @@
|
||||
"evidence": "compiler-dedicated:Hmac",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -23286,6 +23308,8 @@
|
||||
"evidence": "compiler-dedicated:hmac.digest",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -23323,6 +23347,8 @@
|
||||
"evidence": "compiler-dedicated:hmac.update",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -25037,6 +25063,8 @@
|
||||
"evidence": "compiler-dedicated:crypto.pbkdf2",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -28120,6 +28148,8 @@
|
||||
"evidence": "compiler-dedicated:Hash",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -28170,6 +28200,8 @@
|
||||
"evidence": "compiler-dedicated:Hmac",
|
||||
"tests": [
|
||||
"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",
|
||||
@@ -53537,8 +53569,8 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:globals"
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:stdlib.webassembly"
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
|
||||
@@ -203,6 +203,10 @@ function featureOf(row, entries, parentSignature) {
|
||||
|
||||
function symbolCandidates(row) {
|
||||
const result = new Set([row.apiSymbol]);
|
||||
// A global's own row (`WebAssembly`) matches the compiler's
|
||||
// `globalThis.<name>` entries.
|
||||
if (row.chapter === "globals" && row.apiSymbol && !row.apiSymbol.includes("."))
|
||||
result.add(`globalThis.${row.apiSymbol}`);
|
||||
const aliases = [
|
||||
["fsPromises.", "fs/promises."],
|
||||
["dnsPromises.", "dns/promises."],
|
||||
|
||||
@@ -389,7 +389,7 @@
|
||||
{ "symbols": ["assert.deepEqual", "assert.notDeepEqual"], "status": "partial", "evidence": ["tests/corpus/5513-assert-deep-equal-loose.js", "tests/corpus/5514-assert-deep-equal-typed.ts", "tests/corpus/5516-assert-deep-equal-edges.js"] },
|
||||
{ "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts"] },
|
||||
{ "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/fixtures/node-types/cipher.ts"] },
|
||||
{ "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/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.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.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] },
|
||||
|
||||
+134
@@ -13,6 +13,9 @@
|
||||
* they MERGE as overloads instead of colliding — the Timeout handle maps
|
||||
* to the numeric timer id either way, and .unref()/.ref()/.hasRef() are
|
||||
* loop-liveness bookkeeping).
|
||||
* 4. The WebAssembly namespace and BufferSource, which the DOM library
|
||||
* supplies in ordinary TypeScript projects (types only; every reached
|
||||
* use of the API is refused at its site).
|
||||
*
|
||||
* This file ships to EVERY program scriptc builds — the lowering program
|
||||
* and preflight's project-world second-chance program alike (a project
|
||||
@@ -108,3 +111,134 @@ declare function clearTimeout(handle?: Timeout | number | null | undefined): voi
|
||||
* baked result is exactly what Node would compute at runtime (the
|
||||
* differential harness verifies this with a `comptime = (f) => f()` shim). */
|
||||
declare function comptime<T>(compute: () => T): T;
|
||||
|
||||
/* The JavaScript WebAssembly API, as the DOM library declares it (scriptc's
|
||||
* ES-only type world never loads lib.dom, and @types/node leaves it to
|
||||
* lib.dom). Packages whose declarations name these types (quickjs-wasi,
|
||||
* photon-node, ...) typecheck against them. These are types only: the
|
||||
* type mapper gives every type declared here the checked-dynamic
|
||||
* representation, and the lowerer refuses each reached use of the API with
|
||||
* a located diagnostic (frontend/lowering/builtins/webassembly.ts).
|
||||
* BufferSource admits every ArrayBufferView, as lib.dom did before
|
||||
* TypeScript 5.7: Node's Buffer types are views over ArrayBufferLike. */
|
||||
type BufferSource = ArrayBufferView | ArrayBuffer;
|
||||
declare namespace WebAssembly {
|
||||
interface CompileError extends Error {}
|
||||
var CompileError: {
|
||||
prototype: CompileError;
|
||||
new (message?: string): CompileError;
|
||||
(message?: string): CompileError;
|
||||
};
|
||||
interface Global<T extends ValueType = ValueType> {
|
||||
value: ValueTypeMap[T];
|
||||
valueOf(): ValueTypeMap[T];
|
||||
}
|
||||
var Global: {
|
||||
prototype: Global;
|
||||
new <T extends ValueType = ValueType>(
|
||||
descriptor: GlobalDescriptor<T>,
|
||||
v?: ValueTypeMap[T],
|
||||
): Global<T>;
|
||||
};
|
||||
interface Instance {
|
||||
readonly exports: Exports;
|
||||
}
|
||||
var Instance: {
|
||||
prototype: Instance;
|
||||
new (module: Module, importObject?: Imports): Instance;
|
||||
};
|
||||
interface LinkError extends Error {}
|
||||
var LinkError: {
|
||||
prototype: LinkError;
|
||||
new (message?: string): LinkError;
|
||||
(message?: string): LinkError;
|
||||
};
|
||||
interface Memory {
|
||||
readonly buffer: ArrayBuffer;
|
||||
grow(delta: number): number;
|
||||
}
|
||||
var Memory: {
|
||||
prototype: Memory;
|
||||
new (descriptor: MemoryDescriptor): Memory;
|
||||
};
|
||||
interface Module {}
|
||||
var Module: {
|
||||
prototype: Module;
|
||||
new (bytes: BufferSource): Module;
|
||||
customSections(moduleObject: Module, sectionName: string): ArrayBuffer[];
|
||||
exports(moduleObject: Module): ModuleExportDescriptor[];
|
||||
imports(moduleObject: Module): ModuleImportDescriptor[];
|
||||
};
|
||||
interface RuntimeError extends Error {}
|
||||
var RuntimeError: {
|
||||
prototype: RuntimeError;
|
||||
new (message?: string): RuntimeError;
|
||||
(message?: string): RuntimeError;
|
||||
};
|
||||
interface Table {
|
||||
readonly length: number;
|
||||
get(index: number): any;
|
||||
grow(delta: number, value?: any): number;
|
||||
set(index: number, value?: any): void;
|
||||
}
|
||||
var Table: {
|
||||
prototype: Table;
|
||||
new (descriptor: TableDescriptor, value?: any): Table;
|
||||
};
|
||||
interface GlobalDescriptor<T extends ValueType = ValueType> {
|
||||
mutable?: boolean;
|
||||
value: T;
|
||||
}
|
||||
interface MemoryDescriptor {
|
||||
initial: number;
|
||||
maximum?: number;
|
||||
shared?: boolean;
|
||||
}
|
||||
interface ModuleExportDescriptor {
|
||||
kind: ImportExportKind;
|
||||
name: string;
|
||||
}
|
||||
interface ModuleImportDescriptor {
|
||||
kind: ImportExportKind;
|
||||
module: string;
|
||||
name: string;
|
||||
}
|
||||
interface TableDescriptor {
|
||||
element: TableKind;
|
||||
initial: number;
|
||||
maximum?: number;
|
||||
}
|
||||
interface ValueTypeMap {
|
||||
anyfunc: Function;
|
||||
externref: any;
|
||||
f32: number;
|
||||
f64: number;
|
||||
i32: number;
|
||||
i64: bigint;
|
||||
v128: never;
|
||||
}
|
||||
interface WebAssemblyInstantiatedSource {
|
||||
instance: Instance;
|
||||
module: Module;
|
||||
}
|
||||
type ImportExportKind = "function" | "global" | "memory" | "table";
|
||||
type TableKind = "anyfunc" | "externref";
|
||||
type ExportValue = Function | Global | Memory | Table;
|
||||
type Exports = Record<string, ExportValue>;
|
||||
type ImportValue = ExportValue | number;
|
||||
type Imports = Record<string, ModuleImports>;
|
||||
type ModuleImports = Record<string, ImportValue>;
|
||||
type ValueType = keyof ValueTypeMap;
|
||||
function compile(bytes: BufferSource): Promise<Module>;
|
||||
function compileStreaming(source: Response | PromiseLike<Response>): Promise<Module>;
|
||||
function instantiate(
|
||||
bytes: BufferSource,
|
||||
importObject?: Imports,
|
||||
): Promise<WebAssemblyInstantiatedSource>;
|
||||
function instantiate(moduleObject: Module, importObject?: Imports): Promise<Instance>;
|
||||
function instantiateStreaming(
|
||||
source: Response | PromiseLike<Response>,
|
||||
importObject?: Imports,
|
||||
): Promise<WebAssemblyInstantiatedSource>;
|
||||
function validate(bytes: BufferSource): boolean;
|
||||
}
|
||||
|
||||
@@ -84,6 +84,16 @@ const DYN_HANDLE_TAG_NUM: Record<string, number> = {
|
||||
child: 16,
|
||||
fileHandle: 17,
|
||||
dir: 44,
|
||||
cryptoHash: 52,
|
||||
cryptoHmac: 53,
|
||||
};
|
||||
|
||||
/** Handle kinds whose runtime unit boxes them through its own function,
|
||||
* which installs the tag's ops on first use (scr_crypto.h). Programs that
|
||||
* never box one of these values do not link its dynamic members. */
|
||||
const DYN_HANDLE_BOX_FN: Record<string, string> = {
|
||||
cryptoHash: "scr_crypto_hash_to_dyn",
|
||||
cryptoHmac: "scr_crypto_hash_to_dyn",
|
||||
};
|
||||
|
||||
export const DYN_KIND = {
|
||||
@@ -2951,9 +2961,17 @@ export class LlDyn {
|
||||
// tag's installed ops.
|
||||
const h = DYN_HANDLE_KINDS.get(t.kind);
|
||||
if (h) {
|
||||
host.declare(`declare ptr @scr_dyn_new_handle(ptr, i32)`);
|
||||
const box = DYN_HANDLE_BOX_FN[t.kind];
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @scr_dyn_new_handle(ptr %v, i32 ${DYN_HANDLE_TAG_NUM[t.kind]})`);
|
||||
if (box) {
|
||||
host.declare(`declare ptr @${box}(ptr)`);
|
||||
B.line(`${r} = call ptr @${box}(ptr %v)`);
|
||||
} else {
|
||||
host.declare(`declare ptr @scr_dyn_new_handle(ptr, i32)`);
|
||||
B.line(
|
||||
`${r} = call ptr @scr_dyn_new_handle(ptr %v, i32 ${DYN_HANDLE_TAG_NUM[t.kind]})`,
|
||||
);
|
||||
}
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
VOID,
|
||||
} from "../../ir/ir.js";
|
||||
import { moduleRuntimeFeatures, moduleUsesMockTimers } from "../../ir/runtime-features.js";
|
||||
import { captureFreeFunctions, observesStacks } from "../stack-observations.js";
|
||||
import {
|
||||
matchIntegerArrayForLoop,
|
||||
matchIntegerBytesForLoop,
|
||||
@@ -165,6 +166,7 @@ import {
|
||||
mangleFnClosure,
|
||||
mangleFunction,
|
||||
mangleGenDrop,
|
||||
mangleGenSourceClose,
|
||||
mangleGenSpawn,
|
||||
mangleGlobal,
|
||||
mangleLocal,
|
||||
@@ -1179,18 +1181,14 @@ export class LlEmitter {
|
||||
readonly chainSlots = new Map<string, LlValue>();
|
||||
logArgSlots = 0;
|
||||
private readonly stackTraces: boolean;
|
||||
/** Stack-trace mode: functions no capture can include (no frame push). */
|
||||
private readonly unframedFunctions: ReadonlySet<string>;
|
||||
|
||||
constructor(
|
||||
readonly mod: IrModule,
|
||||
options: LlvmTargetOptions,
|
||||
) {
|
||||
this.stackTraces = mod.functions.some(
|
||||
(fn) =>
|
||||
!everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (expr) => !(expr.kind === "libCall" && expr.fn === "error.stack"),
|
||||
}),
|
||||
);
|
||||
this.stackTraces = observesStacks(mod);
|
||||
this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined);
|
||||
this.mockTimers = moduleUsesMockTimers(mod);
|
||||
this.debug =
|
||||
@@ -1272,6 +1270,9 @@ export class LlEmitter {
|
||||
// which Windows lacks.
|
||||
this.stacklessAsyncAllowed = this.stacklessGenerators && mod.lib === undefined && !windows;
|
||||
this.publishedTypes = computePublishedTypes(mod);
|
||||
this.unframedFunctions = this.stackTraces
|
||||
? captureFreeFunctions(mod, this.stackChecks, this.publishedTypes)
|
||||
: new Set();
|
||||
this.immutableFields = computeImmutableFields(mod);
|
||||
this.constructorFunctions = new Set((mod.classes ?? []).map((c) => `%${c.name}.constructor`));
|
||||
this.indirectMayThrow = mt.indirect || mod.workers === true;
|
||||
@@ -4623,6 +4624,25 @@ export class LlEmitter {
|
||||
dr.push(` call void @free(ptr %ap)`, ` ret void`, `}`, ``);
|
||||
for (const line of dr) out.push(line);
|
||||
|
||||
// An iterator helper's return() before its first next() closes the
|
||||
// source generator in its first argument slot.
|
||||
const closesSource = fn.generator.closesSource === true;
|
||||
if (closesSource) {
|
||||
const field = lifted ? 1 : 0;
|
||||
this.declare(`declare void @scr_gen_close(ptr)`);
|
||||
this.declare(`declare void @scr_gen_set_unstarted_return(ptr, ptr)`);
|
||||
out.push(
|
||||
`define internal void @${mangleGenSourceClose(fn.name)}(ptr %ap) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` %sp = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${field}`,
|
||||
` %sv = load ptr, ptr %sp`,
|
||||
` call void @scr_gen_close(ptr %sv)`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
// Spawn wrapper: pack the args (+1 moves in), allocate the
|
||||
// SUSPENDED fiber — nothing runs until the first .next().
|
||||
if (lifted) {
|
||||
@@ -4639,6 +4659,11 @@ export class LlEmitter {
|
||||
settleAsync === null
|
||||
? ` %gg = call ptr @${genNew}(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)})`
|
||||
: ` %gg = call ptr @scr_async_gen_new(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)}, ptr @${settleAsync})`,
|
||||
...(closesSource
|
||||
? [
|
||||
` call void @scr_gen_set_unstarted_return(ptr %gg, ptr @${mangleGenSourceClose(fn.name)})`,
|
||||
]
|
||||
: []),
|
||||
` ret ptr %gg`,
|
||||
`}`,
|
||||
``,
|
||||
@@ -7150,13 +7175,39 @@ export class LlEmitter {
|
||||
// Stackful native fibers and native coroutine frames retain these frames
|
||||
// while suspended. The active exception context owns the chain, so
|
||||
// concurrent fibers isolate it.
|
||||
if (this.stackTraces && !this.wasi && fn.sourceName !== undefined) {
|
||||
this.declare(`declare void @scr_stack_enter(ptr, ptr)`);
|
||||
this.declare(`declare void @scr_stack_leave(ptr)`);
|
||||
if (
|
||||
this.stackTraces &&
|
||||
!this.wasi &&
|
||||
fn.sourceName !== undefined &&
|
||||
!this.unframedFunctions.has(fn.name)
|
||||
) {
|
||||
B.entryAllocas.push(`%source_frame = alloca { ptr, ptr }`);
|
||||
const frame = ` at ${fn.sourceName} (${fn.loc.file})`;
|
||||
B.line(`call void @scr_stack_enter(ptr %source_frame, ptr ${this.cstr(frame)})`);
|
||||
B.returnEpilogue = `call void @scr_stack_leave(ptr %source_frame)`;
|
||||
const frame = this.cstr(` at ${fn.sourceName} (${fn.loc.file})`);
|
||||
if (
|
||||
B.excCellInvariant &&
|
||||
this.mod.lib === undefined &&
|
||||
this.mod.workers !== true &&
|
||||
this.sizeType === "i64"
|
||||
) {
|
||||
// Executables link the active-cell pointer as a plain global, and a
|
||||
// synchronous body sees one cell from entry to every return: push
|
||||
// and pop the frame inline (ScrExcCell.stack is at byte 56 on
|
||||
// 64-bit targets) instead of calling the runtime twice per call.
|
||||
this.declare(`@scr_exc_active = external global ptr`);
|
||||
B.line(`%source_frame.cell = load ptr, ptr @scr_exc_active`);
|
||||
B.line(`%source_frame.chain = getelementptr inbounds i8, ptr %source_frame.cell, i64 56`);
|
||||
B.line(`%source_frame.previous = load ptr, ptr %source_frame.chain`);
|
||||
B.line(`store ptr %source_frame.previous, ptr %source_frame`);
|
||||
B.line(`%source_frame.text = getelementptr inbounds i8, ptr %source_frame, i64 8`);
|
||||
B.line(`store ptr ${frame}, ptr %source_frame.text`);
|
||||
B.line(`store ptr %source_frame, ptr %source_frame.chain`);
|
||||
B.returnEpilogue = `store ptr %source_frame.previous, ptr %source_frame.chain`;
|
||||
} else {
|
||||
this.declare(`declare void @scr_stack_enter(ptr, ptr)`);
|
||||
this.declare(`declare void @scr_stack_leave(ptr)`);
|
||||
B.line(`call void @scr_stack_enter(ptr %source_frame, ptr ${frame})`);
|
||||
B.returnEpilogue = `call void @scr_stack_leave(ptr %source_frame)`;
|
||||
}
|
||||
}
|
||||
if (this.stackChecks?.has(fn.name)) this.emitStackCheck();
|
||||
if (this.workerEntryPolls?.has(fn.name)) this.emitPendingCheck(undefined, true);
|
||||
@@ -9517,14 +9568,15 @@ export class LlEmitter {
|
||||
B.line(`${stash} = call ptr @scr_exc_take() ; stash across finally`);
|
||||
B.line(`store ptr ${stash}, ptr ${stashSlot}`);
|
||||
this.scopes.push([{ slot: stashSlot, type: CAUGHT }]);
|
||||
const suppressHandler = s.suppressFinallyErrors
|
||||
? {
|
||||
label: B.newLabel("try.fs"),
|
||||
used: false,
|
||||
frameDepth: this.frames.length,
|
||||
scopeDepth: this.scopes.length,
|
||||
}
|
||||
: null;
|
||||
const suppressHandler =
|
||||
s.suppressFinallyErrors || s.dropFinallyErrors
|
||||
? {
|
||||
label: B.newLabel("try.fs"),
|
||||
used: false,
|
||||
frameDepth: this.frames.length,
|
||||
scopeDepth: this.scopes.length,
|
||||
}
|
||||
: null;
|
||||
if (suppressHandler) this.tryStack.push(suppressHandler);
|
||||
this.emitBlock(s.finallyBody!);
|
||||
if (suppressHandler) this.tryStack.pop();
|
||||
@@ -9534,8 +9586,9 @@ export class LlEmitter {
|
||||
this.emitUnwind();
|
||||
if (suppressHandler?.used) {
|
||||
B.startBlock(suppressHandler.label);
|
||||
this.declare(`declare void @scr_exc_suppress(ptr)`);
|
||||
B.line(`call void @scr_exc_suppress(ptr ${stash}) ; consumes stash`);
|
||||
const cleanup = s.dropFinallyErrors ? "scr_exc_keep_first" : "scr_exc_suppress";
|
||||
this.declare(`declare void @${cleanup}(ptr)`);
|
||||
B.line(`call void @${cleanup}(ptr ${stash}) ; consumes stash`);
|
||||
this.emitUnwind();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +204,11 @@ export function mangleGenSpawn(fnName: string): string {
|
||||
export function mangleGenDrop(fnName: string): string {
|
||||
return `sc_gd_${sanitize(fnName)}`;
|
||||
}
|
||||
/** The never-started return() hook of a generator that closes its source
|
||||
* argument (an iterator helper). */
|
||||
export function mangleGenSourceClose(fnName: string): string {
|
||||
return `sc_gc_${sanitize(fnName)}`;
|
||||
}
|
||||
/** Emitted ref-kind resolve thunk, interned per inner-type key. */
|
||||
export function mangleResolveThunk(n: number): string {
|
||||
return `sc_rslv_${n}`;
|
||||
|
||||
@@ -580,7 +580,7 @@ function breaksTo(
|
||||
|
||||
/** Statements no execution reaches: those after a statement that cannot
|
||||
* complete normally in the same statement list. */
|
||||
function unreachableStatements(body: readonly IrStmt[]): Set<IrStmt> {
|
||||
export function unreachableStatements(body: readonly IrStmt[]): Set<IrStmt> {
|
||||
const dead = new Set<IrStmt>();
|
||||
const { statement } = completion();
|
||||
const list = (stmts: readonly IrStmt[]): void => {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
F64,
|
||||
STRING,
|
||||
VOID,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
} from "../ir/ir.js";
|
||||
import { captureFreeFunctions, observesStacks } from "./stack-observations.js";
|
||||
|
||||
const loc = { file: "stack-observations.ts", start: 0, end: 0 };
|
||||
const ERROR = { kind: "object", className: "%Error" } as const;
|
||||
const err: IrExpr = { loc, kind: "varRef", localId: "e", type: ERROR };
|
||||
const stackRead: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "libCall", fn: "error.stack", args: [err], type: STRING },
|
||||
};
|
||||
const call = (callee: string): IrStmt => ({
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "call", callee, args: [], type: VOID },
|
||||
});
|
||||
const throwStmt: IrStmt = { loc, kind: "throw", value: err };
|
||||
const ret: IrStmt = { loc, kind: "return", value: null };
|
||||
const fn = (name: string, body: IrStmt[]): IrFunction => ({
|
||||
loc,
|
||||
name,
|
||||
params: [],
|
||||
locals: [],
|
||||
returnType: VOID,
|
||||
body,
|
||||
});
|
||||
const mod = (functions: IrFunction[], extra: Partial<IrModule> = {}): IrModule => ({
|
||||
irVersion: 16,
|
||||
sourceFile: "stack-observations.ts",
|
||||
entry: "%main",
|
||||
functions,
|
||||
classes: [],
|
||||
records: [],
|
||||
unions: [],
|
||||
globals: [],
|
||||
...extra,
|
||||
});
|
||||
|
||||
test("a reachable stack read is observed", () => {
|
||||
expect(observesStacks(mod([fn("%main", [call("parse")]), fn("parse", [stackRead])]))).toBe(true);
|
||||
});
|
||||
|
||||
test("programs without stack reads are not", () => {
|
||||
expect(observesStacks(mod([fn("%main", [call("helper")]), fn("helper", [ret])]))).toBe(false);
|
||||
});
|
||||
|
||||
test("reads after an unconditional throw or return never run", () => {
|
||||
expect(observesStacks(mod([fn("%main", [throwStmt, stackRead])]))).toBe(false);
|
||||
expect(observesStacks(mod([fn("%main", [ret, call("parse")]), fn("parse", [stackRead])]))).toBe(
|
||||
false,
|
||||
);
|
||||
// A terminator ends only its own list: the outer list continues.
|
||||
const cond: IrExpr = { loc, kind: "numLit", value: 1, type: F64 };
|
||||
expect(
|
||||
observesStacks(
|
||||
mod([fn("%main", [{ kind: "if", cond, then: [ret], else_: null, loc }, stackRead])]),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("functions referenced only from dead code are unreached", () => {
|
||||
// The production React shape: a DEV-only body behind a throw stub calls
|
||||
// the stack parser, and a closure created after the stub reaches it too.
|
||||
const closure: IrStmt = {
|
||||
loc,
|
||||
kind: "assign",
|
||||
localId: "replacer",
|
||||
value: {
|
||||
loc,
|
||||
kind: "closure",
|
||||
fnName: "replacer",
|
||||
captures: [],
|
||||
type: { kind: "func", params: [], ret: VOID },
|
||||
},
|
||||
};
|
||||
const program = mod([
|
||||
fn("%main", [call("outline")]),
|
||||
fn("outline", [throwStmt, closure, call("parse")]),
|
||||
fn("replacer", [call("parse")]),
|
||||
fn("parse", [stackRead]),
|
||||
]);
|
||||
expect(observesStacks(program)).toBe(false);
|
||||
// The same closure created before the stub keeps the read reachable.
|
||||
const live = mod([
|
||||
fn("%main", [call("outline")]),
|
||||
fn("outline", [closure, throwStmt]),
|
||||
fn("replacer", [call("parse")]),
|
||||
fn("parse", [stackRead]),
|
||||
]);
|
||||
expect(observesStacks(live)).toBe(true);
|
||||
});
|
||||
|
||||
test("class methods are roots", () => {
|
||||
const program = mod([fn("%main", [ret]), fn("%Parser.read", [stackRead])], {
|
||||
classes: [
|
||||
{ name: "Parser", fields: [], methods: ["read"] } as unknown as NonNullable<
|
||||
IrModule["classes"]
|
||||
>[number],
|
||||
],
|
||||
});
|
||||
expect(observesStacks(program)).toBe(true);
|
||||
});
|
||||
|
||||
test("leaf functions that cannot create an Error need no frame", () => {
|
||||
const num = (value: number): IrExpr => ({ loc, kind: "numLit", value, type: F64 });
|
||||
const add: IrStmt = {
|
||||
loc,
|
||||
kind: "return",
|
||||
value: { loc, kind: "bin", op: "+", left: num(1), right: num(2), type: F64 },
|
||||
};
|
||||
const construct: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "libCall", fn: "error.new", args: [], type: ERROR },
|
||||
};
|
||||
const program = mod([
|
||||
fn("%main", [call("leaf"), call("caller"), call("creates"), call("viaCreates")]),
|
||||
fn("leaf", [add]),
|
||||
fn("caller", [call("leaf"), add]),
|
||||
fn("creates", [construct]),
|
||||
fn("viaCreates", [call("creates")]),
|
||||
fn("recursive", [call("recursive")]),
|
||||
]);
|
||||
const free = captureFreeFunctions(program, new Set(["recursive"]), null);
|
||||
expect([...free].sort()).toEqual(["caller", "leaf"]);
|
||||
// A worker program keeps every frame.
|
||||
expect(captureFreeFunctions({ ...program, workers: true }, null, null).size).toBe(0);
|
||||
});
|
||||
@@ -0,0 +1,237 @@
|
||||
/* Whole-program stack-trace gate (see observesStacks). Pure function of the
|
||||
* IR module; the emitter keeps a source frame in every function only when
|
||||
* it answers true. */
|
||||
import { exprFunctionReference, moduleFunctionReferences } from "../ir/function-references.js";
|
||||
import { MAY_THROW_LIB_FNS } from "../ir/builtin-effects.js";
|
||||
import type { IrExpr, IrFunction, IrModule, IrStmt } from "../ir/ir.js";
|
||||
import { MAY_THROW_ARR_METHODS } from "../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild, everyStmtList } from "../ir/traverse.js";
|
||||
import { unreachableStatements } from "./may-throw.js";
|
||||
import type { PublishedTypes } from "./publication.js";
|
||||
import { writesPublished } from "./publication.js";
|
||||
|
||||
/** Lib calls that read a captured stack trace. */
|
||||
const STACK_READS: ReadonlySet<string> = new Set(["error.stack"]);
|
||||
|
||||
function readsStack(expr: IrExpr): boolean {
|
||||
return expr.kind === "libCall" && STACK_READS.has(expr.fn);
|
||||
}
|
||||
|
||||
function containsStackRead(fn: IrFunction): boolean {
|
||||
return !everyStmtList(fn.body, { stmt: () => true, expr: (expr) => !readsStack(expr) });
|
||||
}
|
||||
|
||||
/** Visit every expression of `body` that execution can reach: statements
|
||||
* after one that cannot complete normally in the same list are skipped. */
|
||||
function visitLiveExprs(body: IrStmt[], visit: (expr: IrExpr) => void): void {
|
||||
const dead = unreachableStatements(body);
|
||||
const expr = (node: IrExpr): boolean => {
|
||||
visit(node);
|
||||
return everyExprChild(node, expr, stmt);
|
||||
};
|
||||
const stmt = (node: IrStmt): boolean => dead.has(node) || everyStmtChild(node, expr, stmt);
|
||||
body.every(stmt);
|
||||
}
|
||||
|
||||
/** Whether the program can read a captured stack trace, which is what makes
|
||||
* every function keep a source frame. A read counts when it can execute:
|
||||
* it sits in live code of a function reachable from the module's roots (its
|
||||
* entry, library exports, class prototype helpers and every class's
|
||||
* methods) through live code. Code after a statement that cannot complete
|
||||
* normally is not live: production builds of libraries such as React's
|
||||
* Flight server keep development-only stack parsing behind `throw` stubs,
|
||||
* and those reads must not cost every call of the program a frame.
|
||||
* Programs without any stack read answer false without the walk. */
|
||||
export function observesStacks(mod: IrModule): boolean {
|
||||
if (!mod.functions.some(containsStackRead)) return false;
|
||||
const byName = new Map(mod.functions.map((fn) => [fn.name, fn]));
|
||||
const classNames = new Set((mod.classes ?? []).map((cls) => cls.name));
|
||||
const pending: string[] = [...moduleFunctionReferences(mod)];
|
||||
// Class methods, accessors and constructors are reachable through
|
||||
// dispatch the IR does not name per site: all of them are roots.
|
||||
for (const fn of mod.functions) {
|
||||
if (!fn.name.startsWith("%")) continue;
|
||||
for (let dot = fn.name.indexOf("."); dot !== -1; dot = fn.name.indexOf(".", dot + 1)) {
|
||||
if (classNames.has(fn.name.slice(1, dot))) {
|
||||
pending.push(fn.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const reached = new Set<string>();
|
||||
let observed = false;
|
||||
while (pending.length > 0 && !observed) {
|
||||
const name = pending.pop()!;
|
||||
if (reached.has(name)) continue;
|
||||
reached.add(name);
|
||||
const fn = byName.get(name);
|
||||
if (fn === undefined) continue;
|
||||
visitLiveExprs(fn.body, (expr) => {
|
||||
if (readsStack(expr)) observed = true;
|
||||
if (expr.kind === "call" && !reached.has(expr.callee)) pending.push(expr.callee);
|
||||
const referenced = exprFunctionReference(expr);
|
||||
if (referenced !== undefined && !reached.has(referenced)) pending.push(referenced);
|
||||
});
|
||||
}
|
||||
return observed;
|
||||
}
|
||||
|
||||
/** Expressions that neither create an Error nor throw one by themselves.
|
||||
* Calls, `new` and virtual calls are capture-free when their callees are. */
|
||||
const CAPTURE_FREE_EXPRS: ReadonlySet<IrExpr["kind"]> = new Set([
|
||||
"numLit",
|
||||
"boolLit",
|
||||
"strLit",
|
||||
"unitLit",
|
||||
"varRef",
|
||||
"incDec",
|
||||
"assignExpr",
|
||||
"bin",
|
||||
"unary",
|
||||
"logical",
|
||||
"ternary",
|
||||
"nullish",
|
||||
"toBool",
|
||||
"fieldGet",
|
||||
"recordGet",
|
||||
"arrayGet",
|
||||
"arrayHas",
|
||||
"arrayState",
|
||||
"strCmp",
|
||||
"strEq",
|
||||
"strConcat",
|
||||
"upcast",
|
||||
"unionDisc",
|
||||
"unionIsTag",
|
||||
"unionEq",
|
||||
"instanceOf",
|
||||
"recordLit",
|
||||
"arrayLit",
|
||||
"unionWrap",
|
||||
"unionNarrow",
|
||||
"libCall",
|
||||
"arrIntrinsic",
|
||||
"call",
|
||||
"new",
|
||||
"virtualCall",
|
||||
]);
|
||||
|
||||
/** Statements that neither create an Error nor throw one by themselves. */
|
||||
const CAPTURE_FREE_STMTS: ReadonlySet<IrStmt["kind"]> = new Set([
|
||||
"varDecl",
|
||||
"assign",
|
||||
"exprStmt",
|
||||
"if",
|
||||
"while",
|
||||
"doWhile",
|
||||
"for",
|
||||
"switch",
|
||||
"block",
|
||||
"return",
|
||||
"break",
|
||||
"continue",
|
||||
"fieldSet",
|
||||
"recordSet",
|
||||
"arraySet",
|
||||
]);
|
||||
|
||||
/** Functions whose frame no stack capture can ever include: a capture runs
|
||||
* where an Error is created (by the program or by a throwing runtime
|
||||
* operation), and these functions only perform operations from a fixed
|
||||
* list that never create one and only call functions of the same kind.
|
||||
* Their frames are therefore never observable and the emitter omits them,
|
||||
* which keeps small leaf methods and helpers free of the per-call frame
|
||||
* push in programs that read stacks. Recursive functions (the stack guard
|
||||
* throws RangeError), async and generator bodies, and every function of a
|
||||
* worker program keep their frames. */
|
||||
export function captureFreeFunctions(
|
||||
mod: IrModule,
|
||||
stackChecks: ReadonlySet<string> | null,
|
||||
published: PublishedTypes | null,
|
||||
): Set<string> {
|
||||
const free = new Set<string>();
|
||||
if (mod.workers === true) return free;
|
||||
const methodImpls = new Map<string, string[]>();
|
||||
for (const cls of mod.classes ?? []) {
|
||||
for (const m of cls.methods ?? []) {
|
||||
let list = methodImpls.get(m);
|
||||
if (!list) methodImpls.set(m, (list = []));
|
||||
list.push(`%${cls.name}.${m}`);
|
||||
}
|
||||
}
|
||||
const tdzGlobals = new Set((mod.globals ?? []).filter((g) => g.tdz).map((g) => g.id));
|
||||
const callees = new Map<string, string[]>();
|
||||
for (const fn of mod.functions) {
|
||||
if (fn.async === true || fn.generator !== undefined || stackChecks?.has(fn.name)) continue;
|
||||
const tdz = new Set([...tdzGlobals, ...fn.locals.filter((l) => l.tdz).map((l) => l.id)]);
|
||||
const calls: string[] = [];
|
||||
const bigint = (e: IrExpr): boolean => e.type.kind === "bigint";
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (!CAPTURE_FREE_EXPRS.has(e.kind)) return false;
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
case "incDec":
|
||||
case "assignExpr":
|
||||
if (tdz.has(e.localId)) return false;
|
||||
break;
|
||||
case "bin":
|
||||
// BigInt division and remainder by zero throw RangeError.
|
||||
if (bigint(e) || bigint(e.left) || bigint(e.right)) return false;
|
||||
break;
|
||||
case "unary":
|
||||
if (bigint(e) || bigint(e.operand)) return false;
|
||||
break;
|
||||
case "libCall":
|
||||
// Math functions only: they neither throw nor call back.
|
||||
if (!e.fn.startsWith("math.") || MAY_THROW_LIB_FNS.has(e.fn)) return false;
|
||||
break;
|
||||
case "arrIntrinsic": {
|
||||
// Methods that cannot throw, over numbers, booleans or strings
|
||||
// (no element conversion can run program code) and without a
|
||||
// callback argument.
|
||||
const elem = e.receiver.type.kind === "array" ? e.receiver.type.elem.kind : null;
|
||||
if (
|
||||
MAY_THROW_ARR_METHODS.has(e.method) ||
|
||||
(elem !== "f64" && elem !== "bool" && elem !== "string") ||
|
||||
e.args.some((arg) => arg.type.kind === "func")
|
||||
)
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
case "call":
|
||||
calls.push(e.callee);
|
||||
break;
|
||||
case "new":
|
||||
calls.push(`%${e.className}.constructor`);
|
||||
break;
|
||||
case "virtualCall": {
|
||||
const impls = methodImpls.get(e.method);
|
||||
if (impls === undefined) return false;
|
||||
calls.push(...impls);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (!CAPTURE_FREE_STMTS.has(s.kind)) return false;
|
||||
if (s.kind === "assign" && tdz.has(s.localId)) return false;
|
||||
if (published !== null && writesPublished(published, s)) return false;
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
if (fn.body.every(stmt)) callees.set(fn.name, calls);
|
||||
}
|
||||
// Drop candidates that call a function outside the set until none do.
|
||||
for (const name of callees.keys()) free.add(name);
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const [name, calls] of callees) {
|
||||
if (free.has(name) && !calls.every((callee) => free.has(callee))) {
|
||||
free.delete(name);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return free;
|
||||
}
|
||||
@@ -285,6 +285,11 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
}
|
||||
externalTypes[specifier] = declarationPath;
|
||||
}
|
||||
const wasmArgs = values.wasm ?? [];
|
||||
if (wasmArgs.length > 0 && command !== "coverage") {
|
||||
fail(`--wasm is a coverage-only option\n\n${USAGE}`);
|
||||
}
|
||||
const wasmFiles = wasmArgs.map((file) => resolve(file));
|
||||
const ffiProfilePath = values.ffi !== undefined ? resolve(values.ffi) : undefined;
|
||||
if (values.backend !== undefined && values.backend !== "llvm") {
|
||||
fail(`unknown backend "${values.backend}" (supported: llvm)\n\n${USAGE}`);
|
||||
@@ -364,6 +369,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
...(Object.keys(externalTypes).length > 0 ? { externalTypes } : {}),
|
||||
...(tier !== undefined ? { tier } : {}),
|
||||
...(explainDynamic !== undefined ? { explainDynamic: true } : {}),
|
||||
...(wasmFiles.length > 0 ? { wasm: wasmFiles } : {}),
|
||||
});
|
||||
if (printDiagnostics) {
|
||||
// Keep stdout pure JSON for tooling.
|
||||
|
||||
@@ -80,6 +80,11 @@ Options:
|
||||
values and the bindings, fields, functions, classes and
|
||||
modules the dynamic tier would take over, with the
|
||||
refusal behind each; a name shows only that unit's chain
|
||||
--wasm <file.wasm>
|
||||
coverage only: report a WebAssembly module the program
|
||||
loads whose path its sources do not spell (repeatable;
|
||||
modules the sources name, inline modules and packaged
|
||||
.wasm files are found without it)
|
||||
-h, --help show this help
|
||||
-v, --version print the version
|
||||
`;
|
||||
@@ -103,6 +108,7 @@ export const CLI_OPTIONS = {
|
||||
"external-types": { type: "string", multiple: true },
|
||||
tier: { type: "string" },
|
||||
"explain-dynamic": { type: "string" },
|
||||
wasm: { type: "string", multiple: true },
|
||||
lib: { type: "boolean", default: false },
|
||||
profile: { type: "string" },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
|
||||
@@ -171,6 +171,9 @@ export interface AnalyzeOptions {
|
||||
/** `--explain-dynamic`: attach the tier-assignment report
|
||||
* (CoverageInput.tiers). Such analyses bypass the verdict cache. */
|
||||
explainDynamic?: boolean;
|
||||
/** `--wasm <file>`: WebAssembly module files the program loads that its
|
||||
* sources do not name (absolute paths); reported with the discovered ones. */
|
||||
wasm?: readonly string[];
|
||||
}
|
||||
|
||||
export interface AnalyzeResult {
|
||||
|
||||
@@ -24,6 +24,7 @@ import { renderDiagnostics } from "../diagnostics/render.js";
|
||||
import type { LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { ProvenanceSources } from "../frontend/provenance-registry.js";
|
||||
import type { DemotionUnit, TierReport } from "../frontend/tier-analysis.js";
|
||||
import type { WasmModuleReport } from "../wasm/discover.js";
|
||||
|
||||
export interface CoverageInput {
|
||||
file: string;
|
||||
@@ -57,6 +58,9 @@ export interface CoverageInput {
|
||||
/** --explain-dynamic: the report-only tier assignment (which bindings,
|
||||
* fields, functions, classes and modules the dynamic tier would take). */
|
||||
tiers?: TierReport;
|
||||
/** The WebAssembly modules the build can see (wasm/discover.ts): their
|
||||
* size, validity and the proposals they use. */
|
||||
wasmModules?: WasmModuleReport[];
|
||||
/** True when tsc preflight failed — no coverage can be computed. */
|
||||
preflightFailed: boolean;
|
||||
}
|
||||
@@ -217,6 +221,51 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions
|
||||
out.push("");
|
||||
}
|
||||
|
||||
// The WebAssembly modules the build can see: what translating them would
|
||||
// take (wasm/discover.ts). No module compiles to native code yet; the
|
||||
// sites that load one are blockers or deferred fences above.
|
||||
const wasm = input.wasmModules ?? [];
|
||||
if (wasm.length > 0) {
|
||||
out.push(
|
||||
` ${c(DIM, "WebAssembly modules:")} ${c(DIM, "(found at build time; none compile to native code yet)")}`,
|
||||
);
|
||||
for (const m of wasm) {
|
||||
const name = m.path !== undefined ? displayPath(m.path, opts.root) : "inline module";
|
||||
const state =
|
||||
m.status === "valid"
|
||||
? c(GREEN, "valid")
|
||||
: m.status === "unsupported"
|
||||
? c(YELLOW, `uses ${m.unsupported.join(", ")}`)
|
||||
: c(RED, `${m.status}: ${m.error ?? ""}`);
|
||||
out.push(` ${name} ${c(DIM, `${groupDigits(m.size)} bytes`)} ${state}`);
|
||||
const imports = m.importModules.length > 0 ? ` (${m.importModules.join(", ")})` : "";
|
||||
out.push(
|
||||
` ${c(DIM, `${counted(m.functions, "function")}, ${counted(m.imports, "import")}${imports}, ${counted(m.exports, "export")}`)}`,
|
||||
);
|
||||
if (m.features.length > 0) out.push(` ${c(DIM, `features: ${m.features.join(", ")}`)}`);
|
||||
const shown = m.sources.slice(0, SITES_SHOWN);
|
||||
for (const s of shown) {
|
||||
const at =
|
||||
s.line !== undefined
|
||||
? `${displayPath(s.file, opts.root)}:${s.line}`
|
||||
: displayPath(s.file, opts.root);
|
||||
const how =
|
||||
s.kind === "path"
|
||||
? `path at ${at}`
|
||||
: s.kind === "inline"
|
||||
? `inline at ${at}`
|
||||
: s.kind === "package"
|
||||
? `shipped by the package at ${displayPath(dirOf(s.file), opts.root)}`
|
||||
: `configured by ${at}`;
|
||||
out.push(` ${c(DIM, `found: ${how}`)}`);
|
||||
}
|
||||
if (m.sources.length > shown.length) {
|
||||
out.push(` ${c(DIM, `found: +${m.sources.length - shown.length} more`)}`);
|
||||
}
|
||||
}
|
||||
out.push("");
|
||||
}
|
||||
|
||||
// --provenance-sources: per-package attribution — the answer to "did
|
||||
// the DEPENDENCY's statements compile static?", computed by summing
|
||||
// statsByFile entries under each package's source dir. Elided
|
||||
@@ -335,6 +384,27 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
/** "1 function", "1,698 functions". */
|
||||
function counted(n: number, noun: string): string {
|
||||
return `${groupDigits(n)} ${noun}${n === 1 ? "" : "s"}`;
|
||||
}
|
||||
|
||||
/** 637405 → "637,405". */
|
||||
function groupDigits(n: number): string {
|
||||
const digits = String(n);
|
||||
let out = "";
|
||||
for (let i = 0; i < digits.length; i++) {
|
||||
if (i > 0 && (digits.length - i) % 3 === 0) out += ",";
|
||||
out += digits[i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function dirOf(file: string): string {
|
||||
const slash = file.lastIndexOf("/");
|
||||
return slash <= 0 ? file : file.slice(0, slash);
|
||||
}
|
||||
|
||||
function DIMPath(file: string, color: boolean): string {
|
||||
return color ? DIM + file + RESET : file;
|
||||
}
|
||||
@@ -453,6 +523,7 @@ export function coverageEnvelope(
|
||||
},
|
||||
}),
|
||||
...(input.tiers !== undefined ? { tiers: tiersEnvelope(input.tiers, locator) } : {}),
|
||||
...(input.wasmModules !== undefined ? { wasmModules: input.wasmModules } : {}),
|
||||
groups,
|
||||
diagnostics: located,
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ import { InternalCompilerError } from "../errors.js";
|
||||
* harness asserts refusals match entry codes. renderSurfaceManifest is
|
||||
* byte-deterministic: entries sort by id, keys are emitted in one fixed
|
||||
* order, and the output carries no timestamps or absolute paths. */
|
||||
import { FENCE_CODES, UNSUPPORTED } from "../diagnostics/diagnostic.js";
|
||||
import { FENCE_CODES, UNSUPPORTED, webAssemblyDiag } from "../diagnostics/diagnostic.js";
|
||||
import { NODE24_FETCH_COMPAT_PROFILE } from "../compat/fetch-profile.js";
|
||||
import { SUPPORTED_BUILTIN_MODULES, SUPPORTED_NODE_MODULES } from "../frontend/builtin-modules.js";
|
||||
import {
|
||||
@@ -275,7 +275,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
status: "static",
|
||||
...(COLLECTION_ITERATOR_METHODS.has(name)
|
||||
? {
|
||||
note: "live native iterators support next(), for...of, and drains; iterator helper methods are refused",
|
||||
note: "live native iterators support next(), for...of, drains, and the iterator helper methods",
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
@@ -288,7 +288,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
status: "static",
|
||||
...(COLLECTION_ITERATOR_METHODS.has(name)
|
||||
? {
|
||||
note: "live native iterators support next(), for...of, and drains; iterator helper methods are refused",
|
||||
note: "live native iterators support next(), for...of, drains, and the iterator helper methods",
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
@@ -444,6 +444,19 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
}
|
||||
}
|
||||
|
||||
// ── the WebAssembly global: every reached use refuses (webAssemblyDiag) ─
|
||||
{
|
||||
const refusal = webAssemblyDiag("WebAssembly", { file: "", start: 0, end: 0 });
|
||||
add({
|
||||
id: "stdlib.webassembly",
|
||||
kind: "stdlib",
|
||||
name: "globalThis.WebAssembly",
|
||||
status: "unsupported",
|
||||
code: refusal.code,
|
||||
...(refusal.hint !== undefined ? { note: refusal.hint } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
// ── fetch/Web platform: one explicit, versioned compatibility profile ─
|
||||
const fetchTarget =
|
||||
`Node ${NODE24_FETCH_COMPAT_PROFILE.target.node} / ` +
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { AnalyzeOptions, AnalyzeResult } from "../compile-types.js";
|
||||
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import type { LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { NpmStaticStatus } from "./report.js";
|
||||
import type { WasmModuleReport } from "../wasm/discover.js";
|
||||
import {
|
||||
FrontendInputTracker,
|
||||
frontendInputsStillMatch,
|
||||
@@ -38,6 +39,7 @@ export interface CachedCoverage {
|
||||
unreached?: { stats: LowerStats; diagnostics: ScrDiagnostic[]; runtimeFences?: ScrDiagnostic[] };
|
||||
divergences?: ScrDiagnostic[];
|
||||
npmStatic?: NpmStaticStatus[];
|
||||
wasmModules?: WasmModuleReport[];
|
||||
preflightFailed: boolean;
|
||||
}
|
||||
|
||||
@@ -125,6 +127,8 @@ export function verdictKey(
|
||||
external.join("\n"),
|
||||
// Demotion (stage S6) makes the tier mode part of the verdict.
|
||||
...(opts.tier === "static-only" ? ["tier=static-only"] : []),
|
||||
// The module files' contents are probes; their names are options.
|
||||
...(opts.wasm !== undefined && opts.wasm.length > 0 ? [`wasm=${opts.wasm.join("\n")}`] : []),
|
||||
].join("\0"),
|
||||
);
|
||||
}
|
||||
@@ -146,6 +150,7 @@ function cacheable(coverage: AnalyzeResult["coverage"]): CachedCoverage | null {
|
||||
if (coverage.unreached !== undefined) out.unreached = coverage.unreached;
|
||||
if (coverage.divergences !== undefined) out.divergences = coverage.divergences;
|
||||
if (coverage.npmStatic !== undefined) out.npmStatic = coverage.npmStatic;
|
||||
if (coverage.wasmModules !== undefined) out.wasmModules = coverage.wasmModules;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -162,6 +167,7 @@ function restore(cached: CachedCoverage, sources: CachedSource[]): AnalyzeResult
|
||||
if (cached.unreached !== undefined) coverage.unreached = cached.unreached;
|
||||
if (cached.divergences !== undefined) coverage.divergences = cached.divergences;
|
||||
if (cached.npmStatic !== undefined) coverage.npmStatic = cached.npmStatic;
|
||||
if (cached.wasmModules !== undefined) coverage.wasmModules = cached.wasmModules;
|
||||
return { coverage, sourceTexts };
|
||||
}
|
||||
|
||||
|
||||
@@ -751,6 +751,20 @@ export function noLoweringDiag(
|
||||
};
|
||||
}
|
||||
|
||||
/** A REACHED use of the WebAssembly API (SC2020): scriptc.d.ts declares
|
||||
* its types so packages that mention them typecheck, but no module
|
||||
* compiles to native code yet. `surface` names what was used
|
||||
* ('WebAssembly.compile', 'new WebAssembly.Module', 'globalThis.WebAssembly'). */
|
||||
export function webAssemblyDiag(surface: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2020",
|
||||
message: `'${surface}' is not supported yet: scriptc does not compile WebAssembly modules to native code yet`,
|
||||
loc,
|
||||
milestone: "later",
|
||||
hint: "WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)",
|
||||
};
|
||||
}
|
||||
|
||||
/** A resolved native addon needs Node's embedding ABI. The migration uses the
|
||||
* underlying native operation through an ordinary C ABI declaration. */
|
||||
export function nativeAddonDiag(specifier: string, loc: SrcLoc): ScrDiagnostic {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* bumps it. */
|
||||
import { readFileSync } from "node:fs";
|
||||
import type { ScrDiagnostic } from "./diagnostic.js";
|
||||
import type { WasmModuleReport } from "../wasm/discover.js";
|
||||
|
||||
export const DIAGNOSTICS_SCHEMA = "scriptc-diagnostics";
|
||||
export const DIAGNOSTICS_SCHEMA_VERSION = 1;
|
||||
@@ -138,6 +139,9 @@ export interface DiagnosticsEnvelope {
|
||||
artifact?: string;
|
||||
/** coverage --explain-dynamic only: the report-only tier assignment. */
|
||||
tiers?: TiersEnvelope;
|
||||
/** coverage only: the WebAssembly modules the build can see, with their
|
||||
* size, validity, proposals and the sources that found them. */
|
||||
wasmModules?: WasmModuleReport[];
|
||||
groups: DiagnosticGroup[];
|
||||
diagnostics: LocatedDiagnostic[];
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ export function analyzeWithFrontend(
|
||||
...(ffi !== null ? { ffiImports: ffi.functions } : {}),
|
||||
...(opts.tier !== undefined ? { tierMode: opts.tier } : {}),
|
||||
...(opts.explainDynamic === true ? { tierReport: { preflight } } : {}),
|
||||
...(opts.wasm !== undefined && opts.wasm.length > 0 ? { wasmFiles: opts.wasm } : {}),
|
||||
});
|
||||
const provenance = provenanceSources();
|
||||
return {
|
||||
@@ -113,6 +114,7 @@ export function analyzeWithFrontend(
|
||||
...(lowered.statsByFile ? { statsByFile: lowered.statsByFile } : {}),
|
||||
...(lowered.provenanceElided ? { provenanceElided: lowered.provenanceElided } : {}),
|
||||
...(lowered.tiers !== undefined ? { tiers: lowered.tiers } : {}),
|
||||
...(lowered.wasmModules !== undefined ? { wasmModules: lowered.wasmModules } : {}),
|
||||
preflightFailed: false,
|
||||
},
|
||||
sourceTexts: fe.sourceTexts(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { lowerBuiltinByteInput, errorFirstBytesCallback, errorFirstCallback } from "./arguments.js";
|
||||
import { isKeyObjectValue } from "./key-object.js";
|
||||
import { boolLit, dynUndefinedExpr, varRef } from "../../../ir/build.js";
|
||||
import { boolLit, dynUndefinedExpr, nodeThrowExpr, varRef } from "../../../ir/build.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer, own } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
@@ -942,9 +942,27 @@ export function lowerCryptoHashMethodCall(
|
||||
if (call.questionDotToken || access.questionDotToken) return null;
|
||||
const receiverType = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
if (receiverType?.kind !== "cryptoHash" && receiverType?.kind !== "cryptoHmac") return null;
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
const name = access.name.text;
|
||||
const loc = locOf(call);
|
||||
// Hmac has no copy(): Node reads the member as undefined and throws a
|
||||
// TypeError once the arguments are evaluated.
|
||||
if (receiverType.kind === "cryptoHmac" && name === "copy") {
|
||||
if (call.arguments.some(ts.isSpreadElement))
|
||||
lowerer.noLowering("Hmac.copy with spread arguments", call);
|
||||
const evaluated = [access.expression, ...call.arguments].map((node): IrStmt => ({
|
||||
kind: "exprStmt",
|
||||
expr: lowerer.lowerExpr(node),
|
||||
loc,
|
||||
}));
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: evaluated,
|
||||
result: nodeThrowExpr(1, "", `${access.getText()} is not a function`, DYN, loc),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
const receiver = (): IrExpr => lowerer.lowerExprExpecting(access.expression, receiverType);
|
||||
const hash = receiverType.kind === "cryptoHash";
|
||||
const label = hash ? "Hash" : "Hmac";
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer, PoisonError } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { webAssemblyDiag } from "../../../diagnostics/diagnostic.js";
|
||||
import { STRING, type IrExpr } from "../../../ir/ir.js";
|
||||
import { stdlibGlobalNameOf } from "../surfaces.js";
|
||||
|
||||
/* The WebAssembly API. scriptc.d.ts declares its types (lib.dom's
|
||||
* namespace) so that packages whose declarations mention them typecheck,
|
||||
* and the type mapper gives those types the checked-dynamic representation.
|
||||
* No module compiles to native code yet, so every reached use of the API
|
||||
* value fences at its expression with webAssemblyDiag: a build error in
|
||||
* TypeScript sources, a located runtime fence in JavaScript ones. typeof
|
||||
* needs no value and folds to Node's answer. */
|
||||
|
||||
/** Strips the spellings that leave a value unchanged: parentheses, type
|
||||
* assertions, `satisfies` and non-null assertions. */
|
||||
function bare(expr: ts.Expression): ts.Expression {
|
||||
for (;;) {
|
||||
if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr)) expr = expr.expression;
|
||||
else if (ts.isAsExpression(expr) || ts.isTypeAssertion(expr)) expr = expr.expression;
|
||||
else if (ts.isSatisfiesExpression(expr)) expr = expr.expression;
|
||||
else return expr;
|
||||
}
|
||||
}
|
||||
|
||||
/** The global object itself, as `globalThis` or `global` spells it. */
|
||||
function isGlobalThis(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
const node = bare(expr);
|
||||
return (
|
||||
ts.isIdentifier(node) &&
|
||||
(node.text === "globalThis" || node.text === "global") &&
|
||||
stdlibGlobalNameOf(lowerer, node) === "globalThis"
|
||||
);
|
||||
}
|
||||
|
||||
/** THE WebAssembly namespace value: `WebAssembly` or `globalThis.WebAssembly`
|
||||
* (and the global object reached through a cast, as in
|
||||
* `(globalThis as any).WebAssembly`). */
|
||||
function isWebAssemblyGlobal(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
expr = bare(expr);
|
||||
if (ts.isIdentifier(expr)) {
|
||||
return expr.text === "WebAssembly" && stdlibGlobalNameOf(lowerer, expr) === "WebAssembly";
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr)) {
|
||||
return expr.name.text === "WebAssembly" && isGlobalThis(lowerer, expr.expression);
|
||||
}
|
||||
if (ts.isElementAccessExpression(expr)) {
|
||||
const key = expr.argumentExpression;
|
||||
return (
|
||||
(ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) &&
|
||||
key.text === "WebAssembly" &&
|
||||
isGlobalThis(lowerer, expr.expression)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The surface a WebAssembly API expression names ('WebAssembly',
|
||||
* 'globalThis.WebAssembly', 'WebAssembly.compile'), or null. A cheap
|
||||
* spelling check runs first: almost every expression is rejected without a
|
||||
* checker query. */
|
||||
function webAssemblySurface(lowerer: Lowerer, expr: ts.Expression): string | null {
|
||||
// `globalThis as unknown as { WebAssembly: ... }`: the cast's target type
|
||||
// names the member, and the value it checks is the global object's
|
||||
// WebAssembly (the spelling pi's codemode uses).
|
||||
if (
|
||||
(ts.isAsExpression(expr) || ts.isTypeAssertion(expr)) &&
|
||||
isGlobalThis(lowerer, expr.expression)
|
||||
) {
|
||||
const target = lowerer.checker.getTypeAtLocation(expr.type);
|
||||
return lowerer.checker.getPropertyOfType(target, "WebAssembly") !== undefined
|
||||
? "globalThis.WebAssembly"
|
||||
: null;
|
||||
}
|
||||
const node = bare(expr);
|
||||
if (ts.isIdentifier(node)) {
|
||||
return node.text === "WebAssembly" && isWebAssemblyGlobal(lowerer, node) ? "WebAssembly" : null;
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(node)) {
|
||||
if (node.name.text === "WebAssembly") {
|
||||
return isWebAssemblyGlobal(lowerer, node) ? "globalThis.WebAssembly" : null;
|
||||
}
|
||||
// WebAssembly.<member>, and members of members
|
||||
// (WebAssembly.Module.customSections).
|
||||
const names = [node.name.text];
|
||||
let receiver = bare(node.expression);
|
||||
while (ts.isPropertyAccessExpression(receiver) && receiver.name.text !== "WebAssembly") {
|
||||
names.unshift(receiver.name.text);
|
||||
receiver = bare(receiver.expression);
|
||||
}
|
||||
const spelled =
|
||||
(ts.isIdentifier(receiver) && receiver.text === "WebAssembly") ||
|
||||
(ts.isPropertyAccessExpression(receiver) && receiver.name.text === "WebAssembly");
|
||||
return spelled && isWebAssemblyGlobal(lowerer, receiver)
|
||||
? `WebAssembly.${names.join(".")}`
|
||||
: null;
|
||||
}
|
||||
if (ts.isElementAccessExpression(node)) {
|
||||
return isWebAssemblyGlobal(lowerer, node) ? "globalThis.WebAssembly" : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Fences a reached use of the WebAssembly API. `prefix` qualifies a
|
||||
* construction ('new '). Returns normally when `expr` is anything else. */
|
||||
export function refuseWebAssemblyUse(lowerer: Lowerer, expr: ts.Expression, prefix = ""): void {
|
||||
const surface = webAssemblySurface(lowerer, expr);
|
||||
if (surface === null) return;
|
||||
lowerer.pushDiag(webAssemblyDiag(`${prefix}${surface}`, locOf(expr)));
|
||||
throw new PoisonError();
|
||||
}
|
||||
|
||||
/** `typeof WebAssembly` is "object" and `typeof WebAssembly.<member>` is
|
||||
* "function" for every member the namespace declares, as in Node. The
|
||||
* operand is an effect-free global read, so nothing else evaluates. */
|
||||
export function lowerWebAssemblyTypeof(lowerer: Lowerer, operand: ts.Expression): IrExpr | null {
|
||||
const surface = webAssemblySurface(lowerer, operand);
|
||||
if (surface === null) return null;
|
||||
const loc = locOf(operand);
|
||||
if (surface === "WebAssembly" || surface === "globalThis.WebAssembly") {
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
}
|
||||
const t = lowerer.typeOf(operand);
|
||||
if (
|
||||
lowerer.checker.getCallSignatures(t).length > 0 ||
|
||||
lowerer.checker.getConstructSignatures(t).length > 0
|
||||
) {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -747,7 +747,7 @@ function lowerForOfMapOrSet(
|
||||
* Recurses through every nested statement list; nested functions are
|
||||
* lifted elsewhere and never appear here, so every return found exits
|
||||
* THIS function through the active iteration. */
|
||||
function exitBeforeReturns(stmts: IrStmt[], make: () => IrStmt): IrStmt[] {
|
||||
export function exitBeforeReturns(stmts: IrStmt[], make: () => IrStmt): IrStmt[] {
|
||||
const walk = (list: IrStmt[]): IrStmt[] =>
|
||||
list.flatMap((s): IrStmt[] => {
|
||||
switch (s.kind) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -315,7 +315,13 @@ export function refreshErrorPropertyDispatch(
|
||||
info.def.name,
|
||||
member,
|
||||
value,
|
||||
[varRef("value", STRING, loc)],
|
||||
[
|
||||
// A JavaScript setter takes its value untyped.
|
||||
lowerer.coerceToExpected(
|
||||
varRef("value", STRING, loc),
|
||||
accessor.sig.params[0]?.type ?? STRING,
|
||||
),
|
||||
],
|
||||
VOID,
|
||||
loc,
|
||||
),
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
BOOL,
|
||||
CAUGHT,
|
||||
DYN,
|
||||
DYN_HANDLE_KINDS,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
@@ -90,7 +91,10 @@ import {
|
||||
import { lowerArrayFromAsyncCall } from "./containers/array-from-async.js";
|
||||
import { lowerDynArrayFilterCall, lowerDynArrayFlatMapCall } from "./containers/array-callbacks.js";
|
||||
import { lowerGroupByStaticCall } from "./containers/collection-grouping.js";
|
||||
import { lowerIteratorHelperCall } from "./containers/iterator-helpers.js";
|
||||
import {
|
||||
lowerGeneratorHelperCall,
|
||||
lowerIteratorHelperCall,
|
||||
} from "./containers/iterator-helpers.js";
|
||||
import {
|
||||
lowerObjectAssignIndexShape,
|
||||
lowerObjectConstructor,
|
||||
@@ -1498,6 +1502,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
// array method paths (the terminal names collide with array
|
||||
// methods, but only iterator-typed receivers reach this).
|
||||
lowerIteratorHelperCall(lowerer, expr, expr.expression) ??
|
||||
// Helper calls on generators and other iterator objects.
|
||||
lowerGeneratorHelperCall(lowerer, expr, expr.expression) ??
|
||||
lowerIteratorStaticFence(lowerer, expr, expr.expression) ??
|
||||
lowerObjectStaticCall(lowerer, expr, expr.expression) ??
|
||||
lowerObjectFromEntriesCall(lowerer, expr, expr.expression) ??
|
||||
@@ -3151,8 +3157,9 @@ function lowerOptionalStringNumber(
|
||||
return { kind: "call", callee: helper, args: [arg], type: F64, loc };
|
||||
}
|
||||
|
||||
/** Iterator types whose helper calls have no lowering on any path (the
|
||||
* array iterator's chains lower in lowerIteratorHelperCall). */
|
||||
/** Iterator types with checked runtime receivers: Map/Set iterators'
|
||||
* helper calls lower in lowerGeneratorHelperCall; the string iterator's
|
||||
* have no lowering. */
|
||||
const UNLOWERED_ITERATOR_TYPES = new Set(["MapIterator", "SetIterator", "StringIterator"]);
|
||||
const ITERATOR_HELPER_METHODS = new Set([
|
||||
"map",
|
||||
@@ -3219,6 +3226,8 @@ function lowerDynReceiverMethodCall(
|
||||
lowerer.isStdlibMember(access)
|
||||
) {
|
||||
if (recvSymbol.name === "MapIterator" || recvSymbol.name === "SetIterator") {
|
||||
const helper = lowerGeneratorHelperCall(lowerer, call, access);
|
||||
if (helper) return helper;
|
||||
lowerer.noLowering(
|
||||
`collection iterator .${access.name.text}()`,
|
||||
call,
|
||||
@@ -3229,7 +3238,7 @@ function lowerDynReceiverMethodCall(
|
||||
lowerer.noLowering(
|
||||
`${recvSymbol.name}.${access.name.text}`,
|
||||
call,
|
||||
"iterator helpers lower on chains from arr.values() or arr.entries() that end in a terminal (toArray, forEach, reduce, some, every, find) — spread the string first: [...s].values()",
|
||||
"iterator helpers lower on generators and on array, Map and Set iterators — spread the string first: [...s].values()",
|
||||
);
|
||||
}
|
||||
const arrayReceiver = lowerer.checker.isArrayType(recvTs);
|
||||
@@ -3368,6 +3377,11 @@ function lowerDynReceiverMethodCall(
|
||||
) {
|
||||
recv = lowerer.coerceToExpected(recv, DYN);
|
||||
}
|
||||
// A native handle read through `as any` (`(hash as any).update(x)`)
|
||||
// dispatches through the handle, as the same value held in an
|
||||
// 'unknown' slot does.
|
||||
if (checkerUntyped && DYN_HANDLE_KINDS.has(recv.type.kind))
|
||||
recv = lowerer.coerceToExpected(recv, DYN);
|
||||
if (recv.type.kind !== "dyn") return null;
|
||||
if (
|
||||
access.name.text === "normalize" &&
|
||||
@@ -6710,9 +6724,9 @@ function recordAssignHelper(
|
||||
}
|
||||
|
||||
/** The `Iterator` global's statics (ES2025 — Iterator.from, and the
|
||||
* abstract constructor as a value): no first-class iterator objects
|
||||
* exist here, so every member fences with the working spelling named
|
||||
* instead of the generic-method fence's monomorphization wording. */
|
||||
* abstract constructor as a value) have no lowering: every member fences
|
||||
* with the working spelling named instead of the generic-method fence's
|
||||
* monomorphization wording. */
|
||||
function lowerIteratorStaticFence(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -6723,8 +6737,8 @@ function lowerIteratorStaticFence(
|
||||
lowerer.noLowering(
|
||||
`Iterator.${access.name.text}`,
|
||||
call,
|
||||
"first-class iterator objects have no lowering — iterator helpers compile as one chain on an " +
|
||||
"array iterator, consumed in place: arr.values().map(f).take(n).toArray()",
|
||||
"iterator helpers apply directly to generators and to array, Map and Set iterators: " +
|
||||
"arr.values().map(f).take(n).toArray()",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { lowerWorkerMetadata } from "./builtins/workers.js";
|
||||
import { esmImportBindingKind } from "./builtins/module-bindings.js";
|
||||
import { lowerReadlineProperty } from "./builtins/readline.js";
|
||||
import { lowerBlobInstanceof } from "./builtins/blob.js";
|
||||
import { lowerWebAssemblyTypeof, refuseWebAssemblyUse } from "./builtins/webassembly.js";
|
||||
import { lowerKeyObjectInstanceof } from "./builtins/key-object.js";
|
||||
import { lowerNetAddressInstanceof, lowerNetAddressStaticRead } from "./builtins/net-address.js";
|
||||
import { isExecExceptionType, refuseExecExceptionCode } from "./builtins/errors.js";
|
||||
@@ -458,6 +459,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (externalTypeSpecifier !== null) {
|
||||
lowerer.externalHostFence(externalTypeSpecifier, expr);
|
||||
}
|
||||
// Every reached use of the WebAssembly API fences at its expression.
|
||||
refuseWebAssemblyUse(lowerer, expr);
|
||||
|
||||
if (ts.isNumericLiteral(expr)) {
|
||||
const value = Number(expr.text.replace(/_/g, ""));
|
||||
@@ -623,6 +626,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
);
|
||||
}
|
||||
if (ts.isTypeOfExpression(expr)) {
|
||||
const webAssembly = lowerWebAssemblyTypeof(lowerer, expr.expression);
|
||||
if (webAssembly) return webAssembly;
|
||||
if (lowerer.isStdlibGlobal(expr.expression, "process"))
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
if (lowerer.isStdlibGlobal(expr.expression, "fetch"))
|
||||
@@ -10438,6 +10443,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
) {
|
||||
const test =
|
||||
lowerConstructorGlobalTypeofTest(lowerer, expr, loc) ??
|
||||
lowerWebAssemblyTypeofTest(lowerer, expr, loc) ??
|
||||
lowerErrorCodeTypeofTest(lowerer, expr, loc) ??
|
||||
lowerer.lowerCaughtTypeofTest(expr, loc) ??
|
||||
lowerDynTypeofTest(lowerer, expr, loc) ??
|
||||
@@ -11830,6 +11836,24 @@ function errorRootedObjectOf(lowerer: Lowerer, t: ts.Type): IrType | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `typeof WebAssembly === "object"` and `typeof WebAssembly.<member> ===
|
||||
* "function"`: the operand needs no value, so the test folds as bare typeof
|
||||
* does (builtins/webassembly.ts) before any dyn-typed operand lowers. */
|
||||
function lowerWebAssemblyTypeofTest(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.BinaryExpression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const negated = expr.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken;
|
||||
for (const [a, b] of operandPairs(expr)) {
|
||||
if (!ts.isTypeOfExpression(a) || !ts.isStringLiteral(b)) continue;
|
||||
const folded = lowerWebAssemblyTypeof(lowerer, a.expression);
|
||||
if (folded?.kind !== "strLit") continue;
|
||||
return { kind: "boolLit", value: (folded.value === b.text) !== negated, type: BOOL, loc };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `typeof e.code === "string"` / `"undefined"` (and the `!==` forms)
|
||||
* where e — through parens and as-casts — is a catch binding or a value
|
||||
* of %Error-rooted type: the runtime error's code slot is a string
|
||||
@@ -12527,6 +12551,7 @@ export function caughtLocalOf(lowerer: Lowerer, node: ts.Expression): IrLocal |
|
||||
* Folding is limited to side-effect-free operands — dropping a computed
|
||||
* operand would skip its effects, so those are rejected instead. */
|
||||
export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc: SrcLoc): IrExpr {
|
||||
refuseWebAssemblyUse(lowerer, expr.right);
|
||||
// `x instanceof net.Socket` over a union with a netSocket arm — the
|
||||
// h2 compat 'connect' narrowing (lower-server.ts): a union tag test.
|
||||
const sockTest = lowerSocketInstanceOf(lowerer, expr, loc);
|
||||
@@ -12883,42 +12908,68 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
"'instanceof' right-hand sides other than classes declared in the program",
|
||||
);
|
||||
}
|
||||
// `x instanceof Writable`: Node's Writable[Symbol.hasInstance] also
|
||||
// answers true for streams with a writable side that do not inherit from
|
||||
// Writable, which are Duplex and its subclasses (Transform, PassThrough,
|
||||
// the zlib streams). Subclasses of Writable keep the prototype test.
|
||||
const duplex =
|
||||
target.def.name === "%Writable" && target.builtinStream !== undefined
|
||||
? lowerer.classes.get("%Duplex")
|
||||
: undefined;
|
||||
// The classes whose interval test can answer true for a value of static
|
||||
// class `className`: Writable and Duplex are disjoint, so a value typed
|
||||
// as one side needs only that side's test.
|
||||
const runtimeTests = (className: string): ClassInfo[] => {
|
||||
if (!duplex) return [target];
|
||||
const mayHold = (info: ClassInfo): boolean =>
|
||||
className === info.def.name ||
|
||||
lowerer.isSubclassOf(className, info.def.name) ||
|
||||
lowerer.isSubclassOf(info.def.name, className);
|
||||
const tests = [target, duplex].filter(mayHold);
|
||||
return tests.length > 0 ? tests : [target];
|
||||
};
|
||||
// A catch binding on the left: the runtime test against the class's
|
||||
// preorder interval (false for non-hierarchy-object payloads). tsc's
|
||||
// narrowing then types the branches; reads bridge through caughtRead.
|
||||
const caughtLocal = lowerer.caughtLocalOf(expr.left);
|
||||
if (caughtLocal) {
|
||||
if (!lowerer.inHierarchy(target)) {
|
||||
const value = varRef(caughtLocal.id, CAUGHT, loc);
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "caughtTest",
|
||||
value,
|
||||
test: "instanceof",
|
||||
className: target.def.name,
|
||||
const caughtInstanceOf = (info: ClassInfo): IrExpr => {
|
||||
if (!lowerer.inHierarchy(info)) {
|
||||
const value = varRef(caughtLocal.id, CAUGHT, loc);
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "caughtTest",
|
||||
value,
|
||||
test: "instanceof",
|
||||
className: info.def.name,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
else_: classInstanceOf(
|
||||
lowerer,
|
||||
{ kind: "caughtToDyn", value, type: DYN, loc },
|
||||
info,
|
||||
loc,
|
||||
),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
else_: classInstanceOf(
|
||||
lowerer,
|
||||
{ kind: "caughtToDyn", value, type: DYN, loc },
|
||||
target,
|
||||
loc,
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "caughtTest",
|
||||
value: { kind: "varRef", localId: caughtLocal.id, type: CAUGHT, loc },
|
||||
test: "instanceof",
|
||||
className: info.def.name,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "caughtTest",
|
||||
value: { kind: "varRef", localId: caughtLocal.id, type: CAUGHT, loc },
|
||||
test: "instanceof",
|
||||
className: target.def.name,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
const test = caughtInstanceOf(target);
|
||||
return duplex
|
||||
? { kind: "logical", op: "||", left: test, right: caughtInstanceOf(duplex), type: BOOL, loc }
|
||||
: test;
|
||||
}
|
||||
const left = lowerer.lowerExpr(expr.left);
|
||||
// `u instanceof Error` on an `unknown` value: the checked-dynamic tree's error encoding
|
||||
@@ -12998,23 +13049,33 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
const lhsInfo = lowerer.classes.get(arm.className);
|
||||
if (!lhsInfo)
|
||||
throw new InternalCompilerError(`lowerer bug: unknown class ${arm.className}`);
|
||||
const result: IrExpr =
|
||||
lowerer.inHierarchy(lhsInfo) && lowerer.inHierarchy(target)
|
||||
const armTest = (info: ClassInfo): IrExpr =>
|
||||
lowerer.inHierarchy(lhsInfo) && lowerer.inHierarchy(info)
|
||||
? {
|
||||
kind: "instanceOf",
|
||||
value: { kind: "unionNarrow", unionId, tag, value, type: arm, loc },
|
||||
className: target.def.name,
|
||||
className: info.def.name,
|
||||
type: BOOL,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "boolLit",
|
||||
value:
|
||||
arm.className === target.def.name ||
|
||||
lowerer.isSubclassOf(arm.className, target.def.name),
|
||||
arm.className === info.def.name ||
|
||||
lowerer.isSubclassOf(arm.className, info.def.name),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
const result = runtimeTests(arm.className)
|
||||
.map(armTest)
|
||||
.reduce((left: IrExpr, right: IrExpr): IrExpr => ({
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left,
|
||||
right,
|
||||
type: BOOL,
|
||||
loc,
|
||||
}));
|
||||
body.push({
|
||||
kind: "if",
|
||||
cond: { kind: "unionIsTag", unionId, tag, negated: false, value, type: BOOL, loc },
|
||||
@@ -13060,11 +13121,39 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
if (!lhsInfo)
|
||||
throw new InternalCompilerError(`lowerer bug: unknown class ${left.type.className}`);
|
||||
if (lowerer.inHierarchy(lhsInfo) && lowerer.inHierarchy(target)) {
|
||||
return { kind: "instanceOf", value: left, className: target.def.name, type: BOOL, loc };
|
||||
const tests = runtimeTests(left.type.className);
|
||||
if (tests.length === 1) {
|
||||
return { kind: "instanceOf", value: left, className: tests[0]!.def.name, type: BOOL, loc };
|
||||
}
|
||||
const local = lowerer.declareHiddenLocal("%writableTest", left.type);
|
||||
const ref = varRef(local.id, left.type, loc);
|
||||
const test = (className: string): IrExpr => ({
|
||||
kind: "instanceOf",
|
||||
value: ref,
|
||||
className,
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: left, loc }],
|
||||
result: {
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left: test(tests[0]!.def.name),
|
||||
right: test(tests[1]!.def.name),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const leftClass = left.type.className;
|
||||
const isInstance = (className: string): boolean =>
|
||||
leftClass === className || lowerer.isSubclassOf(leftClass, className);
|
||||
const value =
|
||||
left.type.className === target.def.name ||
|
||||
lowerer.isSubclassOf(left.type.className, target.def.name);
|
||||
isInstance(target.def.name) || (duplex !== undefined && isInstance(duplex.def.name));
|
||||
if (left.kind === "varRef") {
|
||||
return readsClassAfter(lowerer, target, expr.right, [], {
|
||||
kind: "boolLit",
|
||||
|
||||
@@ -115,6 +115,16 @@ export function lowerGenMethodCall(
|
||||
if (call.arguments.some(ts.isSpreadElement)) {
|
||||
lowerer.unsupported("SC1090", call, "spread arguments");
|
||||
}
|
||||
// Iterator helpers and the built-in iterators have no throw method; an
|
||||
// iterator-typed value may hold one, so only a generator's own member
|
||||
// lowers.
|
||||
if (name === "throw" && !isGeneratorMember(lowerer, access)) {
|
||||
lowerer.noLowering(
|
||||
`${lowerer.checker.typeToString(lowerer.typeOf(access.expression))}.throw`,
|
||||
call,
|
||||
"throw() lowers on values typed as generators (iterator helpers and built-in iterators have no throw method)",
|
||||
);
|
||||
}
|
||||
const loc = locOf(call);
|
||||
const gen = lowerer.lowerExpr(access.expression);
|
||||
if (gen.type.kind !== "generator") return null;
|
||||
@@ -187,6 +197,23 @@ export function lowerGenMethodCall(
|
||||
return { kind: "genResume", mode: name, gen, arg, type: resultT, loc };
|
||||
}
|
||||
|
||||
/** Whether `access` names a member the lib declares on Generator or
|
||||
* AsyncGenerator itself. */
|
||||
function isGeneratorMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean {
|
||||
const member = lowerer.checker.getSymbolAtLocation(access.name);
|
||||
return (
|
||||
member !== undefined &&
|
||||
lowerer.checker
|
||||
.declarationsOf(member)
|
||||
.some(
|
||||
(d) =>
|
||||
ts.isInterfaceDeclaration(d.parent) &&
|
||||
(d.parent.name.text === "Generator" || d.parent.name.text === "AsyncGenerator") &&
|
||||
lowerer.isStdlibFile(d.getSourceFile()),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** Extraction of a suspended resume's yield value out of the result
|
||||
* record's V slot, typed back to the yield channel: V IS the channel for
|
||||
* dyn (and for a union channel already equal to V); a non-union channel
|
||||
@@ -319,7 +346,7 @@ export function genValue(gen: IrExpr & { type: GenType }, loc: SrcLoc): IrExpr {
|
||||
return { kind: "genResume", mode: "value", gen, arg: null, type: gen.type.yieldT, loc };
|
||||
}
|
||||
|
||||
function generatorLoopBinding(
|
||||
export function generatorLoopBinding(
|
||||
lowerer: Lowerer,
|
||||
stmt: ts.ForOfStatement,
|
||||
value: IrExpr,
|
||||
@@ -572,7 +599,9 @@ export function lowerForOfGenerator(
|
||||
catchBody: null,
|
||||
catchLocalId: null,
|
||||
// IteratorClose: every exit that leaves the generator unfinished
|
||||
// closes it — finallys run; the .return() result is dropped.
|
||||
// closes it — finallys run; the .return() result is dropped, and
|
||||
// after a throw from the body an error from the close is too.
|
||||
dropFinallyErrors: true,
|
||||
finallyBody: [
|
||||
{
|
||||
kind: "if",
|
||||
|
||||
@@ -113,6 +113,7 @@ import {
|
||||
lowerForOfSearchParams,
|
||||
lowerForOfSet,
|
||||
} from "./containers/for-of.js";
|
||||
import { lowerForOfHelperChain } from "./containers/iterator-helpers.js";
|
||||
import { objectIterOverIndexShape } from "./containers/indexed-objects.js";
|
||||
import { strCharsCall } from "./containers/array-construction.js";
|
||||
import {
|
||||
@@ -680,10 +681,14 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
|
||||
out,
|
||||
};
|
||||
lowerer.activeStmtLists.push(entry);
|
||||
const inFunction = stmts.length > 0 && enclosedByFunction(stmts[0]!);
|
||||
let unreachable = false;
|
||||
try {
|
||||
for (let i = 0; i < stmts.length; i++) {
|
||||
const stmt = stmts[i]!;
|
||||
entry.index = i;
|
||||
if (unreachable && skipsWhenUnreachable(stmt)) continue;
|
||||
if (inFunction && isAbruptStatement(stmt)) unreachable = true;
|
||||
if (!lowerer.suppressStats) {
|
||||
lowerer.stats.statementsTotal++;
|
||||
if (sf !== undefined) lowerer.bumpFileStat(sf.fileName, "total");
|
||||
@@ -805,6 +810,54 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Whether a statement list belongs to a function body (at any block
|
||||
* depth) rather than to a module's top level. */
|
||||
function enclosedByFunction(stmt: ts.Statement): boolean {
|
||||
let owner: ts.Node | undefined = stmt.parent;
|
||||
while (owner !== undefined && !ts.isFunctionLike(owner) && !ts.isSourceFile(owner))
|
||||
owner = owner.parent;
|
||||
return owner !== undefined && ts.isFunctionLike(owner);
|
||||
}
|
||||
|
||||
/** `return`, `throw`, `break` and `continue`: the rest of the enclosing
|
||||
* statement list never runs. */
|
||||
function isAbruptStatement(stmt: ts.Statement): boolean {
|
||||
return (
|
||||
ts.isReturnStatement(stmt) ||
|
||||
ts.isThrowStatement(stmt) ||
|
||||
ts.isBreakStatement(stmt) ||
|
||||
ts.isContinueStatement(stmt)
|
||||
);
|
||||
}
|
||||
|
||||
/** Statements after an abrupt statement in a function body that lowering
|
||||
* skips: they never execute, so neither their unsupported constructs nor the
|
||||
* functions they reference become part of the program (production builds of
|
||||
* libraries keep development-only code behind `throw` stubs this way).
|
||||
* Hoisting keeps its meaning: a reference above the abrupt statement to a
|
||||
* later function declaration lowers that declaration eagerly
|
||||
* (predeclareForwardFnDecl), and references to a later `var`, `let` or
|
||||
* `const` binding resolve through the forward-hoisting machinery to its
|
||||
* undefined or TDZ state, which skipping its initializer preserves. Class,
|
||||
* enum, module and import/export declarations and generic function
|
||||
* declarations still lower. */
|
||||
function skipsWhenUnreachable(stmt: ts.Statement): boolean {
|
||||
if (ts.isFunctionDeclaration(stmt)) return stmt.typeParameters === undefined;
|
||||
if (ts.isVariableStatement(stmt))
|
||||
return !(stmt.modifiers ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
||||
return !(
|
||||
ts.isClassDeclaration(stmt) ||
|
||||
ts.isEnumDeclaration(stmt) ||
|
||||
ts.isModuleDeclaration(stmt) ||
|
||||
ts.isImportDeclaration(stmt) ||
|
||||
ts.isImportEqualsDeclaration(stmt) ||
|
||||
ts.isExportDeclaration(stmt) ||
|
||||
ts.isExportAssignment(stmt) ||
|
||||
ts.isInterfaceDeclaration(stmt) ||
|
||||
ts.isTypeAliasDeclaration(stmt)
|
||||
);
|
||||
}
|
||||
|
||||
const TDZ_KINDS = new Set<IrType["kind"]>([
|
||||
"func",
|
||||
"string",
|
||||
@@ -8497,6 +8550,12 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
|
||||
}
|
||||
}
|
||||
}
|
||||
// A head that is a chain of iterator-helper stages runs as one loop
|
||||
// over the chain's source.
|
||||
{
|
||||
const fused = lowerForOfHelperChain(lowerer, stmt, labels);
|
||||
if (fused) return fused;
|
||||
}
|
||||
// `for (const k of m.keys())` / `.values()` / `.entries()`: the
|
||||
// iterator methods consumed DIRECTLY by a for-of head ride the
|
||||
// container's live walk — exactly `for..of m` with the projection
|
||||
|
||||
@@ -26,6 +26,9 @@ import { isSettledIteratorValueRead } from "./lower-generators.js";
|
||||
import { concreteCollectionNarrow } from "./collection-narrowing.js";
|
||||
import { lowerWorkerMetadata } from "./builtins/workers.js";
|
||||
import { fenceStaticBlobMember, lowerBlobNew, lowerStaticBlobCall } from "./builtins/blob.js";
|
||||
import { refuseWebAssemblyUse } from "./builtins/webassembly.js";
|
||||
import { discoverWasmModules, type WasmModuleReport } from "../../wasm/discover.js";
|
||||
import { resolveRequireRuntime } from "../runtime-resolve.js";
|
||||
import { lowerKeyObjectCall } from "./builtins/key-object.js";
|
||||
import { hashStreamOf } from "./builtins/hash-stream.js";
|
||||
import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js";
|
||||
@@ -726,6 +729,8 @@ export interface LowerResult {
|
||||
/** Coverage only (LowerOptions.coverage): the unreached remainder,
|
||||
* lowered in a throwaway pass — blockers in it can never fail a build. */
|
||||
unreached?: { diagnostics: ScrDiagnostic[]; stats: LowerStats; runtimeFences?: ScrDiagnostic[] };
|
||||
/** Coverage only: the WebAssembly modules the build can see (wasm/discover.ts). */
|
||||
wasmModules?: WasmModuleReport[];
|
||||
/** Compile-time-detectable behavior differences from Node (SC6xxx
|
||||
* divergence warnings) in the program's own modules. Never failures. */
|
||||
divergences?: ScrDiagnostic[];
|
||||
@@ -746,6 +751,8 @@ export interface LowerOptions {
|
||||
* diagnostics and stats under `unreached` — the whole-program analysis
|
||||
* builds deliberately gave up. */
|
||||
coverage?: boolean;
|
||||
/** Coverage only: module files named by --wasm (absolute). */
|
||||
wasmFiles?: readonly string[];
|
||||
/** The platform the build TARGETS ("win32" under a windows cross triple,
|
||||
* the host platform otherwise — see buildTargetPlatform in index.ts).
|
||||
* The whole program compiles for one platform, so the platform-keyed
|
||||
@@ -1127,6 +1134,23 @@ export function lowerToIr(
|
||||
},
|
||||
};
|
||||
if (tiers !== undefined) covered.tiers = tiers;
|
||||
const wasmModules = discoverWasmModules({
|
||||
files: moduleOrder.includes(entry) ? moduleOrder : [...moduleOrder, entry],
|
||||
entry: entry.fileName,
|
||||
configured: options.wasmFiles ?? [],
|
||||
resolveRequest: (fromFile, specifier) => {
|
||||
const resolved = resolveRequireRuntime(
|
||||
fromFile,
|
||||
specifier,
|
||||
targetPlatform,
|
||||
undefined,
|
||||
options.frontendServices,
|
||||
);
|
||||
return resolved.ok ? resolved.value : null;
|
||||
},
|
||||
});
|
||||
timing("wasm-discovery", { modules: wasmModules.length });
|
||||
if (wasmModules.length > 0) covered.wasmModules = wasmModules;
|
||||
return covered;
|
||||
}
|
||||
|
||||
@@ -11407,6 +11431,7 @@ export class Lowerer {
|
||||
// dispatch. The general identifier-call paths have no lowering for a
|
||||
// promise-returning ambient global, and `import` is a keyword callee no
|
||||
// identifier path matches.
|
||||
refuseWebAssemblyUse(this, expr.expression);
|
||||
return (
|
||||
lowerLifetimeRegionCall(this, expr) ??
|
||||
lowerInternTableCall(this, expr) ??
|
||||
@@ -11632,6 +11657,7 @@ export class Lowerer {
|
||||
}
|
||||
|
||||
lowerNew(expr: ts.NewExpression): IrExpr {
|
||||
refuseWebAssemblyUse(this, expr.expression, "new ");
|
||||
const builtin =
|
||||
lowerLifetimeRegionNew(this, expr) ??
|
||||
lowerInternTableNew(this, expr) ??
|
||||
|
||||
@@ -920,6 +920,24 @@ function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean
|
||||
return false;
|
||||
}
|
||||
|
||||
/** A type declared in the shipped WebAssembly namespace (scriptc.d.ts):
|
||||
* its modules, instances, memories, tables, globals, error classes and the
|
||||
* Exports/Imports records. */
|
||||
export function isWebAssemblyType(
|
||||
type: ts.Type,
|
||||
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
|
||||
): boolean {
|
||||
const symbol = type.getAliasSymbol() ?? type.getSymbol();
|
||||
if (!symbol) return false;
|
||||
return ctx.checker
|
||||
.declarationsOf(symbol)
|
||||
.some(
|
||||
(d) =>
|
||||
ctx.isStdlibFile(d.getSourceFile()) &&
|
||||
isDeclaredInAmbientNamespace(d as ts.Declaration, "WebAssembly"),
|
||||
);
|
||||
}
|
||||
|
||||
/** True when a type contains a record anywhere (the trigger for the
|
||||
* shape-mismatch diagnostic SC2002 — non-record mismatches are tsc's or
|
||||
* the validator's business). */
|
||||
@@ -1419,6 +1437,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
return { kind: "object", className: "%Error" };
|
||||
}
|
||||
if (isOsConstantsType(type, ctx) || isLegacyUrlType(type, ctx)) return DYN;
|
||||
// The WebAssembly API's values have the checked-dynamic representation, so
|
||||
// declarations that merely mention them compile; each reached use of the
|
||||
// API itself is refused at its site (builtins/webassembly.ts).
|
||||
if (isWebAssemblyType(type, ctx)) return DYN;
|
||||
// A union over one of them (quickjs-wasi's `BufferSource | WebAssembly.Module`
|
||||
// option) holds that dynamic value too, so the whole union is dynamic.
|
||||
if (type.isUnionType() && ts.constituentTypes(type).some((t) => isWebAssemblyType(t, ctx)))
|
||||
return DYN;
|
||||
// Immutable<T> (@scriptc/types) is a deep-readonly view: the value is
|
||||
// still the T it maps.
|
||||
const immutableSource = immutableSourceType(type, ctx);
|
||||
@@ -3224,7 +3250,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
isStdlibInterface("IterableIterator") ||
|
||||
// An Iterator<T> annotation holds a single-pass iterator object: a
|
||||
// generator, or a native iterator adapted into one.
|
||||
isStdlibInterface("Iterator")
|
||||
isStdlibInterface("Iterator") ||
|
||||
// IteratorObject<T> is what the iterator helpers return: a helper
|
||||
// generator over its source (containers/iterator-helpers.ts).
|
||||
isStdlibInterface("IteratorObject")
|
||||
) {
|
||||
const args = checker.getTypeArguments(widened as ts.TypeReference);
|
||||
const channels = genChannels(args[0], args[1], args[2], ctx);
|
||||
|
||||
@@ -1651,7 +1651,15 @@ export interface IrFunction {
|
||||
* out, `nextT` what `.next(v)` sends in (the yield expression's result
|
||||
* type). `async` alongside this field marks an async generator: its
|
||||
* resume methods queue requests and return promises. */
|
||||
generator?: { yieldT: IrType; nextT: IrType; resultType: IrType & { kind: "record" } };
|
||||
generator?: {
|
||||
yieldT: IrType;
|
||||
nextT: IrType;
|
||||
resultType: IrType & { kind: "record" };
|
||||
/** The first parameter is a generator this one reads from (an
|
||||
* iterator helper's source): `.return()` before the first `.next()`
|
||||
* closes it. */
|
||||
closesSource?: true;
|
||||
};
|
||||
/** Source function name for native error stack frames. */
|
||||
sourceName?: string;
|
||||
/** Ordinary authored function declarations/expressions own a prototype. */
|
||||
@@ -1924,6 +1932,11 @@ export type IrStmt =
|
||||
/** Resource-management cleanup: if the guarded body and cleanup both
|
||||
* throw, raise a SuppressedError instead of replacing the first error. */
|
||||
suppressFinallyErrors?: true;
|
||||
/** Iterator closing: if the guarded body threw and the cleanup throws
|
||||
* too, the cleanup's error is dropped and the first one propagates.
|
||||
* After a generator .return() the cleanup's error replaces it, and on
|
||||
* normal exits it propagates. */
|
||||
dropFinallyErrors?: true;
|
||||
loc: SrcLoc;
|
||||
};
|
||||
|
||||
@@ -3925,6 +3938,8 @@ export const DYN_HANDLE_KINDS: ReadonlyMap<string, { tag: string; cls: string }>
|
||||
["http2Stream", { tag: "SCR_DYNH_H2_STREAM", cls: "Http2Stream" }],
|
||||
["httpClientReq", { tag: "SCR_DYNH_HTTP_CLIENT", cls: "ClientRequest" }],
|
||||
["dir", { tag: "SCR_DYNH_DIR", cls: "Dir" }],
|
||||
["cryptoHash", { tag: "SCR_DYNH_HASH", cls: "Hash" }],
|
||||
["cryptoHmac", { tag: "SCR_DYNH_HMAC", cls: "Hmac" }],
|
||||
]);
|
||||
|
||||
/** A class value that crosses an `unknown` slot as a compiler-owned typed
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { analyzeWasm, isTranslatableFeature, isWasmHeader } from "./binary.js";
|
||||
|
||||
/** V8's verdict (the compiler's type world declares no WebAssembly value). */
|
||||
const v8Validate = (bytes: Uint8Array): boolean =>
|
||||
(
|
||||
globalThis as unknown as { WebAssembly: { validate(bytes: Uint8Array): boolean } }
|
||||
).WebAssembly.validate(bytes);
|
||||
|
||||
// A minimal module assembler: enough structure to aim at one decoder rule
|
||||
// per test. Every module is also checked against V8 (Node is the oracle).
|
||||
|
||||
function leb(n: number): number[] {
|
||||
const out: number[] = [];
|
||||
do {
|
||||
let b = n % 128;
|
||||
n = Math.floor(n / 128);
|
||||
if (n > 0) b |= 0x80;
|
||||
out.push(b);
|
||||
} while (n > 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
function name(text: string): number[] {
|
||||
const bytes = [...new TextEncoder().encode(text)];
|
||||
return [...leb(bytes.length), ...bytes];
|
||||
}
|
||||
|
||||
function vec(items: number[][]): number[] {
|
||||
return [...leb(items.length), ...items.flat()];
|
||||
}
|
||||
|
||||
function section(id: number, body: number[]): number[] {
|
||||
return [id, ...leb(body.length), ...body];
|
||||
}
|
||||
|
||||
function module(...sections: number[][]): Uint8Array {
|
||||
return new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, ...sections.flat()]);
|
||||
}
|
||||
|
||||
const I32 = 0x7f;
|
||||
const I64 = 0x7e;
|
||||
const F32 = 0x7d;
|
||||
const V128 = 0x7b;
|
||||
const EXTERNREF = 0x6f;
|
||||
|
||||
const funcType = (params: number[], results: number[]): number[] => [
|
||||
0x60,
|
||||
...vec(params.map((p) => [p])),
|
||||
...vec(results.map((r) => [r])),
|
||||
];
|
||||
const types = (...fts: number[][]): number[] => section(1, vec(fts));
|
||||
const funcs = (...typeIndices: number[]): number[] =>
|
||||
section(3, vec(typeIndices.map((t) => leb(t))));
|
||||
const body = (code: number[], locals: number[][] = []): number[] => {
|
||||
const content = [...vec(locals), ...code, 0x0b];
|
||||
return [...leb(content.length), ...content];
|
||||
};
|
||||
const code = (...bodies: number[][]): number[] => section(10, vec(bodies));
|
||||
const memory = (...limits: number[][]): number[] => section(5, vec(limits));
|
||||
const exportsOf = (...entries: number[][]): number[] => section(7, vec(entries));
|
||||
|
||||
function check(bytes: Uint8Array) {
|
||||
const summary = analyzeWasm(bytes);
|
||||
const accepted = summary.status === "valid" || summary.status === "unsupported";
|
||||
expect(accepted, summary.error ?? "").toBe(v8Validate(bytes));
|
||||
return summary;
|
||||
}
|
||||
|
||||
describe("analyzeWasm", () => {
|
||||
test("an empty module is valid and uses no proposal", () => {
|
||||
const s = check(module());
|
||||
expect(s).toMatchObject({ status: "valid", error: null, features: [], functions: 0, size: 8 });
|
||||
expect(isWasmHeader(module())).toBe(true);
|
||||
});
|
||||
|
||||
test("counts functions, imports, exports, memories, tables and globals", () => {
|
||||
const s = check(
|
||||
module(
|
||||
types(funcType([I32, I32], [I32]), funcType([], [])),
|
||||
section(
|
||||
2,
|
||||
vec([
|
||||
[...name("env"), ...name("log"), 0x00, 0x01],
|
||||
[...name("wasi_snapshot_preview1"), ...name("proc_exit"), 0x00, 0x01],
|
||||
]),
|
||||
),
|
||||
funcs(0),
|
||||
section(4, vec([[0x70, 0x00, 0x01]])),
|
||||
memory([0x00, 0x01]),
|
||||
section(6, vec([[I32, 0x01, 0x41, 0x00, 0x0b]])),
|
||||
exportsOf([...name("add"), 0x00, 0x02], [...name("memory"), 0x02, 0x00]),
|
||||
code(body([0x20, 0x00, 0x20, 0x01, 0x6a])),
|
||||
),
|
||||
);
|
||||
expect(s).toMatchObject({
|
||||
status: "valid",
|
||||
functions: 1,
|
||||
imports: 2,
|
||||
exports: 2,
|
||||
importModules: ["env", "wasi_snapshot_preview1"],
|
||||
memories: 1,
|
||||
tables: 1,
|
||||
globals: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
[
|
||||
"sign-extension",
|
||||
module(types(funcType([I32], [I32])), funcs(0), code(body([0x20, 0x00, 0xc0]))),
|
||||
],
|
||||
[
|
||||
"nontrapping-fptoint",
|
||||
module(types(funcType([F32], [I32])), funcs(0), code(body([0x20, 0x00, 0xfc, 0x00]))),
|
||||
],
|
||||
[
|
||||
"bulk-memory",
|
||||
module(
|
||||
types(funcType([], [])),
|
||||
funcs(0),
|
||||
memory([0x00, 0x01]),
|
||||
code(body([0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0xfc, 0x0b, 0x00])),
|
||||
),
|
||||
],
|
||||
[
|
||||
"multi-value",
|
||||
module(types(funcType([], [I32, I64])), funcs(0), code(body([0x41, 0x01, 0x42, 0x02]))),
|
||||
],
|
||||
["reference-types", module(section(4, vec([[EXTERNREF, 0x00, 0x01]])))],
|
||||
["extended-const", module(section(6, vec([[I32, 0x00, 0x41, 0x01, 0x41, 0x02, 0x6a, 0x0b]])))],
|
||||
[
|
||||
"mutable-globals",
|
||||
module(section(2, vec([[...name("env"), ...name("sp"), 0x03, I32, 0x01]]))),
|
||||
],
|
||||
])("detects %s and stays translatable", (feature, bytes) => {
|
||||
const s = check(bytes);
|
||||
expect(s.status).toBe("valid");
|
||||
expect(s.features).toContain(feature);
|
||||
expect(isTranslatableFeature(feature)).toBe(true);
|
||||
});
|
||||
|
||||
test.each([
|
||||
[
|
||||
"simd",
|
||||
module(
|
||||
types(funcType([], [V128])),
|
||||
funcs(0),
|
||||
code(body([0xfd, 0x0c, ...Array.from({ length: 16 }, () => 0)])),
|
||||
),
|
||||
],
|
||||
["threads", module(memory([0x03, 0x01, 0x01]))],
|
||||
["exceptions", module(types(funcType([], [])), section(13, vec([[0x00, 0x00]])))],
|
||||
["tail-call", module(types(funcType([], [])), funcs(0), code(body([0x12, 0x00])))],
|
||||
["memory64", module(memory([0x04, 0x01]))],
|
||||
["multi-memory", module(memory([0x00, 0x01], [0x00, 0x01]))],
|
||||
["gc", module(section(1, vec([[0x5f, 0x00]])))],
|
||||
])("reports %s as outside the translatable set", (feature, bytes) => {
|
||||
const s = check(bytes);
|
||||
expect(s.status).toBe("unsupported");
|
||||
expect(s.unsupported).toEqual([feature]);
|
||||
expect(isTranslatableFeature(feature)).toBe(false);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["bad magic", new Uint8Array([0x00, 0x61, 0x73, 0x6e, 1, 0, 0, 0]), "malformed", "magic"],
|
||||
["bad version", new Uint8Array([0x00, 0x61, 0x73, 0x6d, 2, 0, 0, 0]), "malformed", "version"],
|
||||
["sections out of order", module(funcs(), types()), "malformed", "unexpected section"],
|
||||
["section size mismatch", module([0x01, 0x02, 0x00, 0x00]), "malformed", "section size"],
|
||||
[
|
||||
"overlong integer",
|
||||
module([0x01, 0x06, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00]),
|
||||
"malformed",
|
||||
"integer",
|
||||
],
|
||||
["invalid UTF-8 name", module(exportsOf([0x01, 0xff, 0x00, 0x00])), "malformed", "UTF-8"],
|
||||
[
|
||||
"function and code counts differ",
|
||||
module(types(funcType([], [])), funcs(0, 0), code(body([]))),
|
||||
"malformed",
|
||||
"inconsistent",
|
||||
],
|
||||
[
|
||||
"illegal opcode",
|
||||
module(types(funcType([], [])), funcs(0), code(body([0xff]))),
|
||||
"malformed",
|
||||
"illegal opcode",
|
||||
],
|
||||
[
|
||||
"memory.init without a data count section",
|
||||
module(
|
||||
types(funcType([], [])),
|
||||
funcs(0),
|
||||
memory([0x00, 0x01]),
|
||||
code(body([0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0xfc, 0x08, 0x00, 0x00])),
|
||||
),
|
||||
"malformed",
|
||||
"data count",
|
||||
],
|
||||
[
|
||||
"type mismatch",
|
||||
module(types(funcType([], [I32])), funcs(0), code(body([0x42, 0x00]))),
|
||||
"invalid",
|
||||
"type mismatch",
|
||||
],
|
||||
[
|
||||
"unknown function",
|
||||
module(types(funcType([], [])), funcs(0), code(body([0x10, 0x05]))),
|
||||
"invalid",
|
||||
"unknown function",
|
||||
],
|
||||
[
|
||||
"duplicate export",
|
||||
module(
|
||||
types(funcType([], [])),
|
||||
funcs(0),
|
||||
exportsOf([...name("f"), 0x00, 0x00], [...name("f"), 0x00, 0x00]),
|
||||
code(body([])),
|
||||
),
|
||||
"invalid",
|
||||
"duplicate export",
|
||||
],
|
||||
["memory above 4 GiB", module(memory([0x00, ...leb(65537)])), "invalid", "65536 pages"],
|
||||
[
|
||||
"undeclared function reference",
|
||||
module(types(funcType([], [])), funcs(0), code(body([0xd2, 0x00, 0x1a]))),
|
||||
"invalid",
|
||||
"undeclared",
|
||||
],
|
||||
])("rejects %s", (_label, bytes, status, message) => {
|
||||
const s = check(bytes);
|
||||
expect(s.status).toBe(status);
|
||||
expect(s.error).toContain(message);
|
||||
});
|
||||
|
||||
test("never throws on truncated input", () => {
|
||||
const whole = module(
|
||||
types(funcType([I32, I32], [I32])),
|
||||
funcs(0),
|
||||
code(body([0x20, 0x00, 0x20, 0x01, 0x6a])),
|
||||
);
|
||||
for (let n = 0; n < whole.length; n++) check(whole.subarray(0, n));
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { decodeBase64, packageRootOf } from "./discover.js";
|
||||
|
||||
describe("decodeBase64", () => {
|
||||
test("decodes standard and URL-safe spellings, with or without padding", () => {
|
||||
const bytes = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 0xfb, 0xff]);
|
||||
const standard = Buffer.from(bytes).toString("base64");
|
||||
expect(decodeBase64(standard)).toEqual(bytes);
|
||||
expect(decodeBase64(standard.replace(/=+$/, ""))).toEqual(bytes);
|
||||
expect(decodeBase64(Buffer.from(bytes).toString("base64url"))).toEqual(bytes);
|
||||
});
|
||||
|
||||
test("answers null for text that is not base64", () => {
|
||||
expect(decodeBase64("AGFzbQ*")).toBeNull();
|
||||
expect(decodeBase64("AGFzb")).toBeNull();
|
||||
expect(decodeBase64("AGFzbQ===")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("packageRootOf", () => {
|
||||
test("finds the innermost package, scoped or not", () => {
|
||||
expect(packageRootOf("/p/node_modules/quickjs-wasi/dist/index.js")).toBe(
|
||||
"/p/node_modules/quickjs-wasi",
|
||||
);
|
||||
expect(packageRootOf("/p/node_modules/a/node_modules/@scope/b/lib/x.js")).toBe(
|
||||
"/p/node_modules/a/node_modules/@scope/b",
|
||||
);
|
||||
expect(packageRootOf("/p/src/main.ts")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,426 @@
|
||||
/* Build-time discovery of the WebAssembly modules a program can load.
|
||||
*
|
||||
* `scriptc coverage` reports every module whose bytes the build can see,
|
||||
* from four sources:
|
||||
*
|
||||
* 1. path: a `.wasm` path the compiler resolves from the source text
|
||||
* (`require.resolve`, `createRequire(...).resolve`, `import.meta.resolve`,
|
||||
* `new URL("./x.wasm", import.meta.url)`, and `path.join(__dirname, ...)`
|
||||
* or `__dirname + "/x.wasm"`);
|
||||
* 2. inline: a string literal that decodes as base64 to a module, or an
|
||||
* array literal of its bytes;
|
||||
* 3. package: every `.wasm` file shipped by a reached npm package whose
|
||||
* program files mention `WebAssembly` (this finds paths computed at run
|
||||
* time, such as a loader's default argument);
|
||||
* 4. config: the `scriptc.wasm` list in the entry's package.json and the
|
||||
* `--wasm <file>` flag.
|
||||
*
|
||||
* Each module is decoded and validated (binary.ts) and reported once, by
|
||||
* content, with every source that found it. Nothing here changes what a
|
||||
* build emits. */
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import * as ts from "../frontend/ts7/adapter.js";
|
||||
import {
|
||||
trackedAccessibleEntries,
|
||||
trackedFileExists,
|
||||
trackedReadFile,
|
||||
} from "../frontend/input-tracker.js";
|
||||
import { analyzeWasm, isWasmHeader, type WasmModuleSummary } from "./binary.js";
|
||||
|
||||
export type WasmSourceKind = "path" | "inline" | "package" | "config";
|
||||
|
||||
export interface WasmSource {
|
||||
kind: WasmSourceKind;
|
||||
/** The source file (path, inline) or the file that names the module. */
|
||||
file: string;
|
||||
/** 1-based line of the site, when there is one. */
|
||||
line?: number;
|
||||
}
|
||||
|
||||
/** One discovered module. JSON-shaped: the coverage verdict cache stores it. */
|
||||
export interface WasmModuleReport extends WasmModuleSummary {
|
||||
sha256: string;
|
||||
/** The module file, absent for inline modules. */
|
||||
path?: string;
|
||||
sources: WasmSource[];
|
||||
}
|
||||
|
||||
export interface WasmDiscoveryInput {
|
||||
/** The program's source files (entry and modules). */
|
||||
files: readonly ts.SourceFile[];
|
||||
/** The entry file. */
|
||||
entry: string;
|
||||
/** --wasm files, absolute. */
|
||||
configured: readonly string[];
|
||||
/** require.resolve from `fromFile`, or null when it does not resolve. */
|
||||
resolveRequest: (fromFile: string, specifier: string) => string | null;
|
||||
}
|
||||
|
||||
const SCAN_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
|
||||
ts.SyntaxKind.StringLiteral,
|
||||
ts.SyntaxKind.NoSubstitutionTemplateLiteral,
|
||||
ts.SyntaxKind.CallExpression,
|
||||
ts.SyntaxKind.NewExpression,
|
||||
ts.SyntaxKind.BinaryExpression,
|
||||
ts.SyntaxKind.ArrayLiteralExpression,
|
||||
]);
|
||||
|
||||
/** The base64 spelling of a module's first bytes ("\0asm"). */
|
||||
const BASE64_MAGIC = "AGFzbQ";
|
||||
const BYTES_MAGIC = /\[\s*(?:0|0x0+)\s*,\s*(?:97|0x61)\s*,\s*(?:115|0x73)\s*,\s*(?:109|0x6d)\s*,/i;
|
||||
/** Package directories deeper than this are not searched for modules. */
|
||||
const PACKAGE_SCAN_DEPTH = 6;
|
||||
/** Larger files are reported by size but not read. */
|
||||
const MAX_MODULE_BYTES = 256 * 1024 * 1024;
|
||||
|
||||
class Discovery {
|
||||
input: WasmDiscoveryInput;
|
||||
byHash = new Map<string, WasmModuleReport>();
|
||||
byPath = new Map<string, string>();
|
||||
order: string[] = [];
|
||||
|
||||
constructor(input: WasmDiscoveryInput) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
/** Records a module and one source that found it; answers its hash. */
|
||||
addBytes(bytes: Uint8Array, path: string | undefined, source: WasmSource): string {
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
let report = this.byHash.get(sha256);
|
||||
if (report === undefined) {
|
||||
const summary = analyzeWasm(bytes);
|
||||
report = {
|
||||
size: summary.size,
|
||||
status: summary.status,
|
||||
error: summary.error,
|
||||
features: summary.features,
|
||||
unsupported: summary.unsupported,
|
||||
functions: summary.functions,
|
||||
imports: summary.imports,
|
||||
exports: summary.exports,
|
||||
importModules: summary.importModules,
|
||||
memories: summary.memories,
|
||||
tables: summary.tables,
|
||||
globals: summary.globals,
|
||||
customSections: summary.customSections,
|
||||
sha256,
|
||||
sources: [],
|
||||
};
|
||||
this.byHash.set(sha256, report);
|
||||
this.order.push(sha256);
|
||||
}
|
||||
if (report.path === undefined && path !== undefined) report.path = path;
|
||||
addSource(report, source);
|
||||
return sha256;
|
||||
}
|
||||
|
||||
addFile(path: string, source: WasmSource): void {
|
||||
path = resolve(path);
|
||||
const known = this.byPath.get(path);
|
||||
if (known !== undefined) {
|
||||
addSource(this.byHash.get(known)!, source);
|
||||
return;
|
||||
}
|
||||
// The tracked read makes the coverage verdict cache depend on the file.
|
||||
if (trackedReadFile(path) === null) return;
|
||||
let bytes: Uint8Array;
|
||||
try {
|
||||
bytes = new Uint8Array(readFileSync(path));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (bytes.length > MAX_MODULE_BYTES) return;
|
||||
this.byPath.set(path, this.addBytes(bytes, path, source));
|
||||
}
|
||||
|
||||
// ── source 1 and 2: the program's source text ───────────────────────
|
||||
|
||||
scanFile(sf: ts.SourceFile): void {
|
||||
const text = sf.text;
|
||||
const paths = text.includes(".wasm");
|
||||
const base64 = text.includes(BASE64_MAGIC);
|
||||
const bytes = BYTES_MAGIC.test(text);
|
||||
if (!paths && !base64 && !bytes) return;
|
||||
const file = sf.fileName;
|
||||
const lineOf = (node: ts.Node): number =>
|
||||
ts.getLineAndCharacterOfPosition(sf, node.getStart()).line + 1;
|
||||
ts.walkPreorder(
|
||||
sf,
|
||||
(node) => {
|
||||
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
||||
if (base64 && node.text.startsWith(BASE64_MAGIC)) {
|
||||
const decoded = decodeBase64(node.text);
|
||||
if (decoded !== null && isWasmHeader(decoded)) {
|
||||
this.addBytes(decoded, undefined, { kind: "inline", file, line: lineOf(node) });
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (ts.isArrayLiteralExpression(node)) {
|
||||
if (bytes) {
|
||||
const literal = byteArrayLiteral(node);
|
||||
if (literal !== null && isWasmHeader(literal)) {
|
||||
this.addBytes(literal, undefined, { kind: "inline", file, line: lineOf(node) });
|
||||
return "skip";
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (!paths) return undefined;
|
||||
const path = this.resolvedPath(sf, node);
|
||||
if (path !== null) this.addFile(path, { kind: "path", file, line: lineOf(node) });
|
||||
return undefined;
|
||||
},
|
||||
SCAN_KINDS,
|
||||
);
|
||||
}
|
||||
|
||||
/** The module path a resolution expression names, when it ends in .wasm. */
|
||||
resolvedPath(sf: ts.SourceFile, node: ts.Node): string | null {
|
||||
const dir = dirname(sf.fileName);
|
||||
if (ts.isCallExpression(node)) {
|
||||
const callee = node.expression;
|
||||
const first = node.arguments[0];
|
||||
if (first === undefined) return null;
|
||||
// require.resolve / createRequire(...).resolve / import.meta.resolve
|
||||
if (ts.isPropertyAccessExpression(callee) && callee.name.text === "resolve") {
|
||||
const specifier = wasmLiteral(first);
|
||||
if (specifier === null) {
|
||||
return this.joinedPath(dir, node.arguments);
|
||||
}
|
||||
const receiver = callee.expression;
|
||||
if (ts.isMetaProperty(receiver) || isRequireLike(receiver)) {
|
||||
if (specifier.startsWith("./") || specifier.startsWith("../")) {
|
||||
return resolve(dir, specifier);
|
||||
}
|
||||
if (isAbsolute(specifier)) return specifier;
|
||||
return this.input.resolveRequest(sf.fileName, specifier);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// path.join(__dirname, "x.wasm") and its resolve/bare spellings
|
||||
if (
|
||||
(ts.isPropertyAccessExpression(callee) &&
|
||||
(callee.name.text === "join" || callee.name.text === "resolve")) ||
|
||||
(ts.isIdentifier(callee) && (callee.text === "join" || callee.text === "resolve"))
|
||||
) {
|
||||
return this.joinedPath(dir, node.arguments);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (ts.isNewExpression(node)) {
|
||||
// new URL("./x.wasm", import.meta.url)
|
||||
if (!ts.isIdentifier(node.expression) || node.expression.text !== "URL") return null;
|
||||
const args = node.arguments;
|
||||
if (args === undefined || args.length !== 2) return null;
|
||||
const specifier = wasmLiteral(args[0]!);
|
||||
const base = args[1]!;
|
||||
if (specifier === null || !isImportMetaUrl(base)) return null;
|
||||
if (specifier.startsWith("./") || specifier.startsWith("../")) return resolve(dir, specifier);
|
||||
return null;
|
||||
}
|
||||
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
||||
// __dirname + "/x.wasm"
|
||||
if (!isDirname(node.left)) return null;
|
||||
const rest = wasmLiteral(node.right);
|
||||
return rest === null ? null : join(dir, rest);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
joinedPath(dir: string, args: readonly ts.Expression[]): string | null {
|
||||
if (args.length < 2 || !isDirname(args[0]!)) return null;
|
||||
const parts: string[] = [dir];
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
const arg = args[i]!;
|
||||
if (!ts.isStringLiteral(arg) && !ts.isNoSubstitutionTemplateLiteral(arg)) return null;
|
||||
parts.push(arg.text);
|
||||
}
|
||||
if (!parts[parts.length - 1]!.endsWith(".wasm")) return null;
|
||||
let path = parts[0]!;
|
||||
for (let i = 1; i < parts.length; i++) path = resolve(path, parts[i]!);
|
||||
return path;
|
||||
}
|
||||
|
||||
// ── source 3: package files ─────────────────────────────────────────
|
||||
|
||||
scanPackages(): void {
|
||||
const roots = new Map<string, boolean>();
|
||||
for (const sf of this.input.files) {
|
||||
const root = packageRootOf(sf.fileName);
|
||||
if (root === null) continue;
|
||||
const mentions = sf.text.includes("WebAssembly");
|
||||
roots.set(root, (roots.get(root) ?? false) || mentions);
|
||||
}
|
||||
for (const [root, mentions] of roots) {
|
||||
if (!mentions) continue;
|
||||
const found: string[] = [];
|
||||
collectWasmFiles(root, 0, found);
|
||||
found.sort();
|
||||
for (const path of found)
|
||||
this.addFile(path, { kind: "package", file: join(root, "package.json") });
|
||||
}
|
||||
}
|
||||
|
||||
// ── source 4: configuration ─────────────────────────────────────────
|
||||
|
||||
scanConfig(): void {
|
||||
const manifest = nearestPackageJson(dirname(this.input.entry));
|
||||
if (manifest !== null) {
|
||||
const text = trackedReadFile(manifest);
|
||||
if (text !== null) {
|
||||
let listed: unknown;
|
||||
try {
|
||||
const parsed = JSON.parse(text) as { scriptc?: { wasm?: unknown } };
|
||||
listed = parsed.scriptc?.wasm;
|
||||
} catch {
|
||||
listed = undefined;
|
||||
}
|
||||
if (Array.isArray(listed)) {
|
||||
for (const entry of listed) {
|
||||
if (typeof entry === "string") {
|
||||
this.addFile(resolve(dirname(manifest), entry), { kind: "config", file: manifest });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const path of this.input.configured) {
|
||||
this.addFile(path, { kind: "config", file: path });
|
||||
}
|
||||
}
|
||||
|
||||
run(): WasmModuleReport[] {
|
||||
for (const sf of this.input.files) this.scanFile(sf);
|
||||
this.scanPackages();
|
||||
this.scanConfig();
|
||||
return this.order.map((sha) => this.byHash.get(sha)!);
|
||||
}
|
||||
}
|
||||
|
||||
function addSource(report: WasmModuleReport, source: WasmSource): void {
|
||||
for (const s of report.sources) {
|
||||
if (s.kind === source.kind && s.file === source.file && s.line === source.line) return;
|
||||
}
|
||||
report.sources.push(source);
|
||||
}
|
||||
|
||||
function wasmLiteral(node: ts.Expression): string | null {
|
||||
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) return null;
|
||||
return node.text.endsWith(".wasm") ? node.text : null;
|
||||
}
|
||||
|
||||
function isDirname(node: ts.Expression): boolean {
|
||||
return ts.isIdentifier(node) && node.text === "__dirname";
|
||||
}
|
||||
|
||||
function isImportMetaUrl(node: ts.Expression): boolean {
|
||||
return (
|
||||
ts.isPropertyAccessExpression(node) &&
|
||||
node.name.text === "url" &&
|
||||
ts.isMetaProperty(node.expression)
|
||||
);
|
||||
}
|
||||
|
||||
/** `require`, a `createRequire(...)` call, or a binding named like one. */
|
||||
function isRequireLike(node: ts.Expression): boolean {
|
||||
if (ts.isIdentifier(node)) return node.text === "require" || /require$/i.test(node.text);
|
||||
if (ts.isCallExpression(node)) {
|
||||
const callee = node.expression;
|
||||
return (
|
||||
(ts.isIdentifier(callee) && callee.text === "createRequire") ||
|
||||
(ts.isPropertyAccessExpression(callee) && callee.name.text === "createRequire")
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The bytes of an array literal of integer literals, or null. */
|
||||
function byteArrayLiteral(node: ts.ArrayLiteralExpression): Uint8Array | null {
|
||||
const elements = node.elements;
|
||||
const out = new Uint8Array(elements.length);
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const element = elements[i]!;
|
||||
if (!ts.isNumericLiteral(element)) return null;
|
||||
const value = Number(element.text);
|
||||
if (!Number.isInteger(value) || value < 0 || value > 255) return null;
|
||||
out[i] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const BASE64_VALUES = (() => {
|
||||
const table = new Int16Array(128).fill(-1);
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
for (let i = 0; i < alphabet.length; i++) table[alphabet.charCodeAt(i)] = i;
|
||||
// base64url spellings
|
||||
table["-".charCodeAt(0)] = 62;
|
||||
table["_".charCodeAt(0)] = 63;
|
||||
return table;
|
||||
})();
|
||||
|
||||
/** Decodes standard or URL-safe base64 (padding optional); null when the
|
||||
* text is not base64. */
|
||||
export function decodeBase64(text: string): Uint8Array | null {
|
||||
let end = text.length;
|
||||
while (end > 0 && text.charCodeAt(end - 1) === 61) end--;
|
||||
if (text.length - end > 2 || end % 4 === 1) return null;
|
||||
const out = new Uint8Array(Math.floor((end * 3) / 4));
|
||||
let acc = 0;
|
||||
let bits = 0;
|
||||
let j = 0;
|
||||
for (let i = 0; i < end; i++) {
|
||||
const c = text.charCodeAt(i);
|
||||
const v = c < 128 ? BASE64_VALUES[c]! : -1;
|
||||
if (v < 0) return null;
|
||||
acc = ((acc << 6) | v) & 0xffffff;
|
||||
bits += 6;
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
out[j++] = (acc >> bits) & 0xff;
|
||||
}
|
||||
}
|
||||
return j === out.length ? out : out.subarray(0, j);
|
||||
}
|
||||
|
||||
/** The root directory of the npm package that contains `file` (the
|
||||
* directory below its innermost node_modules), or null outside one. */
|
||||
export function packageRootOf(file: string): string | null {
|
||||
const marker = "/node_modules/";
|
||||
const at = file.lastIndexOf(marker);
|
||||
if (at < 0) return null;
|
||||
const rest = file.slice(at + marker.length);
|
||||
const segments = rest.split("/");
|
||||
const take = segments[0]!.startsWith("@") ? 2 : 1;
|
||||
if (segments.length <= take) return null;
|
||||
return file.slice(0, at + marker.length) + segments.slice(0, take).join("/");
|
||||
}
|
||||
|
||||
function collectWasmFiles(dir: string, depth: number, out: string[]): void {
|
||||
const entries = trackedAccessibleEntries(dir);
|
||||
if (entries === null) return;
|
||||
for (const name of entries.files) if (name.endsWith(".wasm")) out.push(join(dir, name));
|
||||
if (depth >= PACKAGE_SCAN_DEPTH) return;
|
||||
for (const name of entries.directories) {
|
||||
if (name === "node_modules" || name.startsWith(".")) continue;
|
||||
collectWasmFiles(join(dir, name), depth + 1, out);
|
||||
}
|
||||
}
|
||||
|
||||
function nearestPackageJson(dir: string): string | null {
|
||||
for (;;) {
|
||||
const candidate = join(dir, "package.json");
|
||||
if (trackedFileExists(candidate)) return candidate;
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) return null;
|
||||
dir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
/** Every WebAssembly module the program's sources, packages and
|
||||
* configuration make visible to the build, in discovery order. */
|
||||
export function discoverWasmModules(input: WasmDiscoveryInput): WasmModuleReport[] {
|
||||
return new Discovery(input).run();
|
||||
}
|
||||
@@ -4477,7 +4477,7 @@
|
||||
"kind": "stdlib",
|
||||
"name": "Map.prototype.entries",
|
||||
"status": "static",
|
||||
"note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused"
|
||||
"note": "live native iterators support next(), for...of, drains, and the iterator helper methods"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.map.forEach",
|
||||
@@ -4502,7 +4502,7 @@
|
||||
"kind": "stdlib",
|
||||
"name": "Map.prototype.keys",
|
||||
"status": "static",
|
||||
"note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused"
|
||||
"note": "live native iterators support next(), for...of, drains, and the iterator helper methods"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.map.set",
|
||||
@@ -4515,7 +4515,7 @@
|
||||
"kind": "stdlib",
|
||||
"name": "Map.prototype.values",
|
||||
"status": "static",
|
||||
"note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused"
|
||||
"note": "live native iterators support next(), for...of, drains, and the iterator helper methods"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.math.E",
|
||||
@@ -5384,7 +5384,7 @@
|
||||
"kind": "stdlib",
|
||||
"name": "Set.prototype.entries",
|
||||
"status": "static",
|
||||
"note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused"
|
||||
"note": "live native iterators support next(), for...of, drains, and the iterator helper methods"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.set.forEach",
|
||||
@@ -5431,7 +5431,7 @@
|
||||
"kind": "stdlib",
|
||||
"name": "Set.prototype.keys",
|
||||
"status": "static",
|
||||
"note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused"
|
||||
"note": "live native iterators support next(), for...of, drains, and the iterator helper methods"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.set.symmetricDifference",
|
||||
@@ -5452,7 +5452,7 @@
|
||||
"kind": "stdlib",
|
||||
"name": "Set.prototype.values",
|
||||
"status": "static",
|
||||
"note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused"
|
||||
"note": "live native iterators support next(), for...of, drains, and the iterator helper methods"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.sharedArrayBuffer",
|
||||
@@ -5643,6 +5643,14 @@
|
||||
"status": "static",
|
||||
"note": "the lowered call form takes no arguments"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.webassembly",
|
||||
"kind": "stdlib",
|
||||
"name": "globalThis.WebAssembly",
|
||||
"status": "unsupported",
|
||||
"code": "SC2020",
|
||||
"note": "WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)"
|
||||
},
|
||||
{
|
||||
"id": "syntax.async-generators",
|
||||
"kind": "syntax",
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/* Regenerates wasm-spec-cases.txt: the binary modules of the WebAssembly
|
||||
* specification test suite at the feature level the decoder validates
|
||||
* (src/wasm/binary.ts), each with the suite's verdict and V8's.
|
||||
*
|
||||
* node gen-wasm-spec-cases.mjs --wast2json <wabt>/bin/wast2json \
|
||||
* --spec <WebAssembly/spec at tag wg-2.0>/test/core \
|
||||
* --extended-const <WebAssembly/extended-const>/test/core > wasm-spec-cases.txt
|
||||
*
|
||||
* Inputs: every .wast of the 2.0 core suite except simd/ (SIMD is outside
|
||||
* the translatable set; the decoder only reports it), plus the extended
|
||||
* constant expression proposal's global, data and elem tests. Modules given
|
||||
* as text for assert_malformed are skipped (they test the text format).
|
||||
* Identical modules are kept once, at their first location.
|
||||
*
|
||||
* Line format: <suite verdict> <V8 verdict> <file>:<line> <base64>, where a
|
||||
* verdict is "valid", "invalid" or "malformed" (V8: "valid" or "invalid").
|
||||
* The suite's tests are Apache-2.0 licensed (WebAssembly Community Group). */
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
wast2json: { type: "string" },
|
||||
spec: { type: "string" },
|
||||
"extended-const": { type: "string" },
|
||||
},
|
||||
});
|
||||
const inputs = [];
|
||||
for (const name of readdirSync(values.spec).sort()) {
|
||||
if (name.endsWith(".wast")) inputs.push({ label: name, path: join(values.spec, name) });
|
||||
}
|
||||
for (const name of ["global.wast", "data.wast", "elem.wast"]) {
|
||||
inputs.push({ label: `extended-const/${name}`, path: join(values["extended-const"], name) });
|
||||
}
|
||||
const seen = new Set();
|
||||
const lines = [
|
||||
"# WebAssembly spec test modules for src/wasm/binary.ts; generated by gen-wasm-spec-cases.mjs.",
|
||||
"# Sources: WebAssembly/spec wg-2.0 test/core (without simd/) and WebAssembly/extended-const",
|
||||
"# test/core global, data and elem, converted with wabt 1.0.37 wast2json (Apache-2.0).",
|
||||
"# <suite verdict> <V8 verdict> <file>:<line> <base64>",
|
||||
];
|
||||
for (const { label, path } of inputs) {
|
||||
const dir = mkdtempSync(join(tmpdir(), "wasm-spec-"));
|
||||
try {
|
||||
execFileSync(values.wast2json, [path, "-o", join(dir, "x.json")], { stdio: "pipe" });
|
||||
const json = JSON.parse(readFileSync(join(dir, "x.json"), "utf8"));
|
||||
for (const command of json.commands) {
|
||||
let verdict;
|
||||
if (["module", "assert_unlinkable", "assert_uninstantiable"].includes(command.type))
|
||||
verdict = "valid";
|
||||
else if (command.type === "assert_invalid") verdict = "invalid";
|
||||
else if (command.type === "assert_malformed") verdict = "malformed";
|
||||
else continue;
|
||||
if (command.module_type === "text" || !command.filename?.endsWith(".wasm")) continue;
|
||||
const bytes = readFileSync(join(dir, command.filename));
|
||||
const b64 = bytes.toString("base64");
|
||||
if (seen.has(b64)) continue;
|
||||
seen.add(b64);
|
||||
const v8 = WebAssembly.validate(bytes) ? "valid" : "invalid";
|
||||
lines.push(`${verdict} ${v8} ${label}:${command.line} ${b64}`);
|
||||
}
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
process.stdout.write(`${lines.join("\n")}\n`);
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5768-crypto-hash-unknown.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5769-crypto-hash-unknown.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5774-crypto-handle-long-callee.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5908-iterator-helper-objects.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5909-iterator-helper-objects-js.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5979-writable-instanceof-duplex.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6170-flight-dev-stacks-dead.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6171-error-stack-capture-sites.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6172-unreachable-statements.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/error-js-accessor-name-message.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/webassembly-feature-probe.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/webassembly-type-mentions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/webassembly-fences.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,168 @@
|
||||
/* The WebAssembly decoder (src/wasm/binary.ts) against the specification's
|
||||
* test suite, against V8 on mutations of those modules, and on the inline
|
||||
* modules of real packages in this workspace. V8 (the Node running the
|
||||
* test) is the oracle: scriptc must accept exactly the modules Node
|
||||
* accepts, within the translatable feature level. */
|
||||
import { globSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { analyzeWasm } from "../src/wasm/binary.js";
|
||||
import { decodeBase64 } from "../src/wasm/discover.js";
|
||||
|
||||
/** V8's verdict (the compiler's type world declares no WebAssembly value). */
|
||||
const v8Validate = (bytes: Uint8Array): boolean =>
|
||||
(
|
||||
globalThis as unknown as { WebAssembly: { validate(bytes: Uint8Array): boolean } }
|
||||
).WebAssembly.validate(bytes);
|
||||
|
||||
interface SpecCase {
|
||||
suite: string;
|
||||
v8: string;
|
||||
where: string;
|
||||
bytes: Uint8Array;
|
||||
}
|
||||
|
||||
const cases: SpecCase[] = readFileSync(join(import.meta.dirname, "wasm-spec-cases.txt"), "utf8")
|
||||
.split("\n")
|
||||
.filter((line) => line !== "" && !line.startsWith("#"))
|
||||
.map((line) => {
|
||||
const [suite, v8, where, b64] = line.split(" ");
|
||||
return {
|
||||
suite: suite!,
|
||||
v8: v8!,
|
||||
where: where!,
|
||||
bytes: new Uint8Array(Buffer.from(b64!, "base64")),
|
||||
};
|
||||
});
|
||||
|
||||
const accepted = (status: string): boolean => status === "valid" || status === "unsupported";
|
||||
|
||||
describe("WebAssembly spec test suite", () => {
|
||||
test("covers the 2.0 core suite and extended constant expressions", () => {
|
||||
expect(cases.length).toBeGreaterThan(3000);
|
||||
const extended = cases.filter((c) => c.where.startsWith("extended-const/"));
|
||||
expect(extended.length).toBeGreaterThan(10);
|
||||
expect(
|
||||
extended.filter((c) => analyzeWasm(c.bytes).features.includes("extended-const")).length,
|
||||
).toBeGreaterThan(5);
|
||||
});
|
||||
|
||||
test("every module gets V8's verdict and needs no other proposal", () => {
|
||||
const wrong: string[] = [];
|
||||
for (const c of cases) {
|
||||
const s = analyzeWasm(c.bytes);
|
||||
const v8 = v8Validate(c.bytes);
|
||||
if ((v8 ? "valid" : "invalid") !== c.v8) wrong.push(`${c.where}: V8 changed its verdict`);
|
||||
// Multiple memories are the one proposal outside the translatable set
|
||||
// that 2.0 tests exercise (as invalid; V8 accepts them).
|
||||
const allowed = s.status !== "unsupported" || s.unsupported.join() === "multi-memory";
|
||||
if (accepted(s.status) !== v8 || !allowed) {
|
||||
wrong.push(`${c.where}: ${s.status} ${s.error ?? ""} (V8 ${v8 ? "accepts" : "rejects"})`);
|
||||
}
|
||||
}
|
||||
expect(wrong).toEqual([]);
|
||||
});
|
||||
|
||||
test("differs from the 2.0 suite only where WebAssembly 3.0 and V8 relaxed it", () => {
|
||||
// Multiple memories became valid, and memory.size/grow's reserved byte
|
||||
// a memory index, so a long LEB128 zero is accepted (multi-memory);
|
||||
// global initializers and segment offsets may read defined immutable
|
||||
// globals (GC proposal).
|
||||
const relaxed = cases.filter((c) => c.suite !== "valid" && c.v8 === "valid");
|
||||
expect(relaxed.length).toBeGreaterThan(0);
|
||||
for (const c of relaxed) {
|
||||
expect(c.where).toMatch(
|
||||
/^(?:extended-const\/)?(?:binary|data|elem|global|imports|memory)\.wast:\d+$/,
|
||||
);
|
||||
}
|
||||
expect(relaxed.length).toBeLessThan(30);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mutations of the suite's valid modules", () => {
|
||||
test("never disagree with V8 (deterministic sample)", () => {
|
||||
const seeds = cases.filter((c) => c.v8 === "valid").map((c) => c.bytes);
|
||||
let state = 0x9e3779b9;
|
||||
const random = (n: number): number => {
|
||||
state ^= state << 13;
|
||||
state >>>= 0;
|
||||
state ^= state >>> 17;
|
||||
state ^= state << 5;
|
||||
state >>>= 0;
|
||||
return state % n;
|
||||
};
|
||||
const disagreements: string[] = [];
|
||||
for (let i = 0; i < 8000; i++) {
|
||||
const seed = seeds[random(seeds.length)]!;
|
||||
const at = 8 + random(Math.max(1, seed.length - 8));
|
||||
let bytes: Uint8Array;
|
||||
switch (random(4)) {
|
||||
case 0:
|
||||
bytes = Uint8Array.from(seed);
|
||||
bytes[at] = bytes[at]! ^ (1 << random(8));
|
||||
break;
|
||||
case 1:
|
||||
bytes = Uint8Array.from(seed);
|
||||
bytes[at] = random(256);
|
||||
break;
|
||||
case 2:
|
||||
bytes = new Uint8Array([...seed.subarray(0, at), ...seed.subarray(at + 1)]);
|
||||
break;
|
||||
default:
|
||||
bytes = new Uint8Array([...seed.subarray(0, at), random(256), ...seed.subarray(at)]);
|
||||
}
|
||||
const s = analyzeWasm(bytes);
|
||||
if (s.status === "unsupported") continue;
|
||||
if (accepted(s.status) !== v8Validate(bytes)) {
|
||||
disagreements.push(
|
||||
`${s.status} ${s.error ?? ""}: ${Buffer.from(bytes).toString("base64")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
expect(disagreements).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("modules inlined by packages in this workspace", () => {
|
||||
// Lockfile-pinned installs: undici's llhttp (with and without SIMD) and
|
||||
// es-module-lexer, both inlined as base64 string literals.
|
||||
const root = join(import.meta.dirname, "../../..");
|
||||
const inline = (pattern: string): Uint8Array[] =>
|
||||
globSync(join(root, pattern))
|
||||
.sort()
|
||||
.flatMap((file) =>
|
||||
[...readFileSync(file, "utf8").matchAll(/AGFzbQ[A-Za-z0-9+/=]*/g)].map((m) =>
|
||||
decodeBase64(m[0])!,
|
||||
),
|
||||
);
|
||||
|
||||
test("undici's llhttp: the scalar module translates, the SIMD one needs SIMD", () => {
|
||||
const scalar = inline(
|
||||
"node_modules/.pnpm/undici@7.*/node_modules/undici/lib/llhttp/llhttp-wasm.js",
|
||||
);
|
||||
const simd = inline(
|
||||
"node_modules/.pnpm/undici@7.*/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js",
|
||||
);
|
||||
expect(scalar.length).toBeGreaterThan(0);
|
||||
expect(simd.length).toBeGreaterThan(0);
|
||||
for (const bytes of scalar) {
|
||||
expect(analyzeWasm(bytes)).toMatchObject({ status: "valid", importModules: ["env"] });
|
||||
expect(v8Validate(bytes)).toBe(true);
|
||||
}
|
||||
for (const bytes of simd) {
|
||||
expect(analyzeWasm(bytes)).toMatchObject({ status: "unsupported", unsupported: ["simd"] });
|
||||
expect(v8Validate(bytes)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("es-module-lexer's module is plain MVP", () => {
|
||||
const modules = inline(
|
||||
"node_modules/.pnpm/es-module-lexer@1.*/node_modules/es-module-lexer/dist/lexer.js",
|
||||
);
|
||||
expect(modules.length).toBeGreaterThan(0);
|
||||
for (const bytes of modules) {
|
||||
expect(analyzeWasm(bytes)).toMatchObject({ status: "valid", imports: 0 });
|
||||
expect(v8Validate(bytes)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -4890,6 +4890,8 @@ struct ScrGen {
|
||||
/* The never-started teardown: drops the packed (already-retained)
|
||||
* arguments the spawn wrapper built. Emitted per generator function. */
|
||||
void (*drop_args)(void *);
|
||||
/* scr_gen_set_unstarted_return's hook; NULL for most generators. */
|
||||
void (*unstarted_return)(void *);
|
||||
bool is_async;
|
||||
void (*settle_async)(ScrGen *, ScrPromise *);
|
||||
ScrAsyncGenRequest *requests_head;
|
||||
@@ -5449,6 +5451,8 @@ void scr_gen_resume(ScrGen *g) {
|
||||
}
|
||||
}
|
||||
|
||||
void scr_gen_set_unstarted_return(ScrGen *g, void (*hook)(void *)) { g->unstarted_return = hook; }
|
||||
|
||||
void scr_gen_close(ScrGen *g) {
|
||||
scr_gen_slot_reset(&g->ret);
|
||||
scr_gen_resume_return(g);
|
||||
@@ -5483,13 +5487,16 @@ void scr_gen_resume_return(ScrGen *g) {
|
||||
return;
|
||||
case SCR_GEN_UNSTARTED:
|
||||
/* The body never runs: tear the fiber down cleanly (drop the packed
|
||||
* arguments) and complete with the parked value. */
|
||||
* arguments) and complete with the parked value. The generator is
|
||||
* done before the hook runs, so a resume from inside it answers done;
|
||||
* an exception the hook raises stays pending for the caller. */
|
||||
g->state = SCR_GEN_DONE;
|
||||
if (g->unstarted_return != NULL) g->unstarted_return(g->fiber->argpack);
|
||||
if (g->drop_args != NULL) g->drop_args(g->fiber->argpack);
|
||||
else free(g->fiber->argpack);
|
||||
scr_fiber_destroy(g->fiber);
|
||||
g->fiber = NULL;
|
||||
scr_fibers_live--;
|
||||
g->state = SCR_GEN_DONE;
|
||||
scr_gen_ret_to_out(g);
|
||||
return;
|
||||
default:
|
||||
|
||||
@@ -25,6 +25,10 @@ ScrCryptoHash *scr_crypto_hash_retain(ScrCryptoHash *h);
|
||||
void scr_crypto_hash_release(ScrCryptoHash *h);
|
||||
void *scr_crypto_hash_retain_v(void *p);
|
||||
void scr_crypto_hash_release_v(void *p);
|
||||
/* A Hash or Hmac boxed as a checked-dynamic handle (+1; retains h). The
|
||||
* emitted to-dyn conversion calls it, and it installs the handle ops on
|
||||
* first use, so programs that never box a Hash do not link them. */
|
||||
ScrDyn *scr_crypto_hash_to_dyn(ScrCryptoHash *h);
|
||||
ScrCryptoHash *scr_crypto_hash_update_str(ScrCryptoHash *h, ScrStr *data);
|
||||
ScrCryptoHash *scr_crypto_hash_update_bytes(ScrCryptoHash *h, ScrBytes *data);
|
||||
ScrCryptoHash *scr_crypto_hash_copy(ScrCryptoHash *h);
|
||||
|
||||
@@ -171,9 +171,22 @@ typedef enum {
|
||||
SCR_DYNH_DISPOSABLE_STACK, /* DisposableStack (scr_symbol.c) */
|
||||
SCR_DYNH_MESSAGE_EVENT, /* ScrEvent with data — BroadcastChannel's 'message' */
|
||||
SCR_DYNH_BROADCAST_CHANNEL, /* worker_threads BroadcastChannel (scr_worker_events.c) */
|
||||
SCR_DYNH_HASH, /* node:crypto Hash (scr_lib.c) */
|
||||
SCR_DYNH_HMAC, /* node:crypto Hmac (scr_lib.c) */
|
||||
SCR_DYNH_COUNT,
|
||||
} ScrDynHandleTag;
|
||||
|
||||
/* Every per-tag table (scr_dynh_ops in scr_json.c, the message clone hooks
|
||||
* in scr_message.c) is sized by this fixed capacity, never by
|
||||
* SCR_DYNH_COUNT. Appending a tag for a link-gated unit therefore changes
|
||||
* no always-linked storage or code: a program that never boxes the new
|
||||
* handle links byte-identical objects. Raising the capacity is a
|
||||
* program-wide layout change; do it in a change of its own, measured as
|
||||
* neutral, and only when the enum outgrows it. */
|
||||
#define SCR_DYNH_CAPACITY 128
|
||||
_Static_assert(SCR_DYNH_COUNT <= SCR_DYNH_CAPACITY,
|
||||
"ScrDynHandleTag outgrew SCR_DYNH_CAPACITY: raise it in its own change");
|
||||
|
||||
typedef struct ScrDyn ScrDyn;
|
||||
typedef struct ScrBytes ScrBytes; /* full definition below (C11 repeat) */
|
||||
typedef struct ScrClosure ScrClosure; /* full definition below (C11 repeat) */
|
||||
@@ -1162,7 +1175,25 @@ typedef struct ScrDynHandleOps {
|
||||
* lookup): +1 value, or NULL when the class has no member for `key`
|
||||
* (the read answers undefined). NULL hook = no symbol-keyed members. */
|
||||
ScrDyn *(*symbol_get)(void *h, const ScrDyn *key);
|
||||
/* Cross-thread structured clone (postMessage, workerData); NULL =
|
||||
* DataCloneError. See ScrDynHandleTransfer. */
|
||||
const struct ScrDynHandleTransfer *transfer;
|
||||
} ScrDynHandleOps;
|
||||
|
||||
/* How a handle crosses threads, supplied by the handle's owning unit so the
|
||||
* message encoder (scr_message.c) needs no reference to gated units and no
|
||||
* per-tag registry. share runs on the sending thread: it answers a payload
|
||||
* that any thread may use, +1, or NULL when this handle cannot be cloned
|
||||
* (with an exception pending, or DataCloneError is thrown). adopt runs on
|
||||
* the receiving thread and answers a fresh box over the payload (+1; the
|
||||
* message keeps its payload reference). drop releases the payload when the
|
||||
* message is freed, whether or not it was delivered. All three must be
|
||||
* callable from any thread. */
|
||||
typedef struct ScrDynHandleTransfer {
|
||||
void *(*share)(void *h);
|
||||
ScrDyn *(*adopt)(void *payload);
|
||||
void (*drop)(void *payload);
|
||||
} ScrDynHandleTransfer;
|
||||
ScrDyn *scr_dyn_native_handle_iterator(ScrDyn *source, unsigned selection);
|
||||
|
||||
/* ── promises in the checked-dynamic tree (SCR_DYN_PROMISE) ────────────────────────────
|
||||
|
||||
@@ -389,6 +389,17 @@ void scr_exc_suppressed_forget(const void *error_object) {
|
||||
}
|
||||
}
|
||||
|
||||
void scr_exc_keep_first(ScrCaught *first) {
|
||||
if (first->kind == SCR_EXC_GENRET) {
|
||||
scr_caught_release(first);
|
||||
return;
|
||||
}
|
||||
ScrCaught *dropped = scr_exc_take();
|
||||
if (dropped) scr_caught_release(dropped);
|
||||
scr_rethrow(first);
|
||||
scr_caught_release(first);
|
||||
}
|
||||
|
||||
void scr_exc_suppress(ScrCaught *suppressed) {
|
||||
if (suppressed->kind == SCR_EXC_GENRET) {
|
||||
/* A generator .return() is a return completion, not a thrown error.
|
||||
|
||||
@@ -84,6 +84,10 @@ typedef struct ScrExcCell {
|
||||
ScrTraceFn trace_fn;
|
||||
ScrStackFrame *stack; /* execution state; never moves with an exception */
|
||||
} ScrExcCell;
|
||||
/* Executables push and pop source frames inline (emitter.ts, stack-trace
|
||||
* mode) at this offset on 64-bit targets. */
|
||||
_Static_assert(sizeof(void *) != 8 || offsetof(ScrExcCell, stack) == 56,
|
||||
"ScrExcCell.stack moved: update the emitter's inline frame push");
|
||||
|
||||
/* Runtime-internal (fiber machinery in scr_async.c) — never emitted. */
|
||||
ScrExcCell *scr_exc_swap_cell(ScrExcCell *cell); /* NULL = main's cell */
|
||||
@@ -172,6 +176,10 @@ void scr_caught_trace_v(void *c, ScrTraceVisit visit, void *ctx);
|
||||
* when scr_json.c builds its dyn view, and scr_exc_suppressed_forget
|
||||
* releases them. */
|
||||
void scr_exc_suppress(ScrCaught *suppressed);
|
||||
/* An iterator close that threw while `first` was propagating: the close's
|
||||
* error is dropped and `first` is raised again (consumed). A generator
|
||||
* .return() in flight is replaced by the close's error instead. */
|
||||
void scr_exc_keep_first(ScrCaught *first);
|
||||
bool scr_exc_suppressed_values(const void *error_object, const ScrCaught **error,
|
||||
const ScrCaught **suppressed);
|
||||
void scr_exc_suppressed_forget(const void *error_object);
|
||||
|
||||
@@ -60,6 +60,9 @@ void scr_gen_coro_finish(ScrFiber *self);
|
||||
ScrGen *scr_async_gen_new(void (*entry)(ScrFiber *, void *), void *argpack,
|
||||
void (*drop_args)(void *),
|
||||
void (*settle)(ScrGen *, ScrPromise *));
|
||||
/* Runs when return() reaches the generator before its first resume, with
|
||||
* the packed arguments still alive: an iterator helper closes its source. */
|
||||
void scr_gen_set_unstarted_return(ScrGen *g, void (*hook)(void *));
|
||||
ScrGen *scr_gen_retain(ScrGen *g);
|
||||
void scr_gen_release(ScrGen *g); /* NULL-tolerant */
|
||||
void *scr_gen_retain_v(void *g);
|
||||
|
||||
@@ -3651,7 +3651,7 @@ ScrDyn *scr_dyn_bind(ScrDyn *target, ScrDyn *const *args, size_t argc) {
|
||||
* always-linked core never references gated units. A missing tag at use
|
||||
* is an internal error: emitted programs install a unit's ops whenever
|
||||
* they can box its handles. */
|
||||
static SCR_TL const ScrDynHandleOps *scr_dynh_ops[SCR_DYNH_COUNT];
|
||||
static SCR_TL const ScrDynHandleOps *scr_dynh_ops[SCR_DYNH_CAPACITY];
|
||||
|
||||
/* BroadcastChannel's EventTarget table (scr_et_broadcast_install). */
|
||||
SCR_TL const ScrEtBroadcastOps *scr_et_broadcast_ops = NULL;
|
||||
|
||||
@@ -8098,6 +8098,201 @@ void scr_crypto_hash_release(ScrCryptoHash *hash) {
|
||||
void *scr_crypto_hash_retain_v(void *p) { return scr_crypto_hash_retain(p); }
|
||||
void scr_crypto_hash_release_v(void *p) { scr_crypto_hash_release(p); }
|
||||
|
||||
/* Node's TypeError for calling a missing member: "<what> is not a
|
||||
* function", where `what` is the callee's source text, of any length. */
|
||||
static void scr_lib_throw_not_a_function(const char *what) {
|
||||
ScrJsonBuf b;
|
||||
scr_jb_init(&b);
|
||||
scr_jb_puts(&b, what);
|
||||
scr_jb_puts(&b, " is not a function");
|
||||
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));
|
||||
}
|
||||
|
||||
/* Hash and Hmac values seen as 'unknown' are native handles. update(),
|
||||
* digest() and Hash's copy() dispatch through the handle, called or read
|
||||
* as values; the other members Node defines on a Hash or Hmac (the
|
||||
* Transform and emitter surface) throw a coded SC2020 error, and other
|
||||
* names read as undefined, as they do in Node. The member names are one
|
||||
* NUL-separated char array, which the linker drops from programs that
|
||||
* never box a Hash (an array of string pointers keeps the strings in the
|
||||
* unit's shared string section). */
|
||||
static const char scr_crypto_hash_dyn_members[] =
|
||||
"_destroy\0_events\0_eventsCount\0_final\0_flush\0_maxListeners\0_options\0_read\0"
|
||||
"_readableState\0_transform\0_undestroy\0_writableState\0_write\0_writev\0addListener\0"
|
||||
"allowHalfOpen\0closed\0compose\0constructor\0cork\0destroy\0destroyed\0digest\0drop\0"
|
||||
"emit\0end\0errored\0eventNames\0every\0filter\0find\0flatMap\0forEach\0getMaxListeners\0"
|
||||
"isPaused\0iterator\0listenerCount\0listeners\0map\0off\0on\0once\0pause\0pipe\0"
|
||||
"prependListener\0prependOnceListener\0push\0rawListeners\0read\0readable\0"
|
||||
"readableAborted\0readableBuffer\0readableDidRead\0readableEncoding\0readableEnded\0"
|
||||
"readableFlowing\0readableHighWaterMark\0readableLength\0readableObjectMode\0reduce\0"
|
||||
"removeAllListeners\0removeListener\0resume\0setDefaultEncoding\0setEncoding\0"
|
||||
"setMaxListeners\0some\0take\0toArray\0uncork\0unpipe\0unshift\0update\0wrap\0writable\0"
|
||||
"writableBuffer\0writableCorked\0writableEnded\0writableFinished\0writableHighWaterMark\0"
|
||||
"writableLength\0writableNeedDrain\0writableObjectMode\0write\0";
|
||||
|
||||
static bool scr_crypto_hash_dyn_member(const ScrCryptoHash *hash, const char *key, size_t len) {
|
||||
if (len == 4 && memcmp(key, "copy", 4) == 0) return !hash->hmac;
|
||||
for (const char *name = scr_crypto_hash_dyn_members; *name; name += strlen(name) + 1)
|
||||
if (strlen(name) == len && memcmp(name, key, len) == 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static void scr_crypto_hash_dyn_unsupported(const ScrCryptoHash *hash) {
|
||||
static const char hash_msg[] =
|
||||
"Hash members other than update(), digest() and copy() have no lowering on an 'unknown' "
|
||||
"value; cast the value to Hash first";
|
||||
static const char hmac_msg[] =
|
||||
"Hmac members other than update() and digest() have no lowering on an 'unknown' value; "
|
||||
"cast the value to Hmac first";
|
||||
if (hash->hmac) scr_throw_error_msg_code(SCR_ERR_ERROR, hmac_msg, sizeof hmac_msg - 1, "SC2020");
|
||||
else scr_throw_error_msg_code(SCR_ERR_ERROR, hash_msg, sizeof hash_msg - 1, "SC2020");
|
||||
}
|
||||
|
||||
/* The method values: update() is one function for Hash and Hmac, as in
|
||||
* Node; digest() is one per class, and copy() is Hash's. */
|
||||
enum { SCR_HASH_DYN_UPDATE, SCR_HASH_DYN_HASH_DIGEST, SCR_HASH_DYN_HMAC_DIGEST, SCR_HASH_DYN_COPY };
|
||||
static const char *const scr_crypto_hash_dyn_method_names[] = {"update", "digest", "digest", "copy"};
|
||||
static SCR_TL ScrDyn *scr_crypto_hash_dyn_methods[4];
|
||||
static SCR_TL bool scr_crypto_hash_dyn_methods_registered;
|
||||
|
||||
static void scr_crypto_hash_dyn_methods_cleanup(void) {
|
||||
for (size_t i = 0; i < 4; i++) {
|
||||
ScrDyn *method = scr_crypto_hash_dyn_methods[i];
|
||||
scr_crypto_hash_dyn_methods[i] = NULL;
|
||||
scr_dyn_release(method);
|
||||
}
|
||||
}
|
||||
|
||||
static int scr_crypto_hash_dyn_method_index(const ScrCryptoHash *hash, const char *key, size_t len) {
|
||||
if (len == 6 && memcmp(key, "update", 6) == 0) return SCR_HASH_DYN_UPDATE;
|
||||
if (len == 6 && memcmp(key, "digest", 6) == 0)
|
||||
return hash->hmac ? SCR_HASH_DYN_HMAC_DIGEST : SCR_HASH_DYN_HASH_DIGEST;
|
||||
if (len == 4 && memcmp(key, "copy", 4) == 0 && !hash->hmac) return SCR_HASH_DYN_COPY;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Runs method `index` on `hash`; `self` is the receiving box, which
|
||||
* update() answers. */
|
||||
static ScrDyn *scr_crypto_hash_dyn_run(ScrCryptoHash *hash, ScrDyn *self, int index,
|
||||
ScrDyn *const *args, size_t argc) {
|
||||
ScrDyn *undefined = scr_dyn_undefined();
|
||||
ScrDyn *first = argc > 0 ? args[0] : undefined;
|
||||
ScrDyn *second = argc > 1 ? args[1] : undefined;
|
||||
ScrDyn *result = NULL;
|
||||
if (index == SCR_HASH_DYN_UPDATE) {
|
||||
ScrCryptoHash *updated = scr_crypto_hash_update_dyn(hash, first, second);
|
||||
if (updated) {
|
||||
scr_crypto_hash_release(updated);
|
||||
result = scr_dyn_retain(self);
|
||||
}
|
||||
} else if (index == SCR_HASH_DYN_COPY) {
|
||||
ScrCryptoHash *copy = scr_crypto_hash_copy_opts(hash, first);
|
||||
if (copy) {
|
||||
result = scr_dyn_new_handle(copy, SCR_DYNH_HASH);
|
||||
scr_crypto_hash_release(copy);
|
||||
}
|
||||
} else {
|
||||
result = scr_crypto_hash_digest_dyn(hash, first);
|
||||
}
|
||||
scr_dyn_release(undefined);
|
||||
return result;
|
||||
}
|
||||
|
||||
static ScrDyn *scr_crypto_hash_dyn_method_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
|
||||
int index = (int)scr_box_get_f64(clo->caps[0]);
|
||||
ScrDyn *self = scr_dyn_this_get();
|
||||
bool hash_this = self->kind == SCR_DYN_HANDLE && self->v.handle.tag == SCR_DYNH_HASH;
|
||||
bool hmac_this = self->kind == SCR_DYN_HANDLE && self->v.handle.tag == SCR_DYNH_HMAC;
|
||||
bool valid = index == SCR_HASH_DYN_UPDATE ? hash_this || hmac_this
|
||||
: index == SCR_HASH_DYN_HMAC_DIGEST ? hmac_this
|
||||
: hash_this;
|
||||
ScrDyn *result = NULL;
|
||||
if (valid) {
|
||||
result = scr_crypto_hash_dyn_run(self->v.handle.ptr, self, index, args, argc);
|
||||
} else {
|
||||
static const char msg[] =
|
||||
"Hash and Hmac methods called with a receiver other than their own Hash or Hmac have no "
|
||||
"lowering";
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "SC2020");
|
||||
}
|
||||
scr_dyn_release(self);
|
||||
return result;
|
||||
}
|
||||
|
||||
static ScrDyn *scr_crypto_hash_dyn_method_value(int index) {
|
||||
if (!scr_crypto_hash_dyn_methods_registered) {
|
||||
scr_crypto_hash_dyn_methods_registered = true;
|
||||
scr_atexit(scr_crypto_hash_dyn_methods_cleanup);
|
||||
}
|
||||
if (!scr_crypto_hash_dyn_methods[index]) {
|
||||
ScrClosure *clo = scr_closure_new(NULL, 1);
|
||||
clo->caps[0] = scr_box_new(SCR_BOX_F64);
|
||||
scr_box_set_f64(clo->caps[0], (double)index);
|
||||
uint32_t arity = index == SCR_HASH_DYN_UPDATE ? 2 : 1;
|
||||
scr_crypto_hash_dyn_methods[index] = scr_dyn_new_func(
|
||||
clo, scr_crypto_hash_dyn_method_call, arity, "", scr_crypto_hash_dyn_method_names[index]);
|
||||
}
|
||||
return scr_dyn_retain(scr_crypto_hash_dyn_methods[index]);
|
||||
}
|
||||
|
||||
static ScrDyn *scr_crypto_hash_dyn_invoke(void *h, ScrDyn *self, const char *method,
|
||||
ScrDyn *const *args, size_t argc, const char *what) {
|
||||
ScrCryptoHash *hash = h;
|
||||
size_t len = strlen(method);
|
||||
int index = scr_crypto_hash_dyn_method_index(hash, method, len);
|
||||
if (index >= 0) return scr_crypto_hash_dyn_run(hash, self, index, args, argc);
|
||||
if (scr_crypto_hash_dyn_member(hash, method, len)) {
|
||||
scr_crypto_hash_dyn_unsupported(hash);
|
||||
return NULL;
|
||||
}
|
||||
scr_lib_throw_not_a_function(what);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static ScrDyn *scr_crypto_hash_dyn_get(void *h, const char *key, size_t len) {
|
||||
int index = scr_crypto_hash_dyn_method_index(h, key, len);
|
||||
if (index >= 0) return scr_crypto_hash_dyn_method_value(index);
|
||||
if (scr_crypto_hash_dyn_member(h, key, len)) scr_crypto_hash_dyn_unsupported(h);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool scr_crypto_hash_dyn_set(void *h, const char *key, size_t len, const ScrDyn *value) {
|
||||
(void)h;
|
||||
(void)key;
|
||||
(void)len;
|
||||
(void)value;
|
||||
return false;
|
||||
}
|
||||
|
||||
static void scr_crypto_hash_dyn_install(void) {
|
||||
static SCR_TL bool installed;
|
||||
if (installed) return;
|
||||
installed = true;
|
||||
static const ScrDynHandleOps hash_ops = {
|
||||
.cls = "Hash",
|
||||
.retain = scr_crypto_hash_retain_v,
|
||||
.release = scr_crypto_hash_release_v,
|
||||
.invoke = scr_crypto_hash_dyn_invoke,
|
||||
.get = scr_crypto_hash_dyn_get,
|
||||
.set = scr_crypto_hash_dyn_set,
|
||||
};
|
||||
static const ScrDynHandleOps hmac_ops = {
|
||||
.cls = "Hmac",
|
||||
.retain = scr_crypto_hash_retain_v,
|
||||
.release = scr_crypto_hash_release_v,
|
||||
.invoke = scr_crypto_hash_dyn_invoke,
|
||||
.get = scr_crypto_hash_dyn_get,
|
||||
.set = scr_crypto_hash_dyn_set,
|
||||
};
|
||||
scr_dyn_handle_install(SCR_DYNH_HASH, &hash_ops);
|
||||
scr_dyn_handle_install(SCR_DYNH_HMAC, &hmac_ops);
|
||||
}
|
||||
|
||||
ScrDyn *scr_crypto_hash_to_dyn(ScrCryptoHash *hash) {
|
||||
scr_crypto_hash_dyn_install();
|
||||
return scr_dyn_new_handle(hash, hash->hmac ? SCR_DYNH_HMAC : SCR_DYNH_HASH);
|
||||
}
|
||||
|
||||
static ScrCryptoHash *scr_crypto_hash_update_raw(ScrCryptoHash *hash,
|
||||
const unsigned char *data, size_t len) {
|
||||
if (!scr_crypto_hash_updatable(hash)) return NULL;
|
||||
@@ -9336,9 +9531,7 @@ static ScrDyn *scr_key_object_invoke(void *h, ScrDyn *self, const char *method,
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
|
||||
return NULL;
|
||||
}
|
||||
char message[160];
|
||||
int n = snprintf(message, sizeof message, "%s is not a function", what);
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n);
|
||||
scr_lib_throw_not_a_function(what);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ typedef enum {
|
||||
SCR_MESSAGE_TYPED_REF,
|
||||
/* A transferred MessagePort: its channel end (+1), the side in number. */
|
||||
SCR_MESSAGE_PORT,
|
||||
/* A native handle cloned through its unit's ScrDynHandleTransfer. */
|
||||
SCR_MESSAGE_HANDLE,
|
||||
} ScrMessageKind;
|
||||
|
||||
const ScrMessagePortOps *scr_message_port_ops = NULL;
|
||||
@@ -49,6 +51,9 @@ typedef struct {
|
||||
bool data_view;
|
||||
ScrDyn *published; /* SCR_MESSAGE_PUBLISHED: immortal root, owns nothing */
|
||||
ScrChannel *channel; /* SCR_MESSAGE_PORT */
|
||||
/* SCR_MESSAGE_HANDLE: the owning unit's hooks and its payload (+1). */
|
||||
const ScrDynHandleTransfer *transfer;
|
||||
void *payload;
|
||||
/* SCR_MESSAGE_TYPED_REF: the capsule's process-wide adapters and type key
|
||||
* (vtables, adapters and type keys are shared by every thread). */
|
||||
struct {
|
||||
@@ -389,7 +394,20 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input)
|
||||
break;
|
||||
}
|
||||
bool buffer = scr_buffer_storage_is(value);
|
||||
if (value->kind == SCR_DYN_HANDLE && !buffer) goto unsupported;
|
||||
if (value->kind == SCR_DYN_HANDLE && !buffer) {
|
||||
const ScrDynHandleTransfer *transfer = scr_dyn_handle_ops_of(value)->transfer;
|
||||
if (!transfer) goto unsupported;
|
||||
void *payload = transfer->share(value->v.handle.ptr);
|
||||
if (!payload) {
|
||||
if (scr_exc_pending()) return SIZE_MAX;
|
||||
goto unsupported;
|
||||
}
|
||||
node = &encoder->message->nodes[id];
|
||||
node->kind = SCR_MESSAGE_HANDLE;
|
||||
node->transfer = transfer;
|
||||
node->payload = payload;
|
||||
break;
|
||||
}
|
||||
const ScrBytes *bytes = buffer ? value->v.handle.ptr : value->v.bytes;
|
||||
size_t storage = scr_message_storage(encoder, bytes);
|
||||
/* Adding the storage node can reallocate the node array. */
|
||||
@@ -765,6 +783,7 @@ ScrDyn *scr_message_decode(const ScrMessage *message) {
|
||||
case SCR_MESSAGE_PORT:
|
||||
values[i] = scr_message_port_ops->adopt(node->channel, (int)node->number);
|
||||
break;
|
||||
case SCR_MESSAGE_HANDLE: values[i] = node->transfer->adopt(node->payload); break;
|
||||
}
|
||||
if (!values[i] || scr_exc_pending()) goto done;
|
||||
}
|
||||
@@ -820,6 +839,7 @@ void scr_message_free(ScrMessage *message) {
|
||||
free(node->data);
|
||||
scr_shared_release(node->shared);
|
||||
if (node->kind == SCR_MESSAGE_PORT) scr_message_port_ops->release(node->channel);
|
||||
if (node->kind == SCR_MESSAGE_HANDLE) node->transfer->drop(node->payload);
|
||||
}
|
||||
free(message->nodes);
|
||||
free(message);
|
||||
|
||||
@@ -172,6 +172,115 @@ static void check_documents(void) {
|
||||
scr_message_free(message);
|
||||
}
|
||||
|
||||
/* A link-gated unit's handle crosses threads through the transfer hooks in
|
||||
* its ops: share on the sender, adopt on the receiver, drop with the
|
||||
* message. The test borrows the last capacity slot, which no unit uses, so
|
||||
* it also proves that slots past SCR_DYNH_COUNT work. */
|
||||
#define TRANSFER_TAG ((ScrDynHandleTag)(SCR_DYNH_CAPACITY - 1))
|
||||
typedef struct {
|
||||
_Atomic size_t rc;
|
||||
int value;
|
||||
} TransferBox;
|
||||
static _Atomic int transfer_shares, transfer_adopts, transfer_drops;
|
||||
static void *transfer_retain(void *h) {
|
||||
((TransferBox *)h)->rc++;
|
||||
return h;
|
||||
}
|
||||
static void transfer_release(void *h) {
|
||||
if (--((TransferBox *)h)->rc == 0) free(h);
|
||||
}
|
||||
static void *transfer_share(void *h) {
|
||||
TransferBox *source = h;
|
||||
if (source->value < 0) return NULL;
|
||||
transfer_shares++;
|
||||
TransferBox *copy = malloc(sizeof *copy);
|
||||
copy->rc = 1;
|
||||
copy->value = source->value;
|
||||
return copy;
|
||||
}
|
||||
static ScrDyn *transfer_adopt(void *payload) {
|
||||
transfer_adopts++;
|
||||
return scr_dyn_new_handle(payload, TRANSFER_TAG);
|
||||
}
|
||||
static void transfer_drop(void *payload) {
|
||||
transfer_drops++;
|
||||
transfer_release(payload);
|
||||
}
|
||||
static const ScrDynHandleTransfer transfer_hooks = {transfer_share, transfer_adopt, transfer_drop};
|
||||
static const ScrDynHandleOps transfer_ops = {
|
||||
.cls = "Transferable",
|
||||
.retain = transfer_retain,
|
||||
.release = transfer_release,
|
||||
.transfer = &transfer_hooks,
|
||||
};
|
||||
static const ScrDynHandleOps opaque_ops = {
|
||||
.cls = "Opaque",
|
||||
.retain = transfer_retain,
|
||||
.release = transfer_release,
|
||||
};
|
||||
|
||||
static void *receive_handles(void *data) {
|
||||
scr_context_enter(1);
|
||||
scr_init();
|
||||
scr_lib_init(0, NULL);
|
||||
scr_dyn_handle_install(TRANSFER_TAG, &transfer_ops);
|
||||
ScrDyn *root = scr_message_decode(data);
|
||||
assert(root && !scr_exc_pending());
|
||||
ScrDyn *first = scr_dyn_obj_get(root, "first", 5);
|
||||
assert(first->kind == SCR_DYN_HANDLE && first->v.handle.tag == TRANSFER_TAG);
|
||||
assert(((TransferBox *)first->v.handle.ptr)->value == 7);
|
||||
/* One handle reached twice decodes to one box. */
|
||||
assert(first == scr_dyn_obj_get(root, "again", 5));
|
||||
assert(transfer_adopts == 1);
|
||||
scr_dyn_release(root);
|
||||
scr_collect_cycles();
|
||||
scr_context_cleanup();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static ScrDyn *transfer_handle(int value, const ScrDynHandleOps *ops) {
|
||||
scr_dyn_handle_install(TRANSFER_TAG, ops);
|
||||
TransferBox *box = malloc(sizeof *box);
|
||||
box->rc = 1;
|
||||
box->value = value;
|
||||
ScrDyn *handle = scr_dyn_new_handle(box, TRANSFER_TAG);
|
||||
transfer_release(box);
|
||||
return handle;
|
||||
}
|
||||
|
||||
static void check_handle_transfer(void) {
|
||||
ScrDyn *root = scr_dyn_new_obj();
|
||||
ScrDyn *handle = transfer_handle(7, &transfer_ops);
|
||||
scr_dyn_obj_set(root, "first", 5, scr_dyn_retain(handle));
|
||||
scr_dyn_obj_set(root, "again", 5, handle);
|
||||
ScrMessage *message = scr_message_encode(root);
|
||||
assert(message && !scr_exc_pending());
|
||||
assert(transfer_shares == 1);
|
||||
scr_dyn_release(root);
|
||||
pthread_t receiver;
|
||||
assert(pthread_create(&receiver, NULL, receive_handles, message) == 0);
|
||||
assert(pthread_join(receiver, NULL) == 0);
|
||||
assert(transfer_drops == 0);
|
||||
scr_message_free(message);
|
||||
assert(transfer_drops == 1);
|
||||
|
||||
/* A handle whose unit declines (share answers NULL) and a handle with no
|
||||
* transfer hooks both throw DataCloneError, releasing what was shared. */
|
||||
root = scr_dyn_new_arr();
|
||||
scr_dyn_arr_push(root, transfer_handle(1, &transfer_ops));
|
||||
scr_dyn_arr_push(root, transfer_handle(-1, &transfer_ops));
|
||||
assert(!scr_message_encode(root) && scr_exc_pending());
|
||||
scr_exc_clear();
|
||||
assert(transfer_shares == 2 && transfer_drops == 2);
|
||||
scr_dyn_release(root);
|
||||
root = transfer_handle(3, &opaque_ops);
|
||||
assert(!scr_message_encode(root) && scr_exc_pending());
|
||||
scr_exc_clear();
|
||||
assert(transfer_shares == 2);
|
||||
scr_dyn_release(root);
|
||||
scr_collect_cycles();
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
scr_init();
|
||||
scr_lib_init(0, NULL);
|
||||
@@ -188,6 +297,8 @@ int main(void) {
|
||||
check_getters();
|
||||
check_rejection();
|
||||
check_documents();
|
||||
check_handle_transfer();
|
||||
scr_context_cleanup();
|
||||
puts("message graphs preserve identity, sparse values, byte aliases and getter order");
|
||||
puts("native handles cross threads through their transfer hooks");
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@ test.skipIf(process.platform === "win32")(
|
||||
env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" },
|
||||
});
|
||||
expect(result.stdout).toBe(
|
||||
"message graphs preserve identity, sparse values, byte aliases and getter order\n",
|
||||
"message graphs preserve identity, sparse values, byte aliases and getter order\n" +
|
||||
"native handles cross threads through their transfer hooks\n",
|
||||
);
|
||||
expect(result.stderr).toBe("");
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Hash and Hmac values held in 'unknown' and 'any' slots: update(),
|
||||
// digest() and Hash's copy() dispatch through the value, update() answers
|
||||
// the same value, a cast back to Hash or Hmac keeps its identity, and the
|
||||
// errors after digest() are Node's. An Hmac has no copy().
|
||||
import { createHash, createHmac, type Hash, type Hmac } from "node:crypto";
|
||||
|
||||
function code(run: () => unknown): string {
|
||||
try {
|
||||
const value = run();
|
||||
return typeof value === "string" ? `ok ${value.slice(0, 16)}` : `ok ${typeof value}`;
|
||||
} catch (error) {
|
||||
const e = error as Error & { code?: string };
|
||||
return `${e.name} ${e.code ?? ""} ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function hexOf(value: any): string {
|
||||
return value.update("abc").digest("hex");
|
||||
}
|
||||
|
||||
console.log("any param", hexOf(createHash("sha256")));
|
||||
console.log("any param hmac", hexOf(createHmac("sha256", "key")));
|
||||
|
||||
const hash = createHash("sha1");
|
||||
const held: unknown = hash;
|
||||
console.log("typeof", typeof held, String(held), Object.prototype.toString.call(held));
|
||||
console.log("cast keeps identity", (held as Hash) === hash);
|
||||
const chained: any = held;
|
||||
console.log("update answers the value", chained.update("a") === chained);
|
||||
console.log("typed digest after untyped update", hash.digest("hex"));
|
||||
console.log("digest again", code(() => chained.digest("hex")));
|
||||
console.log("update after digest", code(() => chained.update("x")));
|
||||
|
||||
const values: unknown[] = [createHash("md5"), createHmac("sha512", "k"), createHash("sha256")];
|
||||
for (const value of values) {
|
||||
const v = value as any;
|
||||
v.update("one");
|
||||
v.update(Buffer.from("two"));
|
||||
v.update("74687265", "hex");
|
||||
console.log("array", v.digest("base64"));
|
||||
}
|
||||
|
||||
const record: { label: string; hash: unknown } = { label: "rec", hash: createHash("sha384") };
|
||||
const fromRecord = record.hash as Hash;
|
||||
fromRecord.update("record");
|
||||
console.log(record.label, fromRecord.digest("hex").slice(0, 32));
|
||||
|
||||
const original: any = createHash("sha256").update("prefix");
|
||||
const copy = original.copy();
|
||||
copy.update("-copy");
|
||||
original.update("-original");
|
||||
console.log("copy", copy.digest("hex").slice(0, 16), original.digest("hex").slice(0, 16));
|
||||
console.log("copy is a new value", copy === original);
|
||||
const shake: any = createHash("shake256");
|
||||
shake.update("x");
|
||||
console.log("copy outputLength", shake.copy({ outputLength: 8 }).digest("hex"));
|
||||
console.log("copy bad outputLength", code(() => shake.copy({ outputLength: -1 })));
|
||||
|
||||
const mac: unknown = createHmac("sha256", "secret");
|
||||
const macAny: any = mac;
|
||||
console.log("hmac copy", typeof macAny.copy, macAny.copy === undefined);
|
||||
console.log("hmac copy call", code(() => macAny.copy()));
|
||||
console.log("hmac digest", (mac as Hmac).update("data").digest("hex").slice(0, 16));
|
||||
console.log("hmac digest again", JSON.stringify(macAny.digest("hex")));
|
||||
|
||||
console.log("missing member", macAny.nope, typeof chained.missing);
|
||||
console.log("bad data", code(() => (createHash("sha256") as any).update(42)));
|
||||
console.log("default digest", (createHash("md5") as any).update("z").digest().length);
|
||||
@@ -0,0 +1,77 @@
|
||||
// Hash and Hmac values passed through untyped JavaScript code: function
|
||||
// parameters, object fields, arrays and Map values. update() answers the
|
||||
// same value, digest() and copy() follow Node, and Hmac has no copy().
|
||||
const crypto = require("node:crypto");
|
||||
|
||||
function code(run) {
|
||||
try {
|
||||
const value = run();
|
||||
return typeof value === "string" ? `ok ${value.slice(0, 16)}` : `ok ${typeof value}`;
|
||||
} catch (error) {
|
||||
return `${error.name} ${error.code || ""} ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function digestAll(items, encoding) {
|
||||
return items.map((item) => item.update("payload").digest(encoding));
|
||||
}
|
||||
|
||||
console.log(digestAll([crypto.createHash("sha256"), crypto.createHmac("sha1", "k")], "hex"));
|
||||
|
||||
const box = { hash: crypto.createHash("md5"), mac: crypto.createHmac("sha256", Buffer.from("key")) };
|
||||
box.hash.update("a").update("b");
|
||||
console.log("field", box.hash.digest("hex"), box.mac.update("c").digest("base64url"));
|
||||
|
||||
const byName = new Map();
|
||||
byName.set("sha512", crypto.createHash("sha512"));
|
||||
byName.set("hmac", crypto.createHmac("md5", "x"));
|
||||
for (const [name, value] of byName) {
|
||||
const same = value.update(name);
|
||||
console.log("map", name, same === value, value.digest("hex").slice(0, 20));
|
||||
}
|
||||
|
||||
function copyTwice(h) {
|
||||
const a = h.copy();
|
||||
const b = h.copy();
|
||||
a.update("1");
|
||||
b.update("2");
|
||||
return [a.digest("hex").slice(0, 12), b.digest("hex").slice(0, 12), h.digest("hex").slice(0, 12)];
|
||||
}
|
||||
console.log("copies", copyTwice(crypto.createHash("sha256").update("base")));
|
||||
|
||||
function describe(value) {
|
||||
return [typeof value, String(value), Object.prototype.toString.call(value), value.notAMember];
|
||||
}
|
||||
console.log("hash", describe(crypto.createHash("sha256")));
|
||||
console.log("hmac", describe(crypto.createHmac("sha256", "k")));
|
||||
const plainMac = [crypto.createHmac("sha256", "k")][0];
|
||||
console.log("hmac copy read", typeof plainMac.copy, plainMac.copy === undefined);
|
||||
|
||||
const mac = crypto.createHmac("sha256", "k");
|
||||
const viaObject = { mac };
|
||||
console.log("hmac copy", code(() => viaObject.mac.copy()));
|
||||
console.log("digest", viaObject.mac.digest("hex").slice(0, 16), JSON.stringify(viaObject.mac.digest("hex")));
|
||||
console.log("update after digest", code(() => viaObject.mac.update("x")));
|
||||
|
||||
const used = [crypto.createHash("sha1")];
|
||||
used[0].digest();
|
||||
console.log("digest twice", code(() => used[0].digest("hex")));
|
||||
console.log("copy after digest", code(() => used[0].copy()));
|
||||
console.log("update bad", code(() => [crypto.createHash("sha1")][0].update(null)));
|
||||
|
||||
function methods(h, m) {
|
||||
return [h.update === m.update, h.digest === m.digest, typeof h.copy, typeof m.copy,
|
||||
h.update.name, h.update.length, h.digest.name, h.digest.length, h.copy.length];
|
||||
}
|
||||
console.log("methods", methods(crypto.createHash("sha256"), crypto.createHmac("sha1", "k")));
|
||||
function viaCall(h) {
|
||||
const update = h.update;
|
||||
const digest = h.digest;
|
||||
return [update.call(h, "abc") === h, digest.call(h, "hex").slice(0, 16)];
|
||||
}
|
||||
console.log("call", viaCall(crypto.createHash("sha256")), viaCall(crypto.createHmac("sha256", "k")));
|
||||
function sharedUpdate(h, m) {
|
||||
h.update.call(m, "shared");
|
||||
return m.digest("hex").slice(0, 16);
|
||||
}
|
||||
console.log("shared update", sharedUpdate(crypto.createHash("md5"), crypto.createHmac("md5", "k")));
|
||||
@@ -0,0 +1,46 @@
|
||||
// A missing method called on a Hash, Hmac or KeyObject held in an untyped
|
||||
// value throws Node's TypeError with the whole callee text, also when the
|
||||
// text is longer than a fixed-size message buffer.
|
||||
const { createHash, createHmac, createSecretKey } = require("node:crypto");
|
||||
|
||||
const holder = {
|
||||
aHashValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes:
|
||||
createHash("sha256"),
|
||||
anHmacValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes:
|
||||
createHmac("sha256", "key"),
|
||||
aSecretKeyObjectStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixty:
|
||||
createSecretKey(Buffer.alloc(16, 1)),
|
||||
};
|
||||
|
||||
function report(label, run) {
|
||||
try {
|
||||
run();
|
||||
console.log(label, "did not throw");
|
||||
} catch (e) {
|
||||
console.log(label, e instanceof TypeError, e.name, e.message.length);
|
||||
console.log(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
report("hash", () =>
|
||||
holder.aHashValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes.missingMethodOnTheHashValue(),
|
||||
);
|
||||
report("hmac", () =>
|
||||
holder.anHmacValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes.missingMethodOnTheHmacValue(),
|
||||
);
|
||||
report("key", () =>
|
||||
holder.aSecretKeyObjectStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixty.missingMethodOnTheKeyObject(),
|
||||
);
|
||||
|
||||
// A short callee text through an untyped parameter, for comparison.
|
||||
function callMissing(value) {
|
||||
return value.nope();
|
||||
}
|
||||
report("short", () =>
|
||||
callMissing(holder.aHashValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes),
|
||||
);
|
||||
console.log(
|
||||
holder.aHashValueStoredUnderAPropertyNameLongEnoughThatTheCalleeTextOfTheMissingMethodRunsPastOneHundredAndSixtyBytes
|
||||
.update("abc")
|
||||
.digest("hex"),
|
||||
);
|
||||
@@ -0,0 +1,214 @@
|
||||
// Iterator helpers over generators, stored array iterators and Map/Set
|
||||
// iterators: chains that end in a consuming method, helper objects driven
|
||||
// by next(), for...of, spreads, Array.from and yield*, and the points at
|
||||
// which a helper closes its source (runs its return()).
|
||||
const log: string[] = [];
|
||||
function* count(label: string, n: number): Generator<number> {
|
||||
try {
|
||||
for (let i = 1; i <= n; i++) {
|
||||
log.push(`${label}${i}`);
|
||||
yield i;
|
||||
}
|
||||
} finally {
|
||||
log.push(`${label}:closed`);
|
||||
}
|
||||
}
|
||||
function flush(what: unknown): void {
|
||||
console.log(what, "|", log.join(" "));
|
||||
log.length = 0;
|
||||
}
|
||||
|
||||
// Consuming methods over generator chains.
|
||||
flush(count("a", 5).map((x) => x * 10).filter((x) => x !== 20).toArray());
|
||||
flush(count("b", 4).reduce((acc, x) => acc + x));
|
||||
flush(count("c", 4).reduce((acc, x, i) => acc + x * i, 100));
|
||||
flush(count("d", 5).some((x) => x === 2));
|
||||
flush(count("e", 5).every((x) => x < 3));
|
||||
flush(count("f", 5).find((x) => x > 3) ?? -1);
|
||||
flush(count("g", 2).find((x) => x > 3) ?? -1);
|
||||
let each = "";
|
||||
count("h", 3).forEach((x, i) => {
|
||||
each += `${x}@${i} `;
|
||||
});
|
||||
flush(each.trim());
|
||||
|
||||
// take, drop and their budgets.
|
||||
flush(count("i", 9).drop(2).take(3).toArray());
|
||||
flush(count("j", 3).take(Infinity).toArray());
|
||||
flush([count("k", 4).take(2.9).toArray(), count("l", 4).drop(1.5).toArray()]);
|
||||
for (const budget of [-1, NaN, -0.5]) {
|
||||
try {
|
||||
console.log(count("m", 1).take(budget).toArray());
|
||||
} catch (e) {
|
||||
console.log((e as Error).name, (e as Error).message);
|
||||
}
|
||||
}
|
||||
try {
|
||||
count("n", 0).reduce((a, b) => a + b);
|
||||
} catch (e) {
|
||||
console.log((e as Error).name, (e as Error).message);
|
||||
}
|
||||
flush("budgets");
|
||||
|
||||
// An invalid budget closes a started source before the RangeError.
|
||||
const invalid = count("bad", 3);
|
||||
invalid.next();
|
||||
try {
|
||||
invalid.take(-1);
|
||||
} catch (e) {
|
||||
log.push((e as Error).message);
|
||||
}
|
||||
flush(invalid.next().done);
|
||||
|
||||
// A started source closes through take(0) without another pull.
|
||||
const started = count("o", 3);
|
||||
started.next();
|
||||
flush([started.take(0).toArray(), started.next().done]);
|
||||
|
||||
// Helper objects step lazily, one source value per delivered value.
|
||||
const h = count("p", 4)
|
||||
.map((x) => x * 2)
|
||||
.filter((x) => x !== 4);
|
||||
log.push("made");
|
||||
console.log(h.next(), h.next());
|
||||
console.log(h.next(), h.next());
|
||||
flush("next");
|
||||
const words = count("q", 3).map((x, i) => "w".repeat(x) + i);
|
||||
flush([...words]);
|
||||
flush(Array.from(count("r", 3).drop(1)));
|
||||
function* wrap(): Generator<number> {
|
||||
yield 0;
|
||||
yield* count("s", 2).map((x) => -x);
|
||||
yield 9;
|
||||
}
|
||||
flush([...wrap()]);
|
||||
function sum(it: Iterator<number>): number {
|
||||
let total = 0;
|
||||
for (let r = it.next(); !r.done; r = it.next()) total += r.value;
|
||||
return total;
|
||||
}
|
||||
flush(sum(count("t", 4).filter((x) => x % 2 === 0)));
|
||||
|
||||
// next() from inside the helper's own callback.
|
||||
const self: IteratorObject<number> = count("re", 2).map((x) => {
|
||||
if (x === 2) {
|
||||
try {
|
||||
self.next();
|
||||
} catch (e) {
|
||||
log.push(`${(e as Error).name}: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
return x;
|
||||
});
|
||||
flush(self.toArray());
|
||||
|
||||
// Leaving a for...of over a helper closes the helper and its source.
|
||||
for (const x of count("u", 9).map((v) => v * v)) {
|
||||
if (x > 4) break;
|
||||
log.push(`got${x}`);
|
||||
}
|
||||
flush("break");
|
||||
try {
|
||||
count("v", 3).forEach((x) => {
|
||||
if (x === 2) throw new Error("callback threw");
|
||||
});
|
||||
} catch (e) {
|
||||
log.push((e as Error).message);
|
||||
}
|
||||
flush("throw");
|
||||
function* failing(): Generator<number> {
|
||||
try {
|
||||
yield 1;
|
||||
throw new Error("source threw");
|
||||
} finally {
|
||||
log.push("failing:finally");
|
||||
}
|
||||
}
|
||||
try {
|
||||
failing().map((x) => x).toArray();
|
||||
} catch (e) {
|
||||
log.push((e as Error).message);
|
||||
}
|
||||
flush("source");
|
||||
// When closing the source throws while another error propagates, the
|
||||
// first error is the one that reaches the caller.
|
||||
function* closeThrows(): Generator<number> {
|
||||
try {
|
||||
yield 1;
|
||||
} finally {
|
||||
log.push("closeThrows:finally");
|
||||
throw new Error("close threw");
|
||||
}
|
||||
}
|
||||
try {
|
||||
closeThrows()
|
||||
.map((x): number => {
|
||||
if (x > 0) throw new Error("callback threw");
|
||||
return x;
|
||||
})
|
||||
.toArray();
|
||||
} catch (e) {
|
||||
log.push((e as Error).message);
|
||||
}
|
||||
try {
|
||||
for (const x of closeThrows().filter(() => true)) throw new Error(`body threw at ${x}`);
|
||||
} catch (e) {
|
||||
log.push((e as Error).message);
|
||||
}
|
||||
try {
|
||||
for (const x of closeThrows()) throw new Error(`plain loop threw at ${x}`);
|
||||
} catch (e) {
|
||||
log.push((e as Error).message);
|
||||
}
|
||||
flush("close errors");
|
||||
|
||||
// flatMap over arrays and generators; take stops mid-way through an inner
|
||||
// generator and closes it before the outer source.
|
||||
flush(count("w", 2).flatMap((x) => [x, -x]).toArray());
|
||||
flush(count("x", 3).flatMap((x) => count(`y${x}-`, 2)).take(3).toArray());
|
||||
|
||||
// Map and Set iterators, observed live.
|
||||
const set = new Set([1, 2, 3]);
|
||||
console.log(
|
||||
set
|
||||
.values()
|
||||
.map((x) => {
|
||||
if (x === 1) set.add(4);
|
||||
if (x === 2) set.delete(3);
|
||||
return x * 100;
|
||||
})
|
||||
.toArray(),
|
||||
);
|
||||
const map = new Map<string, number>([
|
||||
["a", 1],
|
||||
["b", 2],
|
||||
["c", 3],
|
||||
]);
|
||||
const pairs = map.entries().filter(([, v]) => v !== 2);
|
||||
map.set("d", 4);
|
||||
console.log([...pairs].map(([k, v]) => k + v).join(","));
|
||||
console.log(map.keys().map((k) => k.toUpperCase()).toArray(), map.values().reduce((a, b) => a + b, 0));
|
||||
console.log(set.keys().some((x) => x > 3), set.entries().find(([k]) => k === 2)?.[1]);
|
||||
|
||||
// Array iterators as helper sources.
|
||||
const nums = [5, 6, 7];
|
||||
const doubled = nums.values().map((x) => x * 2);
|
||||
nums.push(8);
|
||||
console.log([...doubled], nums.keys().filter((i) => i > 0).toArray(), [...nums.entries().drop(2)]);
|
||||
|
||||
// Records and strings flow through the stages by reference.
|
||||
interface Item {
|
||||
name: string;
|
||||
qty: number;
|
||||
}
|
||||
const items: Item[] = [
|
||||
{ name: "pen", qty: 3 },
|
||||
{ name: "ink", qty: 0 },
|
||||
{ name: "pad", qty: 7 },
|
||||
];
|
||||
function* stock(): Generator<Item> {
|
||||
yield* items;
|
||||
}
|
||||
const inStock = stock().filter((it) => it.qty > 0);
|
||||
console.log(inStock.next().value === items[0], stock().find((it) => it.name === "pad")?.qty);
|
||||
console.log(stock().map((it) => `${it.name}x${it.qty}`).toArray().join(" "));
|
||||
@@ -0,0 +1,58 @@
|
||||
// Iterator helper objects in JavaScript: inferred callback parameters,
|
||||
// return() on a helper (before its first next(), mid-way and after
|
||||
// completion), and chains over Map/Set iterators.
|
||||
const log = [];
|
||||
function* count(label, n) {
|
||||
try {
|
||||
for (let i = 1; i <= n; i++) {
|
||||
log.push(label + i);
|
||||
yield i;
|
||||
}
|
||||
} finally {
|
||||
log.push(label + ":closed");
|
||||
}
|
||||
}
|
||||
function flush(what) {
|
||||
console.log(what, "|", log.join(" "));
|
||||
log.length = 0;
|
||||
}
|
||||
|
||||
flush(count("a", 4).map((x) => x + 0.5).toArray());
|
||||
flush(count("b", 6).filter((x) => x % 2 === 1).map((x, i) => x * 10 + i).take(2).toArray());
|
||||
flush(count("c", 3).reduce((acc, x) => acc + x, ""));
|
||||
|
||||
// return() before the first next() closes a source that has started.
|
||||
const started = count("d", 3);
|
||||
started.next();
|
||||
const mapped = started.map((x) => x * 2);
|
||||
flush([mapped.return(), started.next()]);
|
||||
// ... and leaves an unstarted source without running its body.
|
||||
const fresh = count("e", 3);
|
||||
const filtered = fresh.filter(() => true);
|
||||
flush([filtered.return(), fresh.next(), filtered.next()]);
|
||||
// return() at a yield closes the source; later calls report done.
|
||||
const stepping = count("f", 5).take(4);
|
||||
flush([stepping.next(), stepping.return(), stepping.next(), stepping.return()]);
|
||||
// A chain of helpers closes through to the source.
|
||||
const chained = count("g", 5);
|
||||
chained.next();
|
||||
const chain = chained.map((x) => x).filter(() => true).take(3);
|
||||
flush([chain.return(), chained.next()]);
|
||||
|
||||
// Map and Set iterators.
|
||||
const scores = new Map([
|
||||
["ann", 3],
|
||||
["bob", 9],
|
||||
["cy", 5],
|
||||
]);
|
||||
console.log(scores.entries().filter(([, v]) => v > 4).map(([k]) => k).toArray());
|
||||
console.log(scores.values().every((v) => v > 2), scores.keys().find((k) => k.length === 2));
|
||||
const tags = new Set(["x", "y", "z"]);
|
||||
const upper = tags.values().map((t) => t.toUpperCase());
|
||||
tags.add("w");
|
||||
console.log([...upper], tags.keys().drop(3).toArray());
|
||||
for (const t of tags.values().flatMap((t) => [t, t + t])) {
|
||||
if (t === "yy") break;
|
||||
log.push(t);
|
||||
}
|
||||
flush("flat");
|
||||
@@ -0,0 +1,75 @@
|
||||
// `x instanceof Writable` answers true for Duplex streams and their
|
||||
// subclasses, as Node's Writable[Symbol.hasInstance] does. Subclasses of
|
||||
// Writable keep the prototype test.
|
||||
import { Duplex, PassThrough, Readable, Transform, Writable } from "node:stream";
|
||||
import * as zlib from "node:zlib";
|
||||
import { EventEmitter } from "node:events";
|
||||
|
||||
class Sink extends Writable {
|
||||
override _write(_chunk: Buffer, _encoding: BufferEncoding, callback: () => void): void {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
class Pipe extends Duplex {
|
||||
override _read(): void {}
|
||||
override _write(_chunk: Buffer, _encoding: BufferEncoding, callback: () => void): void {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
class Upper extends Transform {
|
||||
override _transform(
|
||||
chunk: Buffer,
|
||||
_encoding: BufferEncoding,
|
||||
callback: (error: Error | null, data?: string) => void,
|
||||
): void {
|
||||
callback(null, chunk.toString().toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
const writable = new Writable({
|
||||
write(_chunk, _encoding, callback) {
|
||||
callback();
|
||||
},
|
||||
});
|
||||
const duplex = new Duplex({
|
||||
read() {},
|
||||
write(_chunk, _encoding, callback) {
|
||||
callback();
|
||||
},
|
||||
});
|
||||
const pass = new PassThrough();
|
||||
const transform = new Transform({
|
||||
transform(chunk, _encoding, callback) {
|
||||
callback(null, chunk);
|
||||
},
|
||||
});
|
||||
const readable = new Readable({ read() {} });
|
||||
|
||||
console.log("writable", writable instanceof Writable, new Sink() instanceof Writable);
|
||||
console.log(
|
||||
"duplex family",
|
||||
duplex instanceof Writable,
|
||||
pass instanceof Writable,
|
||||
transform instanceof Writable,
|
||||
new Pipe() instanceof Writable,
|
||||
new Upper() instanceof Writable,
|
||||
zlib.createGzip() instanceof Writable,
|
||||
);
|
||||
console.log("readable", readable instanceof Writable, readable instanceof Duplex);
|
||||
console.log("subclass of writable", pass instanceof Sink, duplex instanceof Sink, new Sink() instanceof Sink);
|
||||
|
||||
const asReadable: Readable = pass;
|
||||
const asEmitter: EventEmitter = new Pipe();
|
||||
const plainEmitter: EventEmitter = new EventEmitter();
|
||||
console.log("base-typed", asReadable instanceof Writable, asEmitter instanceof Writable, plainEmitter instanceof Writable);
|
||||
|
||||
function describe(value: Readable | string): string {
|
||||
return value instanceof Writable ? "writable side" : "no writable side";
|
||||
}
|
||||
console.log("union", describe(duplex), describe(readable), describe("text"));
|
||||
|
||||
try {
|
||||
throw transform;
|
||||
} catch (err) {
|
||||
console.log("caught", err instanceof Writable, err instanceof Readable);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// React Flight's production server as compiled from its sources: the V8
|
||||
// structured-stack parser (ReactFlightStackConfigV8) is development-only and
|
||||
// is referenced only after the production `throw` stubs, so a production
|
||||
// render never parses a stack. Statements after an abrupt statement never
|
||||
// run: they may use APIs with no static lowering (Error.prepareStackTrace,
|
||||
// CallSite) and must not make the program keep stack frames.
|
||||
// V8's structured stack API, declared here as React's Flow build declares
|
||||
// it (the program's own types, not Node's).
|
||||
interface CallSite {
|
||||
getTypeName(): string | null;
|
||||
getMethodName(): string | null;
|
||||
getFunctionName(): string | null;
|
||||
getScriptNameOrSourceURL(): string | null;
|
||||
getEvalOrigin(): string | undefined;
|
||||
getLineNumber(): number | null;
|
||||
getColumnNumber(): number | null;
|
||||
isNative(): boolean;
|
||||
isAsync(): boolean;
|
||||
isConstructor(): boolean;
|
||||
isToplevel(): boolean;
|
||||
isEval(): boolean;
|
||||
toString(): string;
|
||||
}
|
||||
declare global {
|
||||
interface ErrorConstructor {
|
||||
prepareStackTrace?: (error: Error, structuredStackTrace: CallSite[]) => string;
|
||||
}
|
||||
}
|
||||
type ReactStackTrace = Array<[string, string, number, number, number, number, boolean]>;
|
||||
|
||||
let framesToSkip: number = 0;
|
||||
let collectedStackTrace: null | ReactStackTrace = null;
|
||||
const identifierRegExp = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
|
||||
|
||||
function getMethodCallName(callSite: CallSite): string {
|
||||
const typeName = callSite.getTypeName();
|
||||
const methodName = callSite.getMethodName();
|
||||
const functionName = callSite.getFunctionName();
|
||||
let result = '';
|
||||
if (functionName) {
|
||||
if (typeName && identifierRegExp.test(functionName) && functionName !== typeName) {
|
||||
result += typeName + '.';
|
||||
}
|
||||
result += functionName;
|
||||
if (methodName && functionName !== methodName && !functionName.endsWith('.' + methodName) && !functionName.endsWith(' ' + methodName)) {
|
||||
result += ' [as ' + methodName + ']';
|
||||
}
|
||||
} else {
|
||||
if (typeName) {
|
||||
result += typeName + '.';
|
||||
}
|
||||
result += methodName ? methodName : '<anonymous>';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function collectStackTracePrivate(error: Error, structuredStackTrace: CallSite[]): string {
|
||||
const result: ReactStackTrace = [];
|
||||
for (let i = framesToSkip; i < structuredStackTrace.length; i++) {
|
||||
const callSite = structuredStackTrace[i];
|
||||
let name = callSite.getFunctionName() || '<anonymous>';
|
||||
if (name.includes('react_stack_bottom_frame')) {
|
||||
break;
|
||||
} else if (callSite.isNative()) {
|
||||
result.push([name, '', 0, 0, 0, 0, callSite.isAsync()]);
|
||||
} else {
|
||||
if (callSite.isConstructor()) {
|
||||
name = 'new ' + name;
|
||||
} else if (!callSite.isToplevel()) {
|
||||
name = getMethodCallName(callSite);
|
||||
}
|
||||
if (name === '<anonymous>') {
|
||||
name = '';
|
||||
}
|
||||
let filename = callSite.getScriptNameOrSourceURL() || '<anonymous>';
|
||||
if (filename === '<anonymous>') {
|
||||
filename = '';
|
||||
if (callSite.isEval()) {
|
||||
const origin = callSite.getEvalOrigin();
|
||||
if (origin) {
|
||||
filename = origin.toString() + ', <anonymous>';
|
||||
}
|
||||
}
|
||||
}
|
||||
const line = callSite.getLineNumber() || 0;
|
||||
const col = callSite.getColumnNumber() || 0;
|
||||
result.push([name, filename, line, col, 0, 0, callSite.isAsync()]);
|
||||
}
|
||||
}
|
||||
collectedStackTrace = result;
|
||||
return '';
|
||||
}
|
||||
|
||||
function collectStackTrace(error: Error, structuredStackTrace: CallSite[]): string {
|
||||
collectStackTracePrivate(error, structuredStackTrace);
|
||||
const name = error.name || 'Error';
|
||||
const message = error.message || '';
|
||||
let stack = name + ': ' + message;
|
||||
for (let i = 0; i < structuredStackTrace.length; i++) {
|
||||
stack += '\n at ' + structuredStackTrace[i].toString();
|
||||
}
|
||||
return stack;
|
||||
}
|
||||
|
||||
function parseStackTrace(error: Error, skipFrames: number): ReactStackTrace {
|
||||
collectedStackTrace = null;
|
||||
framesToSkip = skipFrames;
|
||||
const previousPrepare = Error.prepareStackTrace;
|
||||
Error.prepareStackTrace = collectStackTrace;
|
||||
let stack;
|
||||
try {
|
||||
stack = String(error.stack);
|
||||
} finally {
|
||||
Error.prepareStackTrace = previousPrepare;
|
||||
}
|
||||
if (collectedStackTrace !== null) {
|
||||
const result = collectedStackTrace;
|
||||
collectedStackTrace = null;
|
||||
return result;
|
||||
}
|
||||
return stack.split('\n').length > 0 ? [] : [];
|
||||
}
|
||||
|
||||
function renderDebugModel(value: any): any {
|
||||
if (value !== null && typeof value === 'object' && value._debugStack != null) {
|
||||
return parseStackTrace(value._debugStack, 1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function emitOutlinedDebugModelChunk(model: any): string {
|
||||
throw new Error('emitOutlinedDebugModel should never be called in production mode. This is a bug in React.');
|
||||
function replacer(this: any, parentPropertyName: string, value: any): any {
|
||||
return renderDebugModel(this[parentPropertyName]);
|
||||
}
|
||||
const json: string = JSON.stringify(model, replacer);
|
||||
return json;
|
||||
}
|
||||
|
||||
function outlineComponentInfo(componentInfo: any): string {
|
||||
throw new Error('outlineComponentInfo should never be called in production mode. This is a bug in React.');
|
||||
const counter = { objectLimit: 10 };
|
||||
if (componentInfo.debugStack != null) {
|
||||
counter.objectLimit += parseStackTrace(componentInfo.debugStack, 1).length;
|
||||
}
|
||||
return emitOutlinedDebugModelChunk(componentInfo) + counter.objectLimit;
|
||||
}
|
||||
|
||||
// The production render: elements become `id:json` rows.
|
||||
type Element = { type: string; props: { [key: string]: string }; children: Element[] };
|
||||
function el(type: string, props: { [key: string]: string }, children: Element[]): Element {
|
||||
return { type, props, children };
|
||||
}
|
||||
function renderModel(node: Element): string {
|
||||
const children = node.children.map(renderModel);
|
||||
return '["$","' + node.type + '",null,' + JSON.stringify(node.props).slice(0, -1) + (children.length > 0 ? (Object.keys(node.props).length > 0 ? ',' : '') + '"children":[' + children.join(',') + ']' : '') + '}]';
|
||||
}
|
||||
const rows: string[] = [];
|
||||
const items: Element[] = [];
|
||||
for (let i = 0; i < 3; i++) items.push(el('li', { key: 'k' + i }, []));
|
||||
rows.push('0:' + renderModel(el('main', {}, [el('ul', { className: 'list' }, items)])));
|
||||
console.log(rows.join('\n'));
|
||||
|
||||
// The production stubs themselves run and throw; their DEV bodies never do.
|
||||
for (const stub of [() => outlineComponentInfo({ debugStack: new Error('owner') }), () => emitOutlinedDebugModelChunk({})]) {
|
||||
try {
|
||||
stub();
|
||||
} catch (error) {
|
||||
console.log((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,79 @@
|
||||
// Stack captures keep the frames of every function active when the Error is
|
||||
// created: callers of constructors and closures, recursion, and errors a
|
||||
// runtime operation creates and a local catch takes. Leaf functions and
|
||||
// accessors that cannot create an Error run in between. Captures record
|
||||
// frames when the Error is created and format them on the first `.stack`
|
||||
// read.
|
||||
let lastCaught: Error | null = null;
|
||||
|
||||
function parseNumber(text: string): number {
|
||||
try {
|
||||
return JSON.parse(text) as number;
|
||||
} catch (error) {
|
||||
lastCaught = error as Error;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
class Failure {
|
||||
readonly error: Error;
|
||||
constructor(reason: string) {
|
||||
this.error = new Error(reason);
|
||||
}
|
||||
}
|
||||
function buildFailure(reason: string): Failure {
|
||||
return new Failure(reason);
|
||||
}
|
||||
|
||||
class Counter {
|
||||
count: number = 0;
|
||||
get next(): number {
|
||||
return ++this.count;
|
||||
}
|
||||
}
|
||||
|
||||
function leaf(n: number): number {
|
||||
return n * 2 + 1;
|
||||
}
|
||||
function sumLeaves(n: number): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < n; i++) total += leaf(i);
|
||||
return total;
|
||||
}
|
||||
function viaClosure(make: () => Error): Error {
|
||||
return make();
|
||||
}
|
||||
function recurse(n: number): Error {
|
||||
return n === 0 ? new Error('deep') : recurse(n - 1);
|
||||
}
|
||||
function hasFrame(error: Error, name: string): boolean {
|
||||
return error.stack!.split('\n').some((line) => line.includes(' ' + name + ' ') || line.endsWith(' ' + name));
|
||||
}
|
||||
|
||||
const counter = new Counter();
|
||||
console.log(parseNumber('12'), parseNumber('{'), counter.next, counter.next, sumLeaves(4));
|
||||
const caught = lastCaught!;
|
||||
console.log(caught.name, caught.stack!.startsWith('SyntaxError: '), hasFrame(caught, 'parseNumber'));
|
||||
|
||||
const constructed = buildFailure('in constructor').error;
|
||||
console.log(constructed.stack!.split('\n')[0], hasFrame(constructed, 'buildFailure'));
|
||||
|
||||
const closure = viaClosure(() => new Error('in closure'));
|
||||
console.log(closure.stack!.split('\n')[0], hasFrame(closure, 'viaClosure'));
|
||||
|
||||
// Frames are recorded at creation: a later limit and later calls do not
|
||||
// change them, and the first read fixes the text.
|
||||
Error.stackTraceLimit = 3;
|
||||
const deep = recurse(10);
|
||||
Error.stackTraceLimit = 10;
|
||||
sumLeaves(100);
|
||||
const first = deep.stack!;
|
||||
console.log(first.split('\n').length, hasFrame(deep, 'recurse'), first === deep.stack);
|
||||
deep.message = 'changed';
|
||||
console.log(deep.stack === first);
|
||||
|
||||
// An Error created with the limit at zero has no frames.
|
||||
Error.stackTraceLimit = 0;
|
||||
const bare = recurse(2);
|
||||
Error.stackTraceLimit = 10;
|
||||
console.log(JSON.stringify(bare.stack));
|
||||
@@ -0,0 +1,85 @@
|
||||
// Statements after return, throw, break or continue never run, while the
|
||||
// declarations among them keep their hoisting: a later function declaration
|
||||
// is callable from above, a later `var` reads undefined, and a later
|
||||
// `let`/`const` stays in its temporal dead zone.
|
||||
function hoistedCall(): number {
|
||||
return twice(21);
|
||||
console.log('never');
|
||||
function twice(n: number): number {
|
||||
return n * 2;
|
||||
}
|
||||
}
|
||||
|
||||
function hoistedVar(): string {
|
||||
const before = typeof late;
|
||||
return before;
|
||||
var late: string | undefined = 'assigned';
|
||||
console.log(late);
|
||||
}
|
||||
|
||||
function deadZone(): string {
|
||||
const read = (): number => counted;
|
||||
try {
|
||||
return String(read());
|
||||
} catch (error) {
|
||||
return (error as Error).name + ': ' + (error as Error).message;
|
||||
}
|
||||
const counted = 1;
|
||||
}
|
||||
|
||||
function closureReference(): number {
|
||||
const call = (): number => helper();
|
||||
return call();
|
||||
function helper(): number {
|
||||
return 7;
|
||||
}
|
||||
}
|
||||
|
||||
function loops(): string {
|
||||
const seen: string[] = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
if (i === 1) {
|
||||
seen.push('skip');
|
||||
continue;
|
||||
seen.push('never');
|
||||
}
|
||||
if (i === 3) {
|
||||
seen.push('stop');
|
||||
break;
|
||||
seen.push('never');
|
||||
}
|
||||
seen.push('i' + i);
|
||||
}
|
||||
return seen.join(',');
|
||||
}
|
||||
|
||||
function cases(n: number): string {
|
||||
switch (n) {
|
||||
case 0:
|
||||
return 'zero';
|
||||
console.log('never');
|
||||
case 1:
|
||||
n += 10;
|
||||
case 2:
|
||||
return 'fell to ' + n;
|
||||
default:
|
||||
throw new RangeError('n=' + n);
|
||||
console.log('never');
|
||||
}
|
||||
}
|
||||
|
||||
function stub(): string {
|
||||
throw new Error('stub');
|
||||
const unused = [1, 2, 3].map((x) => x * 2);
|
||||
return unused.join();
|
||||
}
|
||||
|
||||
console.log(hoistedCall(), hoistedVar(), deadZone(), closureReference(), loops());
|
||||
console.log(cases(0), cases(1), cases(2));
|
||||
for (const run of [() => cases(5), stub]) {
|
||||
try {
|
||||
run();
|
||||
} catch (error) {
|
||||
console.log((error as Error).name, (error as Error).message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// A JavaScript Error subclass that overrides name and message with accessor
|
||||
// pairs, as quickjs-wasi's JSException does. Writing `.name` or `.message`
|
||||
// on an ordinary Error builds the Error property dispatchers, whose branch
|
||||
// for this class calls the JavaScript setters with an untyped value.
|
||||
class GuestException extends Error {
|
||||
#name;
|
||||
#message;
|
||||
constructor(name, message) {
|
||||
super();
|
||||
this.#name = name;
|
||||
this.#message = message;
|
||||
}
|
||||
get name() {
|
||||
return this.#name;
|
||||
}
|
||||
set name(v) {
|
||||
this.#name = v;
|
||||
}
|
||||
get message() {
|
||||
return this.#message;
|
||||
}
|
||||
set message(v) {
|
||||
this.#message = `[${v}]`;
|
||||
}
|
||||
}
|
||||
|
||||
const guest = new GuestException("GuestError", "boom");
|
||||
console.log(guest.name, guest.message, String(guest));
|
||||
guest.name = "Renamed";
|
||||
guest.message = "changed";
|
||||
console.log(guest.name, guest.message, String(guest));
|
||||
|
||||
const plain = new Error("plain");
|
||||
plain.name = "PlainName";
|
||||
plain.message = "plain message";
|
||||
console.log(plain.name, plain.message, String(plain));
|
||||
|
||||
/** @type {Error[]} */
|
||||
const errors = [plain, guest, new TypeError("bad")];
|
||||
for (const err of errors) console.log(String(err));
|
||||
try {
|
||||
throw guest;
|
||||
} catch (caught) {
|
||||
console.log(caught instanceof GuestException, caught instanceof Error, String(caught));
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// @deferred-fences: 2
|
||||
// A JavaScript dependency's WebAssembly glue: the feature probe answers as
|
||||
// in Node, and the loader that would instantiate a module is never reached
|
||||
// (reaching it would throw scriptc's located WebAssembly fence).
|
||||
function supportsWasm() {
|
||||
return typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function";
|
||||
}
|
||||
|
||||
async function instantiate(bytes, imports) {
|
||||
const { instance } = await WebAssembly.instantiate(bytes, imports);
|
||||
return instance.exports;
|
||||
}
|
||||
|
||||
const backend = process.argv.includes("--wasm") ? instantiate : null;
|
||||
console.log("supports wasm:", supportsWasm());
|
||||
console.log("backend:", backend === null ? "javascript" : "wasm");
|
||||
@@ -0,0 +1,22 @@
|
||||
// Declarations that mention the WebAssembly API's types compile: the types
|
||||
// have the checked-dynamic representation, and only a reached use of the
|
||||
// API itself is refused. typeof needs no value and folds to Node's answer.
|
||||
interface LoaderOptions {
|
||||
wasm?: BufferSource | WebAssembly.Module;
|
||||
imports?: WebAssembly.Imports;
|
||||
memory?: WebAssembly.Memory;
|
||||
}
|
||||
|
||||
function describe(name: string, options: LoaderOptions): string {
|
||||
const parts = [name];
|
||||
parts.push(options.wasm === undefined ? "no module" : "module given");
|
||||
parts.push(options.imports === undefined ? "no imports" : "imports given");
|
||||
return parts.join(", ");
|
||||
}
|
||||
|
||||
console.log(describe("empty", {}));
|
||||
console.log(describe("bytes", { wasm: new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]) }));
|
||||
console.log(typeof WebAssembly, typeof globalThis.WebAssembly);
|
||||
console.log(typeof WebAssembly.compile, typeof WebAssembly.instantiate, typeof WebAssembly.validate);
|
||||
console.log(typeof WebAssembly.Module, typeof WebAssembly.Instance, typeof WebAssembly.Memory);
|
||||
console.log(typeof WebAssembly.RuntimeError, typeof WebAssembly.CompileError);
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
// (module (func (export "answer") (result i32) i32.const 42)), inlined the
|
||||
// way undici inlines llhttp.
|
||||
const ANSWER = "AGFzbQEAAAABBQFgAAF/AwIBAAcKAQZhbnN3ZXIAAAoGAQQAQSoL";
|
||||
|
||||
export function hasModule() {
|
||||
return existsSync(new URL("./add.wasm", import.meta.url).pathname);
|
||||
}
|
||||
|
||||
export function inlineModule() {
|
||||
return ANSWER;
|
||||
}
|
||||
|
||||
export async function instantiate(bytes) {
|
||||
const { instance } = await WebAssembly.instantiate(bytes, {});
|
||||
return instance.exports;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// The WebAssembly modules this program can load are visible to the build in
|
||||
// four ways (wasm/discover.ts): a path the compiler resolves (glue.js), an
|
||||
// inline base64 literal (glue.js), a .wasm file shipped by a package whose
|
||||
// code mentions WebAssembly (wasm-fixture-pkg), and the scriptc.wasm list in
|
||||
// package.json (build/generated.wasm).
|
||||
import { hasModule, inlineModule } from "./glue.js";
|
||||
import { describeModule } from "wasm-fixture-pkg";
|
||||
|
||||
console.log(hasModule(), inlineModule().length, describeModule());
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function describeModule(path?: string): string;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Loads lib/core.wasm from a path computed at run time, so only the
|
||||
// package scan finds the module.
|
||||
export function describeModule(path) {
|
||||
const where = path ?? "lib/core.wasm";
|
||||
return typeof WebAssembly === "object" ? `wasm at ${where}` : "no wasm";
|
||||
}
|
||||
BIN
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "wasm-fixture-pkg",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "wasm-coverage-fixture",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scriptc": {
|
||||
"wasm": ["build/generated.wasm"]
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
// Stored collection cursors support next/iteration, but helper methods are
|
||||
// refused explicitly instead of dispatching to array helpers at runtime.
|
||||
const map = new Map<string, number>();
|
||||
const values = map.values();
|
||||
values.map(value => value + 1);
|
||||
map.keys().toArray();
|
||||
const set = new Set<number>();
|
||||
set.values().filter(value => value > 0);
|
||||
set.entries().take(1);
|
||||
// Iterator helpers lower over Map/Set iterators, generators and array
|
||||
// iterators. These helper forms are refused explicitly.
|
||||
const set = new Set<number>([1, 2]);
|
||||
// flatMap callbacks return arrays or generators.
|
||||
set.values().flatMap((value) => new Set([value, value + 1]));
|
||||
// return() on a helper object.
|
||||
const mapped = set.values().map((value) => value * 2);
|
||||
mapped.return!();
|
||||
// Each helper call must produce a diagnostic.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// No WebAssembly module compiles to native code yet: every reached use of
|
||||
// the API refuses at its site, in each spelling, while declarations that
|
||||
// only mention its types compile.
|
||||
interface LoaderOptions {
|
||||
wasm?: BufferSource | WebAssembly.Module;
|
||||
imports?: WebAssembly.Imports;
|
||||
}
|
||||
|
||||
async function load(options: LoaderOptions, bytes: Uint8Array): Promise<void> {
|
||||
console.log(WebAssembly.validate(bytes));
|
||||
const module = await WebAssembly.compile(bytes);
|
||||
const { instance } = await WebAssembly.instantiate(bytes, options.imports);
|
||||
console.log(instance instanceof WebAssembly.Instance, module);
|
||||
const direct = new WebAssembly.Instance(new WebAssembly.Module(bytes));
|
||||
console.log(direct.exports);
|
||||
const memory = new WebAssembly.Memory({ initial: 1 });
|
||||
console.log(memory.buffer.byteLength);
|
||||
const api = globalThis.WebAssembly;
|
||||
console.log(api);
|
||||
console.log(WebAssembly.Module.imports(module).length);
|
||||
}
|
||||
|
||||
// pi's codemode spelling: the global object viewed through a cast.
|
||||
interface WebAssemblyGlobal {
|
||||
compile(bytes: Uint8Array): Promise<unknown>;
|
||||
}
|
||||
const { WebAssembly: viewed } = globalThis as unknown as { WebAssembly: WebAssemblyGlobal };
|
||||
console.log(typeof viewed.compile);
|
||||
|
||||
await load({}, new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]));
|
||||
@@ -1,35 +1,17 @@
|
||||
collection-iterator-helpers.ts:5:1 - error SC2020: 'collection iterator .map()' is part of the standard library types but has no scriptc lowering yet
|
||||
collection-iterator-helpers.ts:5:22 - error SC2020: '.flatMap over a callback returning 'Set<number>'' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
4 | const values = map.values();
|
||||
5 | values.map(value => value + 1);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
6 | map.keys().toArray();
|
||||
4 | // flatMap callbacks return arrays or generators.
|
||||
5 | set.values().flatMap((value) => new Set([value, value + 1]));
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
6 | // return() on a helper object.
|
||||
|
||||
hint: stored Map/Set iterators support .next() and iteration; iterator helper methods have no lowering
|
||||
hint: callbacks returning an array or a generator are the supported forms
|
||||
|
||||
collection-iterator-helpers.ts:6:1 - error SC2020: 'collection iterator .toArray()' is part of the standard library types but has no scriptc lowering yet
|
||||
collection-iterator-helpers.ts:8:1 - error SC2020: 'IteratorObject<number, undefined, unknown>.return' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
5 | values.map(value => value + 1);
|
||||
6 | map.keys().toArray();
|
||||
| ^~~~~~~~~~~~~~~~~~~~
|
||||
7 | const set = new Set<number>();
|
||||
7 | const mapped = set.values().map((value) => value * 2);
|
||||
8 | mapped.return!();
|
||||
| ^~~~~~~~~~~~~
|
||||
9 | // Each helper call must produce a diagnostic.
|
||||
|
||||
hint: stored Map/Set iterators support .next() and iteration; iterator helper methods have no lowering
|
||||
|
||||
collection-iterator-helpers.ts:8:1 - error SC2020: 'collection iterator .filter()' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
7 | const set = new Set<number>();
|
||||
8 | set.values().filter(value => value > 0);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
9 | set.entries().take(1);
|
||||
|
||||
hint: stored Map/Set iterators support .next() and iteration; iterator helper methods have no lowering
|
||||
|
||||
collection-iterator-helpers.ts:9:1 - error SC2020: 'collection iterator .take()' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
8 | set.values().filter(value => value > 0);
|
||||
9 | set.entries().take(1);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~
|
||||
10 | // Each helper call must produce a diagnostic.
|
||||
|
||||
hint: stored Map/Set iterators support .next() and iteration; iterator helper methods have no lowering
|
||||
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
|
||||
@@ -0,0 +1,31 @@
|
||||
scriptc coverage tests/coverage-fixtures/wasm/main.ts
|
||||
|
||||
statements analyzed 8
|
||||
compile statically 6 (75%)
|
||||
in dynamic tier 2 (operate on values typed only at runtime; --explain-dynamic lists them)
|
||||
|
||||
npm package compilation:
|
||||
wasm-fixture-pkg static
|
||||
|
||||
WebAssembly modules: (found at build time; none compile to native code yet)
|
||||
inline module 39 bytes valid
|
||||
1 function, 0 imports, 1 export
|
||||
found: inline at tests/coverage-fixtures/wasm/glue.js:5
|
||||
tests/coverage-fixtures/wasm/add.wasm 41 bytes valid
|
||||
1 function, 0 imports, 1 export
|
||||
found: path at tests/coverage-fixtures/wasm/glue.js:8
|
||||
tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/lib/core.wasm 27 bytes invalid: type mismatch (at byte 27)
|
||||
1 function, 0 imports, 0 exports
|
||||
found: shipped by the package at tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg
|
||||
tests/coverage-fixtures/wasm/build/generated.wasm 43 bytes uses simd
|
||||
1 function, 0 imports, 0 exports
|
||||
features: simd
|
||||
found: configured by tests/coverage-fixtures/wasm/package.json
|
||||
|
||||
in unreached code (never lowered — cannot fail a build)
|
||||
×1 SC2004 uses of 'instance' inherit the blocker on its declaration
|
||||
at tests/coverage-fixtures/wasm/glue.js:17:10
|
||||
hint: the declaration of 'instance' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
×1 SC2020 'WebAssembly.instantiate' is not supported yet: scriptc does not compile WebAssembly modules to native code yet
|
||||
at tests/coverage-fixtures/wasm/glue.js:16:30
|
||||
hint: WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)
|
||||
@@ -43,15 +43,6 @@ es2025-fences.ts:22:16 - error SC2020: 'Atomics.waitAsync' is part of the standa
|
||||
|
||||
hint: use integer load, store, exchange, compareExchange, add, sub, and, or, xor, wait or notify
|
||||
|
||||
es2025-fences.ts:25:7 - error SC2020: 'IteratorObject<number, undefined, unknown>' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
24 | // First-class iterator objects.
|
||||
25 | const it = Iterator.from([1, 2, 3].values());
|
||||
| ^~
|
||||
26 |
|
||||
|
||||
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
|
||||
|
||||
es2025-fences.ts:25:12 - error SC2020: 'Iterator.from' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
24 | // First-class iterator objects.
|
||||
@@ -59,7 +50,7 @@ es2025-fences.ts:25:12 - error SC2020: 'Iterator.from' is part of the standard l
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
26 |
|
||||
|
||||
hint: first-class iterator objects have no lowering — iterator helpers compile as one chain on an array iterator, consumed in place: arr.values().map(f).take(n).toArray()
|
||||
hint: iterator helpers apply directly to generators and to array, Map and Set iterators: arr.values().map(f).take(n).toArray()
|
||||
|
||||
es2025-fences.ts:28:27 - error SC2020: 'Promise.try over a callback of type '(a: number, b?: number | undefined) => number' with 1 argument' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user