Merge main

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-20 09:06:59 -07:00
co-authored by Claude Opus 5.5
220 changed files with 12788 additions and 557 deletions
+1 -1
View File
@@ -134,7 +134,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
<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 &lt;file.wasm&gt;</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>
<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. Builds translate the modules they find to native code; a build reads the <code>scriptc.wasm</code> array but not this flag. 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>
+2
View File
@@ -59,6 +59,8 @@ The declarations check your program; the package's JavaScript compiles with the
A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself.
`graceful-fs` is the exception: it compiles as `node:fs` instead of from its JavaScript, with one [documented difference](/docs/limitations#graceful-fs).
<span id="value-conversion" />
<span id="static-package-compilation-experimental" />
<span id="type-declarations-and-boundary-restrictions" />
+10
View File
@@ -102,6 +102,8 @@ title: "Limitations"
<include>../limitations/builtin-apis/process-memory.mdx</include>
<include>../limitations/builtin-apis/v8.mdx</include>
<include>../limitations/builtin-apis/performance-entries.mdx</include>
<include>../limitations/builtin-apis/builtin-module-lookup.mdx</include>
@@ -212,6 +214,10 @@ title: "Limitations"
<include>../limitations/console-and-process-streams.mdx</include>
### graceful-fs
<include>../limitations/graceful-fs.mdx</include>
### DNS lookup errors
<include>../limitations/dns-lookup-errors.mdx</include>
@@ -224,6 +230,10 @@ title: "Limitations"
<include>../limitations/udp-socket-options.mdx</include>
### Unix domain sockets and request options
<include>../limitations/unix-domain-sockets.mdx</include>
### Process arguments and errors
<include>../limitations/process-arguments-and-errors.mdx</include>
@@ -1,2 +1,4 @@
- **Globals:** Stored `globalThis` references support identity and probes for absent capabilities.
Most builtin members require direct global access; access through a stored global object can raise `SC2020`.
`Temporal` is not defined, as in Node.js 24: `typeof Temporal` is `"undefined"`, `globalThis.Temporal` is `undefined`, and any other use throws `ReferenceError: Temporal is not defined`.
Programs that name it type-check against TypeScript's `Temporal` declarations.
@@ -26,3 +26,4 @@
The `readline/promises` `Readline` class writes to the same outputs.
`readline.emitKeypressEvents` emits `'keypress'` events on `process.stdin` or a readable stream, shared with terminal interfaces on that stream as in Node; a listener's first parameter must admit `undefined`, which it receives for escape sequences.
Chained listener registration is unsupported.
`node:stream/web` exports the same `ReadableStream` class as the global; its other classes are unsupported.
@@ -0,0 +1,5 @@
- **V8 heap statistics:** `node:v8` can be imported.
`v8.getHeapStatistics()` returns Node's fields in Node's order, computed from the runtime allocator like `process.memoryUsage()`: `total_heap_size` and `total_physical_size` are the bytes the heap holds, `used_heap_size` the bytes allocated, `heap_size_limit` the machine's memory (the heap has no fixed ceiling), and `total_available_size` the limit minus the used bytes.
Fields that describe V8 itself (`total_heap_size_executable`, `malloced_memory`, `peak_malloced_memory`, the global handle sizes, `external_memory`, and `does_zap_garbage`) are `0`, and `number_of_native_contexts` is `1`.
The values are approximate and are not V8 heap figures.
The other `node:v8` members observe the V8 engine, such as heap snapshots, profiles, flags, and serialization, and throw `SC2020` where they are used.
@@ -1,3 +1,4 @@
After a failed `dns.lookup` or `dns.lookupService`, Node calls the callback with the error alone, so the address and family parameters are `undefined`.
A callback whose address parameter is typed `string` (as Node's own type declarations type it) receives `""` and `NaN` there, or an empty array for `all: true`, because a typed string or number cannot hold `undefined`.
An untyped callback receives the error alone, as in Node.
The promise forms in `node:dns/promises` (also reached as `dns.promises`) reject with the error and are unaffected.
+4
View File
@@ -0,0 +1,4 @@
The `graceful-fs` package compiles as `node:fs`.
Its imports and `require("graceful-fs")` calls resolve to the builtin module, so the package's JavaScript is not compiled, and `gracefulify(fs)` as a statement does nothing.
Programs behave as with the package except under file-descriptor exhaustion: graceful-fs queues operations that fail with `EMFILE` or `ENFILE` and retries them when a descriptor closes, while a compiled program reports the error to its caller, as `fs` does.
In a compiled program, `require("graceful-fs") === require("fs")` is `true`.
@@ -1,4 +1,7 @@
`eval`, `new Function`, and other ways of running JavaScript source at runtime are unsupported, in application code and in packages.
Rewrite code that depends on them, or replace the package.
To compute a value from code at build time instead, use `comptime`.
A compiled program embeds no JavaScript engine, so it cannot run JavaScript source at runtime.
`eval`, indirect `eval`, `new Function`, and `Function` behave as they do under `node --disallow-code-generation-from-strings`, a deliberate difference from default Node.js: every call throws a catchable `EvalError` with the message `Code generation from strings disallowed for this context`.
The arguments are evaluated first, and the Function constructor converts them to strings before it throws, as Node.js does.
Packages that test for code generation before using it, such as zod 4, take their interpreted path.
Code that needs to evaluate source at runtime must be rewritten; to compute a value from code at build time, use `comptime`.
The `vm` module is refused for the same reason.
See [npm Dependencies](/docs/dependencies#which-packages-compile) for which packages compile.
@@ -0,0 +1,2 @@
`server.listen(path)` and the `socketPath` option of `http.request()` and `https.request()` use Unix domain sockets on Linux and macOS. On Windows, where Node maps these paths to named pipes, they fail with `ENOTSUP`. `server.address()` on a server listening on a Unix socket returns the socket path, as in Node, when the result is typed `string | AddressInfo | null` (the @types/node declaration). Where it is typed as the `{ address, family, port }` record alone, which cannot hold the path, the call throws an error for such a server. `listen(path, backlog, callback)` is refused at compile time; pass `listen(path, callback)`.
When the options of `http.request()` come from a variable, or from an object literal that spreads one, they are read at run time with Node's validation errors. The `auth`, `createConnection`, `family`, `hints`, `insecureHTTPParser`, `joinDuplicateHeaders`, `localAddress`, `localPort`, `lookup` and `uniqueHeaders` options then throw a runtime error when they hold a value, and so do `setHost: false` and `setDefaultHeaders: false`.
@@ -1,7 +1,30 @@
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 translates WebAssembly modules to native code when it builds the program; there is no interpreter or JIT in the executable.
A build translates every module whose bytes it can see: files at paths the compiler resolves (`require.resolve`, `createRequire(...).resolve`, `import.meta.resolve`, `new URL("./x.wasm", import.meta.url)`, `path.join(__dirname, ...)`), modules inlined as base64 or byte-array literals, `.wasm` files shipped by packages whose code uses WebAssembly, and files listed in a `scriptc.wasm` array in `package.json`.
At run time `WebAssembly.compile`, `instantiate`, `validate` and `new WebAssembly.Module` find the translated module by the SHA-256 of the bytes they are given.
Bytes the build did not see, including bytes produced at run time, reject with a `CompileError` that says the module was not translated, and `WebAssembly.validate` answers `false` for them.
`scriptc coverage` lists the modules a build finds, with their size, whether they validate, and the WebAssembly proposals they use.
`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`.
Translated modules can use the WebAssembly 2.0 feature set: the MVP plus sign-extension operators, non-trapping float-to-int conversions, bulk memory, multi-value, mutable globals, reference types, and extended constant expressions.
Modules that use SIMD, threads, exception handling, tail calls, typed function references or GC, memory64, or multiple memories are not translated, so loading them rejects with the `CompileError` above; packages that fall back to a scalar build when that happens, such as undici and es-module-lexer, take their fallback.
Supported: `WebAssembly.compile`, `instantiate` (with bytes or a `Module`), `validate`, `new WebAssembly.Module`, `new WebAssembly.Instance`, and `instanceof` for every class; the error classes `CompileError`, `LinkError` and `RuntimeError`, constructed with or without `new`; `globalThis.WebAssembly` and its `compile`, `instantiate` and `validate` functions as values.
Instances expose exported functions, memories (`buffer`, `grow`), tables (`get`, `set`, `grow`, `length`) and globals (`value`).
Imports can be functions, numbers or BigInts for immutable globals, and memories, tables and globals exported by other instances.
Traps throw `WebAssembly.RuntimeError` with Node.js's messages, unbounded recursion throws `RangeError`, and an exception thrown by an imported function propagates through the WebAssembly frames unchanged.
A `WebAssembly.Module` can be sent to a worker through `workerData` or `postMessage`.
Not supported yet: the `Memory`, `Table`, `Global`, `Tag` and `Exception` constructors, `WebAssembly.Module.imports`, `exports` and `customSections`, `compileStreaming` and `instantiateStreaming`, and the classes used as values other than with `new` and `instanceof`.
TypeScript sources fail to build with SC2020 at each such use, and JavaScript sources throw that diagnostic when the statement runs.
Programs built for `wasm32-wasi` can't use the WebAssembly API.
Differences from Node.js:
- `Memory.prototype.grow` does not detach the previous `ArrayBuffer`.
The memory never moves, so views created before the growth stay valid but keep their old length.
- Instances are never freed, together with their memories, imports and exports.
- A function reference (`funcref`) can reach JavaScript only as a function the module exports; other function references throw a `TypeError`.
- A load whose result the module never uses may be removed instead of trapping when its address is out of bounds.
Loads whose result is dropped explicitly still trap.
- NaN payloads may differ, for example when a signaling NaN passes through arithmetic or a JavaScript number.
- `WebAssembly.compile` and `instantiate` with bytes settle after the current microtasks, as in Node.js, but can settle after `setImmediate` callbacks that Node.js would run later.
- The executable reads `.wasm` files at run time from the paths they had when it was built.
+45 -45
View File
@@ -239,9 +239,9 @@
"apiEntries": 51,
"static": {
"supported": 0,
"partial": 0,
"partial": 10,
"refused": 0,
"not-implemented": 51,
"not-implemented": 41,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -329,8 +329,8 @@
"apiEntries": 92,
"static": {
"supported": 1,
"partial": 15,
"refused": 1,
"partial": 16,
"refused": 0,
"not-implemented": 75,
"by-design": 0,
"unreviewed": 0,
@@ -809,10 +809,10 @@
"apiEntries": 68,
"static": {
"supported": 0,
"partial": 3,
"partial": 4,
"refused": 0,
"not-implemented": 0,
"by-design": 65,
"by-design": 64,
"unreviewed": 0,
"not-applicable": 0
}
@@ -869,9 +869,9 @@
"apiEntries": 89,
"static": {
"supported": 0,
"partial": 7,
"partial": 8,
"refused": 1,
"not-implemented": 81,
"not-implemented": 80,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -16626,7 +16626,7 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"status": "partial",
"detail": "Derived from the exact API rows in this chapter.",
"verification": "derived"
}
@@ -16718,9 +16718,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -16741,9 +16741,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -17132,9 +17132,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -17201,9 +17201,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -17316,9 +17316,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -17339,9 +17339,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -17362,9 +17362,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -17730,9 +17730,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -17753,9 +17753,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -36590,9 +36590,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -75973,9 +75973,9 @@
"anchor": "v8getheapstatistics",
"nodeStability": null,
"static": {
"status": "by-design",
"detail": "Intentionally outside the static native execution model.",
"verification": "architectural-policy"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -81011,9 +81011,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented static async-iteration subset.",
"verification": "test-backed"
}
},
{
@@ -3,20 +3,20 @@
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"summary": {
"taskCount": 3453,
"tierItemCount": 3453,
"taskCount": 3454,
"tierItemCount": 3454,
"tiers": {
"static": {
"audit-partial": 1179,
"verify-gap": 2146,
"replace-refusal": 41,
"audit-partial": 1191,
"verify-gap": 2136,
"replace-refusal": 40,
"classify": 2,
"implement": 85
}
},
"priorities": {
"high": 2287,
"normal": 694,
"normal": 695,
"low": 472
}
},
@@ -12300,12 +12300,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dnsPromises.lookup",
"tests": [
"tests/corpus/dns-promises-lookup.ts",
"tests/corpus/dns-promises-untyped.cjs"
]
}
}
},
@@ -12325,12 +12328,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dnsPromises.lookupService",
"tests": [
"tests/corpus/dns-promises-lookup.ts",
"tests/corpus/dns-promises-untyped.cjs"
]
}
}
},
@@ -12750,12 +12756,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dnsPromises.getDefaultResultOrder",
"tests": [
"tests/corpus/dns-promises-lookup.ts",
"tests/corpus/dns-promises-untyped.cjs"
]
}
}
},
@@ -12825,12 +12834,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dns.lookup",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
}
}
},
@@ -12950,12 +12963,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dns.getServers",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
}
}
},
@@ -12975,12 +12992,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dns.lookup",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
}
}
},
@@ -13000,12 +13021,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dns.lookupService",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
}
}
},
@@ -13400,12 +13425,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dns.setDefaultResultOrder",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
}
}
},
@@ -13425,12 +13454,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dns.getDefaultResultOrder",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
}
}
},
@@ -34202,12 +34235,21 @@
"priority": "high",
"tiers": {
"static": {
"status": "refused",
"action": "replace-refusal",
"verification": "explicit-refusal",
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:unsupported:stdlib.webassembly",
"tests": []
"source": "surface-manifest:stdlib.webassembly",
"tests": [
"tests/corpus/webassembly-api.mjs",
"tests/corpus/webassembly-typed.ts",
"tests/corpus/webassembly-worker.ts",
"tests/corpus/webassembly-spec-int/main.mjs",
"tests/corpus/webassembly-spec-float/main.mjs",
"tests/corpus/webassembly-spec-control/main.mjs",
"tests/corpus/webassembly-spec-memory/main.mjs",
"tests/corpus/webassembly-spec-table/main.mjs"
]
}
}
},
@@ -77813,6 +77855,29 @@
}
}
},
{
"id": "v8:cefde4a8297c",
"chapter": "v8",
"label": "v8.getHeapStatistics()",
"signature": "`v8.getHeapStatistics()`",
"apiSymbol": "v8.getHeapStatistics",
"docsUrl": "https://nodejs.org/docs/v24.22.0/api/v8.html#v8getheapstatistics",
"nodeStability": null,
"priority": "normal",
"tiers": {
"static": {
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.v8.getHeapStatistics",
"tests": [
"tests/corpus/v8-heap-statistics.ts",
"tests/corpus/v8-require-optional.cjs"
]
}
}
},
{
"id": "webcrypto:22408f834c48",
"chapter": "webcrypto",
@@ -81965,12 +82030,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webstreams",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webstreams.ReadableStream",
"tests": [
"tests/corpus/stream-web-readable.ts",
"tests/corpus/stream-web-require.cjs",
"tests/corpus/2862-web-readable-async-iteration.ts"
]
}
}
},
@@ -251,9 +251,9 @@
"apiEntries": 51,
"static": {
"supported": 0,
"partial": 0,
"partial": 10,
"refused": 0,
"not-implemented": 51,
"not-implemented": 41,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -341,8 +341,8 @@
"apiEntries": 92,
"static": {
"supported": 1,
"partial": 15,
"refused": 1,
"partial": 16,
"refused": 0,
"not-implemented": 75,
"by-design": 0,
"unreviewed": 0,
@@ -866,10 +866,10 @@
"apiEntries": 68,
"static": {
"supported": 0,
"partial": 3,
"partial": 4,
"refused": 0,
"not-implemented": 0,
"by-design": 65,
"by-design": 64,
"unreviewed": 0,
"not-applicable": 0
}
@@ -926,9 +926,9 @@
"apiEntries": 89,
"static": {
"supported": 0,
"partial": 7,
"partial": 8,
"refused": 1,
"not-implemented": 81,
"not-implemented": 80,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -30720,8 +30720,15 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"evidence": "derived:descendants"
"status": "partial",
"evidence": "derived:descendants",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs",
"tests/corpus/dns-promises-lookup.ts",
"tests/corpus/dns-promises-untyped.cjs"
]
},
"anchorSource": "chapter"
},
@@ -30835,8 +30842,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dnsPromises.lookup",
"tests": [
"tests/corpus/dns-promises-lookup.ts",
"tests/corpus/dns-promises-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -30858,8 +30869,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dnsPromises.lookupService",
"tests": [
"tests/corpus/dns-promises-lookup.ts",
"tests/corpus/dns-promises-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -31249,8 +31264,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dnsPromises.getDefaultResultOrder",
"tests": [
"tests/corpus/dns-promises-lookup.ts",
"tests/corpus/dns-promises-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -31364,8 +31383,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dns.lookup",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -31479,8 +31503,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dns.getServers",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -31502,8 +31531,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dns.lookup",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -31548,8 +31582,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dns.lookupService",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -31916,8 +31955,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dns.setDefaultResultOrder",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -31939,8 +31983,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dns.getDefaultResultOrder",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -52249,6 +52298,14 @@
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/corpus/6331-jose-root-entry/main.ts",
"tests/corpus/text-codec-values/main.ts",
"tests/corpus/webassembly-api.mjs",
"tests/corpus/webassembly-spec-control/main.mjs",
"tests/corpus/webassembly-spec-float/main.mjs",
"tests/corpus/webassembly-spec-int/main.mjs",
"tests/corpus/webassembly-spec-memory/main.mjs",
"tests/corpus/webassembly-spec-table/main.mjs",
"tests/corpus/webassembly-typed.ts",
"tests/corpus/webassembly-worker.ts",
"tests/fixtures/node-types/navigator.ts",
"tests/harness/fetch-conformance.test.ts",
"tests/harness/project-config.test.ts"
@@ -53600,8 +53657,18 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:stdlib.webassembly"
"status": "partial",
"evidence": "surface-manifest:stdlib.webassembly",
"tests": [
"tests/corpus/webassembly-api.mjs",
"tests/corpus/webassembly-typed.ts",
"tests/corpus/webassembly-worker.ts",
"tests/corpus/webassembly-spec-int/main.mjs",
"tests/corpus/webassembly-spec-float/main.mjs",
"tests/corpus/webassembly-spec-control/main.mjs",
"tests/corpus/webassembly-spec-memory/main.mjs",
"tests/corpus/webassembly-spec-table/main.mjs"
]
},
"anchorSource": "exact"
},
@@ -104477,6 +104544,8 @@
"status": "partial",
"evidence": "derived:descendants",
"tests": [
"tests/corpus/v8-heap-statistics.ts",
"tests/corpus/v8-require-optional.cjs",
"tests/harness/node-test.test.ts"
]
},
@@ -105599,8 +105668,12 @@
"anchor": "v8getheapstatistics",
"nodeStability": null,
"static": {
"status": "by-design",
"evidence": "compiler-chapter-policy:v8"
"status": "partial",
"evidence": "surface-manifest:node-builtin.v8.getHeapStatistics",
"tests": [
"tests/corpus/v8-heap-statistics.ts",
"tests/corpus/v8-require-optional.cjs"
]
},
"anchorSource": "exact"
},
@@ -111212,6 +111285,8 @@
"tests/corpus/2630-stream-consumers-errors.ts",
"tests/corpus/2862-web-readable-async-iteration.ts",
"tests/corpus/4875-stream-consumers-array-buffer.ts",
"tests/corpus/stream-web-readable.ts",
"tests/corpus/stream-web-require.cjs",
"tests/harness/fetch-conformance.test.ts"
]
},
@@ -111480,8 +111555,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webstreams"
"status": "partial",
"evidence": "compiler-feature:webstreams.ReadableStream",
"tests": [
"tests/corpus/stream-web-readable.ts",
"tests/corpus/stream-web-require.cjs",
"tests/corpus/2862-web-readable-async-iteration.ts"
]
},
"anchorSource": "exact"
},
+9 -1
View File
@@ -28,6 +28,7 @@
"webstreams": "unreviewed"
},
"evidenceByManifestPrefix": {
"stdlib.webassembly": ["tests/corpus/webassembly-api.mjs", "tests/corpus/webassembly-typed.ts", "tests/corpus/webassembly-worker.ts", "tests/corpus/webassembly-spec-int/main.mjs", "tests/corpus/webassembly-spec-float/main.mjs", "tests/corpus/webassembly-spec-control/main.mjs", "tests/corpus/webassembly-spec-memory/main.mjs", "tests/corpus/webassembly-spec-table/main.mjs"],
"node-builtin.fs.readFileSync": ["tests/corpus/1403-buffer-fs.ts", "tests/corpus/2595-fs-arg-ladders.cjs", "tests/corpus/2919-fs-function-values.ts", "tests/corpus/2920-fs-promises-function-values.ts", "tests/corpus/fs-read-encodings.ts"],
"syntax.using-declarations": ["tests/corpus/2841-using-sync.ts"],
"syntax.await-using-declarations": ["tests/corpus/2842-await-using.ts"],
@@ -47,6 +48,10 @@
"node-builtin.dgram": ["tests/corpus/2597-dgram-send-ladders.cjs"],
"node-builtin.diagnostics_channel": ["tests/corpus/2214-dc-bind-store.cjs"],
"node-builtin.dns": ["tests/corpus/2597-dgram-send-ladders.cjs"],
"node-builtin.dns.promises": ["tests/corpus/dns-promises-lookup.ts", "tests/corpus/dns-promises-untyped.cjs"],
"node-builtin.stream.web": ["tests/corpus/stream-web-readable.ts", "tests/corpus/stream-web-require.cjs"],
"node-builtin.v8": ["tests/corpus/v8-require-optional.cjs"],
"node-builtin.v8.getHeapStatistics": ["tests/corpus/v8-heap-statistics.ts", "tests/corpus/v8-require-optional.cjs"],
"node-builtin.events": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"],
"node-builtin.fs": ["tests/corpus/1403-buffer-fs.ts", "tests/corpus/2595-fs-arg-ladders.cjs", "tests/corpus/2919-fs-function-values.ts", "tests/corpus/2920-fs-promises-function-values.ts"],
"node-builtin.fs.realpathSync.native": ["tests/corpus/fs-native-realpath.ts"],
@@ -381,7 +386,10 @@
{ "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] },
{ "chapter": "zlib", "signatures": ["Class: `zlib.Deflate`", "Class: `zlib.Inflate`", "Class: `zlib.Gzip`", "Class: `zlib.Gunzip`", "Class: `zlib.DeflateRaw`", "Class: `zlib.InflateRaw`", "Class: `zlib.Unzip`", "Class: `zlib.ZlibBase`", "`zlib.close([callback])`", "`zlib.flush([kind, ]callback)`", "`zlib.params(level, strategy, callback)`", "`zlib.reset()`", "`bytesWritten` Type: {number}", "Class: `Options`"], "status": "partial", "evidence": ["tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5372-zlib-stream-errors.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts", "tests/corpus/5375-zlib-stream-promises.ts", "tests/corpus/5377-zlib-stream-backpressure.cjs"] },
{ "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] }
{ "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] },
{ "chapter": "dns", "symbols": ["dns.lookup", "dns.lookupService", "dns.getServers", "dns.getDefaultResultOrder", "dns.setDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/5648-dns-lookup-forms.ts", "tests/corpus/5649-dns-service-servers.ts", "tests/corpus/5650-dns-lookup-errors-untyped.cjs"] },
{ "chapter": "dns", "symbols": ["dnsPromises.lookup", "dnsPromises.lookupService", "dnsPromises.getDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/dns-promises-lookup.ts", "tests/corpus/dns-promises-untyped.cjs"] },
{ "chapter": "webstreams", "symbols": ["ReadableStream"], "status": "partial", "evidence": ["tests/corpus/stream-web-readable.ts", "tests/corpus/stream-web-require.cjs", "tests/corpus/2862-web-readable-async-iteration.ts"] }
],
"dedicated": [
{ "symbols": ["assert.doesNotThrow", "assert.doesNotReject"], "status": "partial", "evidence": ["tests/corpus/node24-no-error-assertions.ts", "tests/corpus/node24-no-error-assertions.cjs", "tests/corpus/node24-no-error-assertions-coercion.js", "tests/fixtures/node-types/no-error-assertions.ts", "tests/harness/project-config.test.ts"] },
+117 -4
View File
@@ -1878,6 +1878,13 @@ declare module "node:fs" {
declare module "fs" {
export * from "node:fs";
}
/* graceful-fs — served as node:fs (it monkeypatches fs and retries on
* EMFILE/ENFILE; compiled programs call fs directly, without the retry
* queue). gracefulify is a no-op statement. The @types/graceful-fs shape. */
declare module "graceful-fs" {
export * from "node:fs";
export function gracefulify<T>(fsModule: T): T;
}
/* fs/promises — the SAME synchronous operations behind already-settled
* promises: success fulfills, failure REJECTS with the would-be thrown
@@ -4687,6 +4694,11 @@ declare module "net" {
on(event: "close", listener: (hadError: boolean) => void): void;
on(event: "error", listener: (err: Error) => void): void;
on(event: "session", listener: (session: Buffer) => void): void;
/* The host name's resolution, before the connect. */
on(
event: "lookup",
listener: (err: Error | null, address: string, family: number, host: string) => void,
): void;
/* addListener IS on (Node aliases them) — the suite spells both. */
addListener(event: "data", listener: (chunk: Buffer) => void): void;
/* After setEncoding the chunk is a string. */
@@ -4698,6 +4710,11 @@ declare module "net" {
addListener(event: "close", listener: (hadError: boolean) => void): void;
addListener(event: "error", listener: (err: Error) => void): void;
addListener(event: "session", listener: (session: Buffer) => void): void;
/* The host name's resolution, before the connect. */
addListener(
event: "lookup",
listener: (err: Error | null, address: string, family: number, host: string) => void,
): void;
once(event: "data", listener: (chunk: Buffer) => void): void;
/* After setEncoding the chunk is a string. */
once(event: "data", listener: (chunk: string) => void): void;
@@ -4708,6 +4725,11 @@ declare module "net" {
once(event: "close", listener: (hadError: boolean) => void): void;
once(event: "error", listener: (err: Error) => void): void;
once(event: "session", listener: (session: Buffer) => void): void;
/* The host name's resolution, before the connect. */
once(
event: "lookup",
listener: (err: Error | null, address: string, family: number, host: string) => void,
): void;
}
export interface Server {
/* Whether the server keeps the event loop alive (ref'd by default). */
@@ -4726,6 +4748,8 @@ declare module "net" {
/* The positional bind address (the options form's host in argument
* spelling). */
listen(port: number, host: string, callback?: () => void): Server;
/* A Unix-socket server; the socket file is removed when it closes. */
listen(path: string, callback?: () => void): Server;
/* The explicit-interface bind: host is an IP literal (absent = the
* host-less dual-stack any); ipv6Only sets IPV6_V6ONLY and reusePort
* requests kernel connection distribution where Node supports it. */
@@ -5051,10 +5075,11 @@ declare module "http" {
* callback receives the response as an IncomingMessage (statusCode,
* headers, body via on("data"/"end")). */
export interface RequestOptions {
hostname?: string;
host?: string;
port?: number;
path?: string;
hostname?: string | null;
host?: string | null;
/* A string port (a URL's .port) is accepted, as by Node. */
port?: number | string | null;
path?: string | null;
method?: string;
timeout?: number;
headers?: OutgoingHttpHeaders;
@@ -5068,6 +5093,8 @@ declare module "http" {
agent?: Agent | boolean | null;
/* Aborting destroys the request with an AbortError. */
signal?: AbortSignal;
/* HTTP over the Unix socket at this path instead of host:port. */
socketPath?: string;
/* The options-record stance: Node ignores an options record's
* unknown keys, so every key typechecks here — the option WALK in
* the lowering decides per key (lower, fence by name, or drop
@@ -5343,6 +5370,7 @@ declare module "https" {
agent?: Agent | boolean | null;
/* Aborting destroys the request with an AbortError. */
signal?: AbortSignal;
socketPath?: string;
/* The options-record stance (see http.RequestOptions). */
[option: string]: unknown;
}
@@ -6160,10 +6188,79 @@ declare module "dns" {
export function getServers(): string[];
export function getDefaultResultOrder(): "ipv4first" | "ipv6first" | "verbatim";
export function setDefaultResultOrder(order: "ipv4first" | "ipv6first" | "verbatim"): void;
/* The promise forms (node:dns/promises). */
export const promises: typeof import("dns/promises");
}
declare module "node:dns" {
export * from "dns";
}
/* node:dns/promises — lookup and lookupService as promises over the same
* resolution as the callback forms: argument errors throw synchronously,
* failures reject with the callbacks' errors. */
declare module "dns/promises" {
import type { LookupAddress, LookupAllOptions, LookupOneOptions, LookupOptions } from "dns";
export function lookup(hostname: string, family: number): Promise<LookupAddress>;
export function lookup(hostname: string, options: LookupOneOptions): Promise<LookupAddress>;
export function lookup(hostname: string, options: LookupAllOptions): Promise<LookupAddress[]>;
export function lookup(
hostname: string,
options: LookupOptions,
): Promise<LookupAddress | LookupAddress[]>;
export function lookup(hostname: string): Promise<LookupAddress>;
export function lookupService(
address: string,
port: number,
): Promise<{ hostname: string; service: string }>;
export function getServers(): string[];
export function getDefaultResultOrder(): "ipv4first" | "ipv6first" | "verbatim";
export function setDefaultResultOrder(order: "ipv4first" | "ipv6first" | "verbatim"): void;
}
declare module "node:dns/promises" {
export * from "dns/promises";
}
/* node:v8 — importable for the packages that require it at top level.
* getHeapStatistics answers allocator-derived figures in Node's shape
* (approximate: the runtime heap's total and used bytes, the machine's
* memory as the limit; V8-only figures are 0). The other members observe
* V8 itself and fence at their use with the by-design reason; they are
* declared so programs that type-check under their own tsc reach that
* fence. */
declare module "v8" {
type DoesZapCodeSpaceFlag = 0 | 1;
export interface HeapInfo {
total_heap_size: number;
total_heap_size_executable: number;
total_physical_size: number;
total_available_size: number;
used_heap_size: number;
heap_size_limit: number;
malloced_memory: number;
peak_malloced_memory: number;
does_zap_garbage: DoesZapCodeSpaceFlag;
number_of_native_contexts: number;
number_of_detached_contexts: number;
total_global_handles_size: number;
used_global_handles_size: number;
external_memory: number;
}
export function getHeapStatistics(): HeapInfo;
export function cachedDataVersionTag(): number;
export function getHeapSpaceStatistics(): unknown[];
export function getHeapCodeStatistics(): unknown;
export function getHeapSnapshot(options?: unknown): unknown;
export function writeHeapSnapshot(fileName?: string, options?: unknown): string;
export function setFlagsFromString(flags: string): void;
export function setHeapSnapshotNearHeapLimit(limit: number): void;
export function serialize(value: unknown): Buffer;
export function deserialize(buffer: Uint8Array): unknown;
export function takeCoverage(): void;
export function stopCoverage(): void;
export function isStringOneByteRepresentation(content: string): boolean;
}
declare module "node:v8" {
export * from "v8";
}
/* node:worker_threads — native workers use statically compiled entry points.
* Lowering and runtime diagnostics define the supported options and methods. */
@@ -6953,6 +7050,22 @@ declare module "node:stream/consumers" {
import streamConsumers = require("stream/consumers");
export = streamConsumers;
}
/* node:stream/web — the module exports the web stream globals themselves
* (require("stream/web").ReadableStream === globalThis.ReadableStream).
* ReadableStream is the implemented class; its reader and controller are
* exported as types. */
declare module "stream/web" {
export const ReadableStream: typeof globalThis.ReadableStream;
export type ReadableStream<T = unknown> = globalThis.ReadableStream<T>;
export type ReadableStreamDefaultReader<T = unknown> = globalThis.ReadableStreamDefaultReader<T>;
export type ReadableStreamDefaultController<T = unknown> =
globalThis.ReadableStreamDefaultController<T>;
export type UnderlyingSource<T = unknown> = globalThis.UnderlyingSource<T>;
export type QueuingStrategy<T = unknown> = globalThis.QueuingStrategy<T>;
}
declare module "node:stream/web" {
export * from "stream/web";
}
interface ErrorConstructor {
stackTraceLimit: number;
}
@@ -16,6 +16,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
fileHandle: features.fileHandle,
fetch: features.fetch,
...(features.eventTarget ? { eventTarget: true } : {}),
...(features.wasm ? { wasm: true } : {}),
...(features.warningReplace ? { warningReplace: true } : {}),
...(features.stdioExtended ? { stdioExtended: true } : {}),
zlib: features.zlib,
@@ -37,6 +38,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
http: features.http,
http2: features.http2,
dgram: features.dgram,
dnsPromises: features.dnsPromises,
watch: features.fsWatch,
foreignFfi: hasForeignFfiCallback(mod.ffiImports ?? []),
nodeTest: features.nodeTest,
@@ -48,6 +48,7 @@ export async function compilePackedLibrary(
http: false,
http2: false,
dgram: false,
dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
@@ -1,4 +1,5 @@
/* LlEmitter's module-level output: module parts, helper definitions, library declarations and FFI callback definitions. */
import { emitWasmModules } from "./wasm-modules.js";
import { type LlEmitter, ffiCallbackDummyLl, llStrBytes } from "./emitter.js";
import {
mangleAsyncSpawn,
@@ -177,6 +178,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
// programs additionally stamp the httpReq/httpRes ops — the C main's
// install lines, gated on the same predicates native/executable.ts links by.
const usesNet = runtimeFeatures.net;
const usesNetLookup = runtimeFeatures.netLookup;
const usesDgram = runtimeFeatures.dgram;
const usesHttp2 = runtimeFeatures.http2;
const usesHttp = runtimeFeatures.http;
@@ -226,6 +228,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
if (usesFsWatch) emitter.declare(`declare void @scr_watch_install()`);
if (emitter.ffiHasForeignCallback) emitter.declare(`declare void @scr_ffi_install()`);
if (usesStream) emitter.declare(`declare void @scr_stream_install()`);
if (usesNetLookup) emitter.declare(`declare void @scr_net_lookup_install()`);
if (usesNet) {
emitter.declare(`declare void @scr_net_install()`);
emitter.declare(`declare void @scr_net_dyn_install()`);
@@ -728,6 +731,9 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
out.push(fnDefs[0] ?? "");
for (let i = 1; i < fnDefs.length; i++) out.push("", fnDefs[i]!);
out.push("");
// WebAssembly modules translated into this program, and the registry.
for (const line of emitWasmModules(emitter.mod, flushedDecls, emitter.wasmBoundsChecks))
out.push(line);
// main(): scr_init, the program-dependent error-vt interval stamps,
// scr_lib_init(argc, argv), then the entry function. An uncaught
@@ -837,6 +843,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
...(snapshotsTlsCa ? [` call void @scr_tls_ca_install()`] : []),
...(usesFetch ? [` call void @scr_fetch_install()`] : []),
...(usesNet ? [` call void @scr_net_install()`, ` call void @scr_net_dyn_install()`] : []),
...(usesNetLookup ? [` call void @scr_net_lookup_install()`] : []),
...(usesHttp ? [` call void @scr_http_dyn_install()`] : []),
...(usesDgram ? [` call void @scr_dgram_install()`] : []),
...(usesHttp2 ? [` call void @scr_http2_dyn_install()`] : []),
@@ -1,3 +1,4 @@
import { wasmBoundsChecks } from "./wasm-modules.js";
import { jsTruthy } from "./jsval.js";
import { type ArraySearchKind } from "./array-search.js";
import { emitStringInputs } from "./string-lifetimes.js";
@@ -413,6 +414,9 @@ export interface LlvmTargetOptions {
* teardown after `process.exit` to a helper process (`--detached-exit`).
* Executables only; the runtime's weak default keeps it off. */
detachedExit?: boolean;
/** Translated WebAssembly memories check bounds explicitly instead of
* relying on guard pages (wasm-modules.ts wasmBoundsChecks). */
wasmBoundsChecks?: boolean;
}
export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string {
@@ -682,6 +686,7 @@ export class LlEmitter {
readonly emitLibraryIdentity: boolean;
readonly runtimeAbiMarker: boolean;
readonly detachedExit: boolean;
readonly wasmBoundsChecks: boolean;
/** Interned string literals: UTF-8 text → { symbol, byte length } —
* first-use order, the runtime ABI’s determinism discipline. */
readonly literals = new Map<string, { sym: string; len: number }>();
@@ -1175,6 +1180,13 @@ export class LlEmitter {
this.emitLibraryIdentity = options.emitLibraryIdentity !== false;
this.runtimeAbiMarker = options.runtimeAbiMarker === true;
this.detachedExit = options.detachedExit === true;
this.wasmBoundsChecks =
options.wasmBoundsChecks === true ||
wasmBoundsChecks({
sanitize: false,
pointerBits: options.pointerBits ?? 64,
targetTriple: options.targetTriple ?? "",
});
this.rcHelpers = options.inlineRc !== undefined ? new Set() : null;
this.rcMode = options.inlineRc;
this.rcHelpersNoinline = options.rcHelpersNoinline === true;
@@ -236,6 +236,10 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
return finishRuntimeCall(host, e, "scr_dns_lookup_dyn", a);
case "dns.lookupService":
return finishRuntimeCall(host, e, "scr_dns_lookup_service", a);
case "dns.promisesLookup":
return finishRuntimeCall(host, e, "scr_dns_promises_lookup", a);
case "dns.promisesLookupService":
return finishRuntimeCall(host, e, "scr_dns_promises_lookup_service", a);
case "dns.getServers":
return finishRuntimeCall(host, e, "scr_dns_get_servers", a);
case "dns.getDefaultResultOrder":
@@ -857,6 +857,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "event":
case "events":
case "blob":
case "wasm":
case "record":
case "disposable":
case "punycode":
@@ -60,7 +60,54 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
...handler,
]);
}
if (
e.fn === "http.requestDyn" ||
e.fn === "http.requestDynCb" ||
e.fn === "https.requestDyn" ||
e.fn === "https.requestDynCb"
) {
// [options, autoEnd(, cb)]: the runtime walks the options record and
// attaches its signal through events.addAbortListener (the event-target
// unit, linked for these calls by runtime-features).
const args = e.args.map((arg) => host.emitExpr(arg));
host.usesTimers = true;
const cb = args[2];
const handler =
cb === undefined
? [ptr(), ptr()]
: callbackAdapter(
host,
cb,
callbackType(cb).params.length === 0
? "scr_http_resp_thunk0"
: "scr_http_resp_thunk_res",
["ptr", "ptr"],
);
host.declare(`declare ptr @scr_events_add_abort_listener(ptr, ptr)`);
return finishRuntimeCall(
host,
e,
e.fn.startsWith("https.") ? "scr_https_request_dyn" : "scr_http_request_dyn",
[
abiValue(host, args[0]!),
abiValue(host, args[1]!),
...handler,
ptr("@scr_events_add_abort_listener"),
],
);
}
const B = host.B;
if (e.fn === "net.listenPath" || e.fn === "net.listenPathCb") {
const args = e.args.map((a) => host.emitExpr(a));
let cb = "null";
if (e.fn === "net.listenPathCb") {
host.toHeap(args[2]!);
cb = args[2]!.name;
}
host.declare(`declare void @scr_net_listen_path(ptr, ptr, ptr)`);
B.line(`call void @scr_net_listen_path(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${cb})`);
return { name: "", type: e.type };
}
if (e.fn === "net.connectOptsChk") {
return emitAlwaysThrowLibCall(host, e, "scr_net_connect_opts_chk");
}
@@ -860,6 +860,16 @@ export const LIB_FN_SYMS: Record<string, string> = {
"event.get": "scr_event_obj_get",
"event.set": "scr_event_obj_set",
"blob.new": "scr_blob_new",
"wasm.compile": "scr_wasm_compile",
"wasm.validate": "scr_wasm_validate",
"wasm.instantiate": "scr_wasm_instantiate",
"wasm.moduleNew": "scr_wasm_module_new",
"wasm.instanceNew": "scr_wasm_instance_new",
"wasm.is": "scr_wasm_is",
"wasm.errorNew": "scr_wasm_error_new",
"wasm.function": "scr_wasm_function",
"wasm.namespace": "scr_wasm_namespace",
"wasm.globalView": "scr_wasm_global_view",
"blob.fileNew": "scr_file_new",
"blob.slice": "scr_blob_slice",
"blob.text": "scr_blob_text",
@@ -1158,6 +1168,10 @@ export const LIB_FN_SYMS: Record<string, string> = {
"net.sockReadyState": "scr_net_sock_ready_state",
"net.sockSetKeepAlive": "scr_net_sock_set_keepalive_ret",
"net.sockOnCloseDyn": "scr_net_sock_on_close_dyn",
"net.sockOnLookup": "scr_net_sock_on_lookup",
"net.serverCloseDyn": "scr_net_server_close_dyn",
"net.serverUnixPath": "scr_net_server_unix_path",
"net.serverAddressCheck": "scr_net_server_address_check",
"net.sockOnWriteFlushDyn": "scr_net_sock_on_write_flush_dyn",
"net.sockOnFinishDyn": "scr_net_sock_on_finish_dyn",
"net.isIP": "scr_net_is_ip",
@@ -1633,6 +1647,8 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"sc.arrayBuffer",
"net.listen",
"net.listenCb",
"net.listenPath",
"net.listenPathCb",
"net.listenOpts",
"net.listenOptsCb",
"net.listenOptsReusePort",
@@ -1652,6 +1668,10 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"http.requestCb",
"http.requestUrl",
"http.requestUrlCb",
"http.requestDyn",
"http.requestDynCb",
"https.requestDyn",
"https.requestDynCb",
"https.request",
"https.requestCb",
"https.requestUrl",
@@ -0,0 +1,74 @@
/* The WebAssembly modules translated into a program (IrModule.wasmModules):
* each module's translation (wasm/translate.ts) and the registry the
* runtime looks modules up in (scr_wasm_registry, scr_wasm_abi.h). The
* translations join the program module, so the native helper optimizes,
* partitions and caches them with it. Programs that do not use the
* WebAssembly API carry no wasmModules and emit nothing here. */
import type { IrModule } from "../../ir/ir.js";
import { decodeBase64 } from "../../wasm/base64.js";
import { translateWasmModule, WasmTranslateError } from "../../wasm/translate.js";
function hexBytes(hex: string): Uint8Array {
const out = new Uint8Array(hex.length / 2);
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(2 * i, 2 * i + 2), 16);
return out;
}
/** Whether translated memories check bounds explicitly instead of relying
* on guard pages and the runtime's fault handler: sanitizer builds (whose
* runtime owns SIGSEGV), 32-bit targets, and Windows, which has no
* fault handler yet. */
export function wasmBoundsChecks(options: {
sanitize: boolean;
pointerBits: 32 | 64;
targetTriple: string;
}): boolean {
if (options.sanitize || options.pointerBits === 32) return true;
const target = options.targetTriple || process.platform;
return /windows|mingw|cygwin|win32|msvc/.test(target);
}
/** The top-level lines of every translated module plus the registry.
* `declared` holds the declarations the module already carries; runtime
* entry points it lacks are declared here. A module the translator
* cannot translate is left out of the registry, so loading it at run
* time rejects with the CompileError for untranslated bytes. */
export function emitWasmModules(
mod: IrModule,
declared: ReadonlySet<string>,
boundsChecks: boolean,
): string[] {
const modules = mod.wasmModules;
if (modules === undefined) return [];
const out: string[] = [];
const decls = new Set(declared);
const descriptors: string[] = [];
for (const module of modules) {
const bytes = decodeBase64(module.bytes);
if (bytes === null) continue;
let translation;
try {
translation = translateWasmModule(bytes, hexBytes(module.sha256), { boundsChecks });
} catch (error) {
if (error instanceof WasmTranslateError) continue;
throw error;
}
for (const decl of translation.decls) {
if (decls.has(decl)) continue;
decls.add(decl);
out.push(decl);
}
for (const part of translation.text) out.push(part);
descriptors.push(translation.descriptor);
}
if (descriptors.length === 0) {
out.push(`@scr_wasm_registry = constant { i64, ptr } { i64 0, ptr null }`);
} else {
out.push(
`@sc_wasm_registry_modules = private constant [${descriptors.length} x ptr] [${descriptors.map((d) => `ptr ${d}`).join(", ")}]`,
`@scr_wasm_registry = constant { i64, ptr } { i64 ${descriptors.length}, ptr @sc_wasm_registry_modules }`,
);
}
out.push("");
return out;
}
@@ -25,6 +25,9 @@ export interface NativeLinkFeatures {
fileHandle: boolean;
fetch: boolean;
eventTarget?: boolean;
/** The WebAssembly API (scr_wasm.c). Absent when unused, so programs
* without it keep their link identity. */
wasm?: boolean;
/** The program assigns process.emitWarning: links the
* SCR_WARNING_REPLACE variant of the dyn-async unit. */
warningReplace?: boolean;
@@ -52,6 +55,8 @@ export interface NativeLinkFeatures {
http: boolean;
http2: boolean;
dgram: boolean;
/** node:dns/promises: scr_dns_promises.c. */
dnsPromises: boolean;
watch: boolean;
foreignFfi: boolean;
nodeTest: boolean;
@@ -118,6 +118,8 @@ export interface CcOptions {
* native fetch AbortSignal built on the same engine: compiles
* scr_event_target.c. */
eventTarget?: boolean;
/** The WebAssembly API: compiles scr_wasm.c. */
wasm?: boolean;
/** SCR_WARNING_REPLACE: process.emitWarning replacement support. */
warningReplace?: boolean;
/** SCR_STDIO_EXTENDED: stdin encoding, string data listeners, resize. */
@@ -177,6 +179,11 @@ export interface CcOptions {
* on the IR): compiles scr_dgram.c into the binary — the net gating
* precedent, so dgram-free binaries keep their exact link line. */
dgram?: boolean;
/** The program uses node:dns/promises (the dns.promisesLookup libCalls):
* compiles scr_dns_promises.c over the dgram unit, so callback-only
* binaries keep their exact link line. */
dnsPromises?: boolean;
/** The program uses fs.watch (moduleUsesFsWatch on the IR): compiles
* scr_watch.c into the binary — the net gating precedent, so watch-free
* binaries keep their exact link line. */
@@ -182,6 +182,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
unit((u) => u.opts.dc, "scr_dc.c"),
unit((u) => u.opts.dynTier, "scr_js_value.c"),
unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"),
unit((u) => u.opts.wasm, "scr_wasm.c"),
unit(
(u) =>
u.opts.dynAsync ||
@@ -252,6 +253,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
unit((u) => u.http, "scr_http.c"),
unit((u) => u.opts.http2, "scr_http2.c"),
unit((u) => u.opts.dgram, "scr_dgram.c"),
unit((u) => u.opts.dgram && u.opts.dnsPromises, "scr_dns_promises.c"),
unit((u) => u.opts.watch, "scr_watch.c"),
unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"),
unit((u) => u.opts.nodeTest, "scr_test.c"),
@@ -82,6 +82,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
workers: boolean;
youngNursery: boolean;
eventTarget: boolean;
wasm: boolean;
dynTier: boolean;
warningReplace: boolean;
stdioExtended: boolean;
@@ -272,6 +273,7 @@ export function effectiveRuntimeFeatures(
workers: features.workers === true,
youngNursery: features.youngNursery === true,
eventTarget: features.eventTarget === true,
wasm: features.wasm === true,
dynTier: features.dynTier === true,
warningReplace: features.warningReplace === true,
stdioExtended: features.stdioExtended === true,
@@ -113,6 +113,7 @@ const features = {
http: false,
http2: false,
dgram: false,
dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
@@ -50,6 +50,7 @@ const BASE: NativeLinkFeatures = {
http: false,
http2: false,
dgram: false,
dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
@@ -192,6 +192,14 @@ export function moduleWasiUnavailableSurface(
found = { surface: "OS signals (WASI Preview 1 has no signal API)", loc };
return false;
}
if (node.fn.startsWith("wasm.")) {
found = {
surface:
"the WebAssembly API (a wasm32-wasi program cannot load translated modules into its own linear memory yet)",
loc,
};
return false;
}
if (node.fn === "crypto.cipherNew") {
found = {
surface:
@@ -56,6 +56,7 @@ export async function warmNativeCaches(
http: false,
http2: false,
dgram: false,
dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
+4 -4
View File
@@ -221,13 +221,13 @@ 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.
// The WebAssembly modules the build can see (wasm/discover.ts). A build
// translates the valid ones to native code (wasm/translate.ts) when the
// program uses the WebAssembly API; the others fail to load at run time.
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)")}`,
` ${c(DIM, "WebAssembly modules:")} ${c(DIM, "(found at build time; valid ones compile to native code)")}`,
);
for (const m of wasm) {
const name = m.path !== undefined ? displayPath(m.path, opts.root) : "inline module";
@@ -444,16 +444,17 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
}
}
// ── the WebAssembly global: every reached use refuses (webAssemblyDiag) ─
// ── the WebAssembly global (builtins/webassembly.ts, wasm/translate.ts):
// the core API lowers over modules translated at build time; the rest
// refuses at each use (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 } : {}),
status: "static",
note: `compile, instantiate, validate, Module, Instance, instanceof, the error classes, exports, memories, tables, globals and Module transfer to workers, over the modules translated at build time; the rest refuses with ${refusal.code} (${refusal.hint ?? ""})`,
});
}
@@ -271,11 +271,11 @@ export const UNSUPPORTED: Record<string, UnsupportedEntry> = {
"imports (#alias via the imports field, self-name references via " +
"exports), statically compiled npm packages, " +
"and the built-in fs, fs/promises, path, os, url, crypto, zlib, " +
"child_process, net, http, tls, https, http2, dgram, dns, util, " +
"child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, " +
"util/types, string_decoder, querystring, readline, events, stream, " +
"stream/promises, stream/consumers, buffer, assert, assert/strict, worker_threads, " +
"stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, " +
"cluster, tty, async_hooks, timers, timers/promises, " +
"diagnostics_channel, perf_hooks, module, and console modules (bare or " +
"diagnostics_channel, perf_hooks, module, v8, and console modules (bare or " +
"node:-prefixed) and node:test (node:-prefixed only, like in Node) " +
"are supported",
},
@@ -740,17 +740,19 @@ 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'). */
/** A REACHED use of a part of the WebAssembly API scriptc does not
* implement yet (SC2020): the Memory, Table and Global constructors,
* Module.imports/exports/customSections, the streaming functions, the
* classes as values. compile, instantiate, validate, Module, Instance and
* the error classes lower (builtins/webassembly.ts). `surface` names what
* was used ('new WebAssembly.Memory', 'WebAssembly.Module.imports'). */
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`,
message: `'${surface}' is not supported yet: scriptc implements WebAssembly.compile, instantiate, validate, Module, Instance and the error classes over modules translated at build time, but not this part of the API`,
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)",
hint: "the Memory, Table and Global constructors, Module.imports, exports and customSections, and the streaming functions are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations)",
};
}
@@ -51,6 +51,7 @@ const native: EarlyExecutableNativeFeatures = {
http: false,
http2: false,
dgram: false,
dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
@@ -56,6 +56,7 @@ export interface EarlyExecutableNativeFeatures {
fileHandle: boolean;
fetch: boolean;
eventTarget?: boolean;
wasm?: boolean;
warningReplace?: boolean;
stdioExtended?: boolean;
zlib: boolean;
@@ -77,6 +78,7 @@ export interface EarlyExecutableNativeFeatures {
http: boolean;
http2: boolean;
dgram: boolean;
dnsPromises: boolean;
watch: boolean;
foreignFfi: boolean;
nodeTest: boolean;
@@ -194,6 +196,7 @@ const BOOLEAN_NATIVE_KEYS = [
"http",
"http2",
"dgram",
"dnsPromises",
"watch",
"foreignFfi",
"nodeTest",
@@ -42,6 +42,9 @@ export const SUPPORTED_BUILTIN_MODULES = [
"module",
"console",
"process",
"v8",
"stream/web",
"dns/promises",
"punycode",
"constants",
] as const;
@@ -76,14 +79,33 @@ export function builtinDefaultImportModule(spec: string): string | null {
export function canonicalBuiltinModule(spec: string): string | null {
const bare = spec.startsWith("node:") ? spec.slice(5) : spec;
if (bare === spec && PREFIX_ONLY_BUILTIN_MODULES.has(bare)) return null;
return (SUPPORTED_BUILTIN_MODULES as readonly string[]).includes(bare) ? bare : null;
if ((SUPPORTED_BUILTIN_MODULES as readonly string[]).includes(bare)) return bare;
return bare === spec ? npmBuiltinAliasOf(spec) : null;
}
/** npm packages served as a builtin module instead of compiling their
* JavaScript (maintainer decision): graceful-fs monkeypatches the fs
* module object and queues retries on EMFILE/ENFILE, which statically
* lowered fs calls cannot honor, and it behaves as fs everywhere else. Its
* import and require resolve to node:fs; gracefulify is a no-op statement.
* The one documented difference: no retry queueing under file-descriptor
* exhaustion. */
const NPM_BUILTIN_ALIASES: Record<string, string | undefined> = { "graceful-fs": "fs" };
/** The builtin module an npm package specifier is served as, or null. */
export function npmBuiltinAliasOf(spec: string): string | null {
return Object.hasOwn(NPM_BUILTIN_ALIASES, spec) ? (NPM_BUILTIN_ALIASES[spec] ?? null) : null;
}
/** Both specifier spellings of every supported builtin (prefix-only ones
* keep just the node: form) — the ambient-module allowlist preflight uses. */
export const SUPPORTED_NODE_MODULES: readonly string[] = SUPPORTED_BUILTIN_MODULES.flatMap((m) =>
PREFIX_ONLY_BUILTIN_MODULES.has(m) ? [`node:${m}`] : [m, `node:${m}`],
);
* keep just the node: form) — the ambient-module allowlist preflight uses
* — plus the npm packages served as builtins. */
export const SUPPORTED_NODE_MODULES: readonly string[] = [
...SUPPORTED_BUILTIN_MODULES.flatMap((m) =>
PREFIX_ONLY_BUILTIN_MODULES.has(m) ? [`node:${m}`] : [m, `node:${m}`],
),
...Object.keys(NPM_BUILTIN_ALIASES),
];
/* ── out-of-scope builtins ───────────────────────────────────────────────
* Builtin modules a compiled binary is not going to serve — not "yet",
@@ -92,8 +114,8 @@ export const SUPPORTED_NODE_MODULES: readonly string[] = SUPPORTED_BUILTIN_MODUL
* so 'node:sqlite' keys as 'sqlite'. Exact-match only: an npm package
* whose name collides with none of these keys keeps the generic wording.
* Everything absent from this table stays the plain "not supported yet"
* story — genuinely pending surface (console's Console class, stream/web)
* must not read as permanently refused. */
* story — genuinely pending surface (console's Console class) must not
* read as permanently refused. */
/** Node's own HTTP/TLS/stream implementation internals, requireable by
* legacy convention (underscore-prefixed lib/ modules). They expose
@@ -102,11 +124,15 @@ export const SUPPORTED_NODE_MODULES: readonly string[] = SUPPORTED_BUILTIN_MODUL
const NODE_INTERNAL_REASON =
"a Node-internal module: it exposes Node's own implementation objects, which the scriptc runtime does not replicate";
const V8_REASON =
"it observes V8 engine internals — heap statistics and snapshots, GC and CPU profiles, serialize/deserialize — and a compiled binary embeds no V8 engine to observe";
/** node:v8 is importable (packages require it at top level and touch it
* only on optional paths), and getHeapStatistics answers allocator-derived
* figures; every other member observes the engine itself. Member fences
* carry this reason instead of a module-level refusal. */
export const V8_MEMBER_REASON =
"by design: this member observes V8 engine internals — heap snapshots and spaces, GC and CPU profiles, flags, serialize/deserialize — and a compiled binary embeds no V8 engine to observe";
const OUT_OF_SCOPE_BUILTIN_REASONS: Record<string, string | undefined> = {
v8: V8_REASON,
vm: "it compiles and runs JavaScript source at runtime, and a compiled binary embeds no JavaScript engine to run it",
inspector:
"it drives the V8 inspector protocol — debugger, profiler, heap access — and a compiled binary embeds no V8 engine to inspect",
"inspector/promises":
@@ -142,3 +168,10 @@ export function unsupportedModuleFeatureOf(spec: string): string {
: undefined;
return reason === undefined ? `the '${spec}' module` : `the '${spec}' module (${reason})`;
}
/** The ESNext temporal global's name. Node 24 does not define it: programs
* that name it type-check against TS 7's lib slice (program.ts adds it only
* for them) and lower to Node's answers. Spelled in two pieces so the
* compiler's own sources never trip program.ts's scan when scriptc
* compiles itself. */
export const TEMPORAL_GLOBAL = "Tempo" + "ral";
@@ -15,6 +15,8 @@ import { lowerStreamModuleCall } from "./lower-stream.js";
import { lowerCallbackPromisify } from "./builtins/promisify-callback.js";
import { lowerNodeModuleCall } from "./builtins/module-resolution.js";
import { builtinModuleFnOf, builtinFenceHintOf } from "./surfaces.js";
import { resultIsDiscarded } from "./call-position.js";
import { locOf, requireSpecOf } from "../program.js";
/** `events.addAbortListener(signal, listener)`: the runtime registers the
* once/resist-stop-propagation abort listener (or queues the call for an
@@ -54,6 +56,38 @@ export function lowerAddAbortListener(
return { kind: "dynObjLit", fields: [{ key, value: dispose }], type: DYN, loc };
}
/** graceful-fs's `gracefulify(fsModule)` (graceful-fs is served as node:fs —
* builtin-modules.ts): it patches a module object with the retry queue,
* which statically lowered fs calls never consult, so the call statement
* is a no-op. The argument still evaluates unless it is a plain reference
* (the patched module, typically `require("fs")` or a namespace binding).
* The returned module object has no representation; other positions
* fence. */
function lowerGracefulify(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
const args = expr.arguments.nodes;
if (!resultIsDiscarded(expr) || args.length !== 1 || ts.isSpreadElement(args[0]!)) {
lowerer.noLowering(
"gracefulify",
expr,
"graceful-fs compiles as node:fs; gracefulify(fsModule) lowers as a statement whose result is unused",
);
}
let arg: ts.Expression = args[0]!;
while (ts.isParenthesizedExpression(arg)) arg = arg.expression;
const plain =
ts.isIdentifier(arg) ||
(ts.isCallExpression(arg) && requireSpecOf(arg) !== null) ||
(ts.isPropertyAccessExpression(arg) && ts.isIdentifier(arg.expression));
if (plain) return { kind: "seqExpr", stmts: [], result: dynUndefinedExpr(loc), type: DYN, loc };
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) }],
result: dynUndefinedExpr(loc),
type: DYN,
loc,
};
}
/** Named imports, namespace members and callable aliases share validation
* and dispatch. Resolve diagnostic provenance only if no handler accepts. */
export function lowerBuiltinCall(
@@ -126,6 +160,8 @@ export function lowerBuiltinCall(
if (bi.module === "worker_threads" && bi.member === "markAsUncloneable") {
return lowerUncloneable(lowerer, expr);
}
if (bi.module === "fs" && bi.member === "gracefulify")
return lowerGracefulify(lowerer, expr, loc);
const builtinFn = builtinModuleFnOf(lowerer, bi.module, bi.member);
if (!builtinFn) {
lowerer.noLowering(
@@ -23,6 +23,17 @@ import { stripTypeCasts } from "./arguments.js";
export function builtinImportOf(
lowerer: Lowerer,
ident: ts.Identifier,
): { module: string; member: string } | null {
const binding = builtinModuleBindingOf(lowerer, ident);
// node:stream/web's exports ARE the web stream globals: its bindings
// lower through the globals' own paths (stdlibGlobalNameOf resolves the
// alias to the same name), not through module-member tables.
return binding?.module === "stream/web" ? null : binding;
}
function builtinModuleBindingOf(
lowerer: Lowerer,
ident: ts.Identifier,
): { module: string; member: string } | null {
const symbol =
ts.isShorthandPropertyAssignment(ident.parent) && ident.parent.name === ident
@@ -48,6 +48,7 @@ import {
} from "./operating-system.js";
import { lowerQuerystringParseCall, lowerQuerystringStringifyCall } from "./querystring.js";
import { lowerFsGlobCallbackCall, lowerFsGlobSyncCall, lowerFspGlobCall } from "./fs-glob.js";
import { lowerV8HeapStatisticsCall } from "./v8.js";
export function lowerBuiltinModuleCall(
lowerer: Lowerer,
@@ -134,6 +135,9 @@ export function lowerBuiltinModuleCall(
if (bi.module === "child_process" && (bi.member === "execFileSync" || bi.member === "execSync")) {
return lowerer.lowerExecSyncCall(expr, bi.member === "execSync", loc);
}
if (bi.module === "v8" && bi.member === "getHeapStatistics") {
return lowerV8HeapStatisticsCall(lowerer, expr, loc);
}
if (bi.module === "os" && bi.member === "networkInterfaces") {
return lowerOsNetworkInterfacesCall(lowerer, expr, loc);
}
@@ -0,0 +1,111 @@
/* node:v8 — importable, with one lowered member. Packages require it at
* top level and touch it on optional paths (dd-trace's runtime metrics),
* so the module itself is not refused. getHeapStatistics answers
* allocator-derived figures in Node's shape and field order, in the
* spirit of process.memoryUsage(): approximate values, not V8 heap
* figures (a compiled binary embeds no V8 engine). Every other member
* fences at its use with the by-design reason (builtinFenceHintOf). */
import { numLit } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { F64, type IrExpr, type SrcLoc, VOID } from "../../../ir/ir.js";
/** v8.getHeapStatistics()'s fields, in Node's order. */
const HEAP_STATISTICS_FIELDS = [
"total_heap_size",
"total_heap_size_executable",
"total_physical_size",
"total_available_size",
"used_heap_size",
"heap_size_limit",
"malloced_memory",
"peak_malloced_memory",
"does_zap_garbage",
"number_of_native_contexts",
"number_of_detached_contexts",
"total_global_handles_size",
"used_global_handles_size",
"external_memory",
];
/** `v8.getHeapStatistics()`: one memoryUsage sample (the runtime heap's
* total and used bytes), with the machine's memory as the heap limit (the
* runtime heap has no fixed ceiling) and the available size the limit
* minus used bytes. V8-only figures (executable code space, malloced
* memory, global handles, external memory, zapping) read 0, and one
* native context exists. The record is the call site's interned HeapInfo
* shape, built by a shared helper per shape. */
export function lowerV8HeapStatisticsCall(
lowerer: Lowerer,
call: ts.CallExpression,
loc: SrcLoc,
): IrExpr {
if (call.arguments.nodes.length !== 0) {
lowerer.noLowering(`v8.getHeapStatistics with ${call.arguments.nodes.length} arguments`, call);
}
const t = lowerer.mapTypeOf(lowerer.typeOf(call));
if (t?.kind !== "record") lowerer.badType(call, lowerer.typeOf(call));
const shape = lowerer.shapes.get(t.shapeId);
if (
!shape ||
!shape.fields.every((f) => HEAP_STATISTICS_FIELDS.includes(f.name) && f.type.kind === "f64")
) {
lowerer.badType(call, lowerer.typeOf(call));
}
const key = `v8.getHeapStatistics:${t.shapeId}`;
let helper = lowerer.valueHelpers.get(key);
if (!helper) {
helper = `%v8.getHeapStatistics.${lowerer.valueHelpers.size}`;
lowerer.valueHelpers.set(key, helper);
const sample = (index: number): IrExpr => ({
kind: "libCall",
fn: "process.memoryUsageField",
args: [numLit(index, loc)],
type: F64,
loc,
});
const limit = (): IrExpr => ({ kind: "libCall", fn: "os.totalmem", args: [], type: F64, loc });
const value = (name: string): IrExpr => {
switch (name) {
case "total_heap_size":
case "total_physical_size":
return sample(1);
case "used_heap_size":
return sample(2);
case "heap_size_limit":
return limit();
case "total_available_size":
return { kind: "bin", op: "-", left: limit(), right: sample(2), type: F64, loc };
case "number_of_native_contexts":
return numLit(1, loc);
default:
return numLit(0, loc);
}
};
lowerer.liftedFns.push({
name: helper,
params: [],
returnType: t,
locals: [],
body: [
{
kind: "exprStmt",
expr: { kind: "libCall", fn: "process.memoryUsageTake", args: [], type: VOID, loc },
loc,
},
{
kind: "return",
value: {
kind: "recordLit",
fields: shape.fields.map((f) => ({ name: f.name, value: value(f.name) })),
type: t,
loc,
},
loc,
},
],
loc,
});
}
return { kind: "call", callee: helper, args: [], type: t, loc };
}
@@ -2,17 +2,41 @@ 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 { BOOL, DYN, F64, STRING, type IrExpr } from "../../../ir/ir.js";
import { dynUndefinedExpr } from "../../../ir/build.js";
import { lowerDynObjectLiteral } from "../expressions/object-literals.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. */
* namespace) and the type mapper gives them the checked-dynamic
* representation. Modules whose bytes the build sees are translated to
* native code (wasm/translate.ts) and found at run time by the SHA-256 of
* their bytes (scr_wasm.c):
* - WebAssembly.compile, instantiate and validate calls, new
* WebAssembly.Module and new WebAssembly.Instance lower to wasm.*
* libCalls; members of the values they produce (exports, memory
* buffers, globals, tables) dispatch through the native handles;
* - `v instanceof WebAssembly.<Class>` lowers to wasm.is;
* - the error classes construct with or without `new` (wasm.errorNew).
* Every other reached use of the API value (the bare namespace, the
* Memory, Table and Global constructors, Module.imports and friends, the
* streaming functions) still 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. A lowering marks the lowerer, so the build translates the
* modules it discovers (lowerToIr). */
/** The classes `wasm.is` answers, by kind. */
const WASM_CLASSES = [
"Module",
"Instance",
"Memory",
"Table",
"Global",
"CompileError",
"LinkError",
"RuntimeError",
] as const;
/** Strips the spellings that leave a value unchanged: parentheses, type
* assertions, `satisfies` and non-null assertions. */
function bare(expr: ts.Expression): ts.Expression {
@@ -130,3 +154,158 @@ export function lowerWebAssemblyTypeof(lowerer: Lowerer, operand: ts.Expression)
}
return null;
}
/** The member `WebAssembly.<member>` names (any spelling of the namespace),
* or null. */
function webAssemblyMember(lowerer: Lowerer, expr: ts.Expression): string | null {
const surface = webAssemblySurface(lowerer, expr);
if (surface === null || !surface.startsWith("WebAssembly.")) return null;
const member = surface.slice("WebAssembly.".length);
return member.includes(".") ? null : member;
}
function lowerArg(lowerer: Lowerer, arg: ts.Expression): IrExpr {
return ts.isObjectLiteralExpression(arg)
? lowerDynObjectLiteral(lowerer, arg)
: lowerer.lowerExprExpecting(arg, DYN);
}
/** The arguments, padded with undefined to `count`; null for spreads or
* extra arguments (they stay refused). */
function lowerArgs(
lowerer: Lowerer,
args: readonly ts.Expression[],
count: number,
loc: IrExpr["loc"],
): IrExpr[] | null {
if (args.length > count || args.some(ts.isSpreadElement)) return null;
const out = args.map((arg) => lowerArg(lowerer, arg));
while (out.length < count) out.push(dynUndefinedExpr(loc));
return out;
}
function errorNew(member: string, args: IrExpr[], loc: IrExpr["loc"]): IrExpr {
const kind = (WASM_CLASSES as readonly string[]).indexOf(member);
return {
kind: "libCall",
fn: "wasm.errorNew",
args: [{ kind: "numLit", value: kind, type: F64, loc }, args[0]!, args[1]!],
type: DYN,
loc,
};
}
/** `WebAssembly.compile(bytes)`, `WebAssembly.instantiate(source,
* imports?)`, `WebAssembly.validate(bytes)`, and the error classes called
* without `new`. Null for anything else. */
export function lowerWebAssemblyCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
if (call.questionDotToken) return null;
const member = webAssemblyMember(lowerer, call.expression);
if (member === null) return null;
const loc = locOf(call);
const isError = member === "CompileError" || member === "LinkError" || member === "RuntimeError";
if (member !== "compile" && member !== "instantiate" && member !== "validate" && !isError)
return null;
const used = member === "instantiate" || isError ? 2 : 1;
const args = lowerArgs(lowerer, call.arguments.nodes, used, loc);
if (args === null) return null;
lowerer.usesWebAssembly = true;
let result: IrExpr;
if (isError) result = errorNew(member, args, loc);
else if (member === "validate")
result = { kind: "libCall", fn: "wasm.validate", args: [args[0]!], type: BOOL, loc };
else if (member === "compile")
result = {
kind: "libCall",
fn: "wasm.compile",
args: [args[0]!],
type: { kind: "promise", inner: DYN },
loc,
};
else
result = {
kind: "libCall",
fn: "wasm.instantiate",
args: [args[0]!, args[1]!],
type: { kind: "promise", inner: DYN },
loc,
};
return result;
}
/** `new WebAssembly.Module(bytes)`, `new WebAssembly.Instance(module,
* imports?)` and the error classes. Null for anything else. */
export function lowerWebAssemblyNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
const member = webAssemblyMember(lowerer, expr.expression);
if (member === null) return null;
const isError = member === "CompileError" || member === "LinkError" || member === "RuntimeError";
if (member !== "Module" && member !== "Instance" && !isError) return null;
const loc = locOf(expr);
const used = member === "Module" ? 1 : 2;
const args = lowerArgs(lowerer, expr.arguments?.nodes ?? [], used, loc);
if (args === null) return null;
lowerer.usesWebAssembly = true;
let result: IrExpr;
if (isError) result = errorNew(member, args, loc);
else if (member === "Module")
result = { kind: "libCall", fn: "wasm.moduleNew", args: [args[0]!], type: DYN, loc };
else
result = {
kind: "libCall",
fn: "wasm.instanceNew",
args: [args[0]!, args[1]!],
type: DYN,
loc,
};
return result;
}
/** `v instanceof WebAssembly.<Class>`. Null for anything else. */
export function lowerWebAssemblyInstanceof(
lowerer: Lowerer,
expr: ts.BinaryExpression,
): IrExpr | null {
const member = webAssemblyMember(lowerer, expr.right);
if (member === null) return null;
const kind = (WASM_CLASSES as readonly string[]).indexOf(member);
if (kind < 0) return null;
const loc = locOf(expr);
lowerer.usesWebAssembly = true;
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
return {
kind: "libCall",
fn: "wasm.is",
args: [value, { kind: "numLit", value: kind, type: F64, loc }],
type: BOOL,
loc,
};
}
/** WebAssembly API values: `WebAssembly.compile`, `instantiate` and
* `validate` read as functions, the namespace itself (`WebAssembly`,
* `globalThis.WebAssembly`), and the global object cast to a type with a
* `WebAssembly` member (pi codemode's spelling). Null for anything else,
* which refuseWebAssemblyUse then fences. */
export function lowerWebAssemblyValue(lowerer: Lowerer, expr: ts.Expression): IrExpr | null {
const surface = webAssemblySurface(lowerer, expr);
if (surface === null) return null;
const loc = locOf(expr);
const node = bare(expr);
const fn = (name: string, args: IrExpr[]): IrExpr => {
lowerer.usesWebAssembly = true;
return { kind: "libCall", fn: name as "wasm.namespace", args, type: DYN, loc };
};
if (surface === "globalThis.WebAssembly") {
// The cast spelling names the global object, not its member.
const cast =
(ts.isAsExpression(expr) || ts.isTypeAssertion(expr)) &&
isGlobalThis(lowerer, expr.expression);
return cast ? fn("wasm.globalView", []) : fn("wasm.namespace", []);
}
if (surface === "WebAssembly") return fn("wasm.namespace", []);
const index = ["WebAssembly.compile", "WebAssembly.instantiate", "WebAssembly.validate"].indexOf(
surface,
);
if (index < 0 || !ts.isPropertyAccessExpression(node)) return null;
return fn("wasm.function", [{ kind: "numLit", value: index, type: F64, loc }]);
}
@@ -0,0 +1,142 @@
/* Code generation from strings: `eval(...)`, `new Function(...)` and
* `Function(...)`. A compiled binary embeds no JavaScript engine, so scriptc
* answers exactly as `node --disallow-code-generation-from-strings` does:
* every call throws the catchable EvalError "Code generation from strings
* disallowed for this context". Packages that probe for code generation
* (zod 4's `allowsEval`, template engines, CSP-aware JITs) then take their
* interpreted paths. The arguments still evaluate first, in order, and the
* Function constructor converts each to a string before it refuses, as
* V8's CreateDynamicFunction does; eval refuses without converting. */
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import { isJsSourceFile, locOf } from "../program.js";
import { DYN, F64, type IrExpr, type IrStmt, type IrType, STRING } from "../../ir/ir.js";
import { nodeThrowExpr } from "../../ir/build.js";
import { ambientUndefReadType, contextualUndefReadType } from "./lower-namespaces.js";
import { stdlibGlobalNameOf } from "./surfaces.js";
export const CODE_GENERATION_DISALLOWED =
"Code generation from strings disallowed for this context";
/** The code-generation entry point a callee names: the stdlib `eval` or
* `Function` global (directly, through `globalThis.`, or through a stable
* alias), or the indirect-eval spelling `(0, eval)`. Null otherwise. */
export function codeGenerationCalleeOf(
lowerer: Lowerer,
callee: ts.Expression,
): "eval" | "Function" | null {
let e = callee;
while (ts.isParenthesizedExpression(e)) e = e.expression;
if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.CommaToken) {
return codeGenerationCalleeOf(lowerer, e.right) === "eval" ? "eval" : null;
}
// Syntactic pre-check first: every call and construction asks, and only
// these spellings can name the globals (aliases are stdlib-resolved
// symbols, checked below only when the spelling could match).
const text = ts.isIdentifier(e) ? e.text : ts.isPropertyAccessExpression(e) ? e.name.text : null;
if (text === null) return null;
if (text !== "eval" && text !== "Function" && !hasCodeGenerationAlias(lowerer)) return null;
const name = stdlibGlobalNameOf(lowerer, e);
return name === "eval" || name === "Function" ? name : null;
}
/** Some stable binding aliases `eval` or `Function` (`const F = Function`,
* zod 4's spelling). */
function hasCodeGenerationAlias(lowerer: Lowerer): boolean {
for (const name of lowerer.stdlibGlobalAliases.values()) {
if (name === "eval" || name === "Function") return true;
}
return false;
}
/** The comma operands an indirect eval evaluates before the call: `a` in
* `(a, eval)(...)`, outermost first. */
function indirectEvalPrefix(callee: ts.Expression): ts.Expression[] {
let e = callee;
while (ts.isParenthesizedExpression(e)) e = e.expression;
if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.CommaToken) return [];
return [...indirectEvalPrefix(e.left), e.left, ...indirectEvalPrefix(e.right)].filter(
(operand) => {
let o = operand;
while (ts.isParenthesizedExpression(o)) o = o.expression;
return !(ts.isBinaryExpression(o) && o.operatorToken.kind === ts.SyntaxKind.CommaToken);
},
);
}
/** Lowers a code-generation call or construction to Node's refusal under
* --disallow-code-generation-from-strings: the callee's own operands, then
* the arguments in order (converted to strings for the Function
* constructor), then the EvalError. The result carries the use site's type
* (the throw always fires, so the value is never observed). */
export function lowerCodeGenerationCall(
lowerer: Lowerer,
expr: ts.CallExpression | ts.NewExpression,
kind: "eval" | "Function",
): IrExpr {
const loc = locOf(expr);
const stmts: IrStmt[] = [];
for (const operand of indirectEvalPrefix(expr.expression)) {
stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(operand), loc: locOf(operand) });
}
for (const arg of expr.arguments?.nodes ?? []) {
if (ts.isSpreadElement(arg)) {
lowerer.noLowering(
`${kind === "eval" ? "eval" : "the Function constructor"} with spread arguments`,
arg,
"code generation from strings throws EvalError in compiled programs, as under node --disallow-code-generation-from-strings; pass the arguments directly",
);
}
const value = lowerer.lowerExpr(arg);
const argLoc = locOf(arg);
if (kind === "Function" && !isPrimitiveIrType(value.type)) {
stmts.push({
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "dyn.toStringCoerce",
args: [lowerer.coerceToExpected(value, DYN)],
type: STRING,
loc: argLoc,
},
loc: argLoc,
});
} else {
stmts.push({ kind: "exprStmt", expr: value, loc: argLoc });
}
}
const type = useSiteType(lowerer, expr);
const refusal = nodeThrowExpr(6, "", CODE_GENERATION_DISALLOWED, type, loc);
return stmts.length === 0 ? refusal : { kind: "seqExpr", stmts, result: refusal, type, loc };
}
function isPrimitiveIrType(t: IrType): boolean {
return (
t.kind === "string" ||
t.kind === "f64" ||
t.kind === "bool" ||
t.kind === "void" ||
t.kind === "bigint"
);
}
function useSiteType(lowerer: Lowerer, expr: ts.Expression): IrType {
// The Function interface has no mapping (mapping it would fence): the
// constructed value is checked-dynamic in JavaScript, and a typed dummy
// elsewhere — the throw always fires first.
const type = lowerer.typeOf(expr);
const symbol = type.getSymbol();
if (symbol?.name === "Function" && lowerer.isStdlibSymbol(symbol)) {
return isJsSourceFile(expr.getSourceFile()) ? DYN : F64;
}
const mapped = lowerer.mapTypeOf(type);
if (
mapped &&
mapped.kind !== "void" &&
mapped.kind !== "func" &&
!lowerer.typeNamesUnregisteredClass(mapped)
) {
return mapped;
}
return ambientUndefReadType(lowerer, expr) ?? contextualUndefReadType(lowerer, expr) ?? F64;
}
@@ -44,6 +44,7 @@ import {
ambientUndefinedFnSymbolOf,
contextualUndefReadType,
fenceEarlyAliasUse,
isTemporalGlobalRoot,
lowerNsIdentifierValue,
nsUndefRead,
} from "../lower-namespaces.js";
@@ -543,7 +544,12 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
return builtinFunctionFenceValue(lowerer, bi, expr, loc);
return {
kind: "strLit",
value: `[builtin ${bi.module}.${bi.member}]`,
// node:stream/web exports the web stream globals themselves:
// the same identity token as the global spelling.
value:
bi.module === "stream/web"
? `[builtin ${bi.member}]`
: `[builtin ${bi.module}.${bi.member}]`,
type: STRING,
loc,
};
@@ -723,6 +729,13 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
}
{
const sym = lowerer.checker.getSymbolAtLocation(expr);
// The temporal global is typed (TS 7's lib slice) but Node 24 does
// not define it: the read throws Node's ReferenceError.
if (isTemporalGlobalRoot(lowerer, expr, sym)) {
const t =
ambientUndefReadType(lowerer, expr) ?? contextualUndefReadType(lowerer, expr) ?? F64;
return nsUndefRead(lowerer, expr.text, expr, t);
}
if (lowerer.isStdlibSymbol(sym) || expr.text === "globalThis") {
const canonical = stdlibGlobalNameOf(lowerer, expr) ?? expr.text;
// Builtin constructors have real function values in both source
@@ -96,6 +96,7 @@ import {
lowerRequireMainProperty,
} from "../lower-node-module.js";
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../../ir/build.js";
import { TEMPORAL_GLOBAL } from "../../builtin-modules.js";
import { tryLowerExpression } from "./try-lower-expression.js";
import {
cjsClassExprWholeExportOf,
@@ -781,6 +782,8 @@ export function lowerPropertyAccessExpr(
const canonical = stdlibGlobalNameOf(lowerer, expr);
if (canonical !== null) {
if (canonical === "globalThis") return lowerGlobalValue(lowerer, expr);
// Typed for the programs that name it, absent in Node 24.
if (canonical === TEMPORAL_GLOBAL) return dynUndefinedExpr(loc);
if (canonical === "ArrayBuffer")
return { kind: "libCall", fn: "arrayBuffer.constructor", args: [], type: DYN, loc };
if (canonical === "fetch")
@@ -25,7 +25,7 @@ import {
typeEquals,
typeKey,
} from "../../../ir/ir.js";
import { refuseWebAssemblyUse } from "../builtins/webassembly.js";
import { lowerWebAssemblyInstanceof, refuseWebAssemblyUse } from "../builtins/webassembly.js";
import { lowerSocketInstanceOf } from "../lower-server.js";
import { builtinClassRefOf } from "../builtins/class-values.js";
import { lowerZlibInstanceOf } from "../builtins/compression.js";
@@ -62,6 +62,8 @@ import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js"
* 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 {
const webAssembly = lowerWebAssemblyInstanceof(lowerer, expr);
if (webAssembly !== null) return webAssembly;
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.
@@ -33,6 +33,8 @@ import { isSafeToDiscard, isSafeToRepeat } from "./evaluation-safety.js";
import { varRef } from "../../../ir/build.js";
import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors.js";
import { InternalCompilerError } from "../../../errors.js";
import { TEMPORAL_GLOBAL } from "../../builtin-modules.js";
import { isTemporalGlobalRoot } from "../lower-namespaces.js";
export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc: SrcLoc): IrExpr {
const webAssembly = lowerWebAssemblyTypeof(lowerer, expr.expression);
@@ -158,6 +160,15 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc
// declarations type some (AbortSignal) without a construct signature,
// so neither the operand nor the type can answer; the read has no
// effects.
// `typeof globalThis.<temporal>`: the global object has no such
// property in Node 24.
if (
ts.isPropertyAccessExpression(tested) &&
tested.name.text === TEMPORAL_GLOBAL &&
stdlibGlobalNameOf(lowerer, tested.expression) === "globalThis"
) {
return { kind: "strLit", value: "undefined", type: STRING, loc };
}
if (constructorGlobalTypeof(lowerer, tested) !== null)
return { kind: "strLit", value: "function", type: STRING, loc };
// `typeof globalThis.AbortSignal.any` (ky's feature probe): a static
@@ -182,6 +193,11 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc
if (!sym) {
return { kind: "strLit", value: "undefined", type: STRING, loc };
}
// Node 24 defines no temporal global: typeof answers "undefined"
// without reading it, like any undeclared name.
if (isTemporalGlobalRoot(lowerer, tested, sym)) {
return { kind: "strLit", value: "undefined", type: STRING, loc };
}
// `const { inspect } = require("util")` / `import { inspect } from
// "node:util"`: a binding of a builtin module's exported function,
// class or object. JavaScript sources represent such a value without
@@ -248,6 +248,8 @@ import {
genericCallInstance,
} from "./generic-functions.js";
import { codeGenerationCalleeOf, lowerCodeGenerationCall } from "./code-generation.js";
const ZLIB_STREAM_METHODS: ReadonlySet<string> = new Set(["flush", "params", "close", "reset"]);
export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
@@ -268,6 +270,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const json = lowerJsonRequireCall(lowerer, expr);
if (json) return json;
// eval(...) and Function(...): Node's EvalError under
// --disallow-code-generation-from-strings (code-generation.ts).
{
const codeGeneration = codeGenerationCalleeOf(lowerer, expr.expression);
if (codeGeneration !== null) return lowerCodeGenerationCall(lowerer, expr, codeGeneration);
}
// Inline require() of a program module (cjs-inline-require.ts): a
// member call over its export table resolves like a binding's member
// call after the module's %init at this position; the require itself
@@ -16,6 +16,8 @@ import { InternalCompilerError } from "../../errors.js";
* accessor access, upcasts, `new` expressions, and the builtin Error
* hierarchy registration. */
import { fsStreamClassOf, lowerFsStreamNew } from "./builtins/fs-streams.js";
import { codeGenerationCalleeOf, lowerCodeGenerationCall } from "./code-generation.js";
import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
import * as ts from "../ts7/adapter.js";
import { lowerZlibStreamNew, zlibClassName, zlibStreamModeOf } from "./builtins/compression.js";
import { everyStmtList } from "../../ir/traverse.js";
@@ -145,6 +147,7 @@ import {
ambientUndefReadType,
ambientUndefVarRootOf,
ambientUndefinedFnSymbolOf,
contextualUndefReadType,
fenceEarlyAliasUse,
fenceEarlyNsMemberRef,
nsMemberIdentOf,
@@ -8960,6 +8963,20 @@ function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean {
export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
const loc = locOf(expr);
// new Function(...): Node's EvalError under
// --disallow-code-generation-from-strings (code-generation.ts).
if (codeGenerationCalleeOf(lowerer, expr.expression) === "Function")
return lowerCodeGenerationCall(lowerer, expr, "Function");
// `new <temporal>.X(...)`: Node 24 has no such global, so the callee's
// root read throws the ReferenceError before any argument evaluates.
{
const root = ambientUndefVarRootOf(lowerer, expr.expression);
if (root !== null && root.text === TEMPORAL_GLOBAL) {
const t =
ambientUndefReadType(lowerer, expr) ?? contextualUndefReadType(lowerer, expr) ?? F64;
return nsUndefRead(lowerer, root.text, expr, t);
}
}
// `new (require("./x").C)()`: the module's %init at the require's
// position, then construction through the member as for a binding.
if (ts.isPropertyAccessExpression(expr.expression) && !expr.expression.questionDotToken) {
@@ -15,6 +15,7 @@ import {
BOOL,
canBoxFuncIntoDyn,
canDynCheckTo,
typeKey,
DGRAMSOCK_T,
DYN,
F64,
@@ -131,6 +132,7 @@ export function lowerDgramDnsModuleCall(
loc: SrcLoc,
): IrExpr | null {
if (bi.module === "dns") return lowerDnsModuleCall(lowerer, expr, bi, loc);
if (bi.module === "dns/promises") return lowerDnsPromisesCall(lowerer, expr, bi, loc);
if (bi.module !== "dgram") return null;
const args = expr.arguments.nodes;
if (bi.member === "createSocket") {
@@ -278,6 +280,124 @@ function lowerDnsCallback(lowerer: Lowerer, node: ts.Expression, what: string):
);
}
/** node:dns/promises calls (and dns.promises members): lookup(hostname[,
* options]) and lookupService(address, port) as promises over the callback
* forms' runtime (argument errors throw synchronously, as Node validates
* before it creates the promise), plus the shared getServers and result
* order functions. A typed result (Promise<LookupAddress>) checks the
* settled value through the checked-dynamic boundary. */
function lowerDnsPromisesCall(
lowerer: Lowerer,
expr: ts.CallExpression,
bi: { module: string; member: string },
loc: SrcLoc,
): IrExpr {
const args = expr.arguments.nodes;
const dyn = (node: ts.Expression): IrExpr =>
lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN);
if (bi.member === "lookup" || bi.member === "lookupService") {
const service = bi.member === "lookupService";
if (service ? args.length !== 2 : args.length !== 1 && args.length !== 2) {
lowerer.noLowering(
`dnsPromises.${bi.member} with ${args.length} arguments`,
expr,
service
? "the form is lookupService(address, port)"
: "the supported forms are lookup(hostname) and lookup(hostname, options)",
);
}
const promise: IrExpr = {
kind: "libCall",
fn: service ? "dns.promisesLookupService" : "dns.promisesLookup",
args: [dyn(args[0]!), args.length === 2 ? dyn(args[1]!) : dynUndefinedExpr(loc)],
type: { kind: "promise", inner: DYN },
loc,
};
const declared = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (declared === null || declared.kind !== "promise" || declared.inner.kind === "dyn") {
return promise;
}
return checkedDynPromise(lowerer, promise, declared, expr, loc);
}
if (["getServers", "getDefaultResultOrder", "setDefaultResultOrder"].includes(bi.member)) {
return lowerDnsModuleCall(lowerer, expr, { module: "dns", member: bi.member }, loc);
}
lowerer.noLowering(
`dnsPromises.${bi.member}`,
expr,
"lookup, lookupService, getServers, getDefaultResultOrder, and setDefaultResultOrder are the lowered dns/promises functions",
ts.isIdentifier(expr.expression) ? lowerer.resolveValueSymbol(expr.expression) : undefined,
);
}
/** A promise<dyn> as the call site's typed promise: an interned async
* helper awaits it and checks the settled value into the declared inner
* type (a mismatch rejects with the checked-dynamic TypeError). */
function checkedDynPromise(
lowerer: Lowerer,
promise: IrExpr,
declared: IrType & { kind: "promise" },
node: ts.Node,
loc: SrcLoc,
): IrExpr {
const inner = declared.inner;
if (
!canDynCheckTo(
inner,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
)
) {
lowerer.noLowering(
`a dns/promises result typed '${lowerer.fmt(declared)}'`,
node,
"the settled value must have a checked-dynamic conversion (type it as the documented LookupAddress shape or unknown)",
);
}
const key = `dns.promise:${typeKey(declared)}`;
let helper = lowerer.valueHelpers.get(key);
if (!helper) {
helper = `%dns.promise.${lowerer.valueHelpers.size}`;
lowerer.valueHelpers.set(key, helper);
const promiseT: IrType = { kind: "promise", inner: DYN };
lowerer.liftedFns.push({
name: helper,
params: [{ localId: "promise", name: "promise", type: promiseT }],
returnType: inner,
locals: [
{ id: "promise", name: "promise", type: promiseT, mutable: false },
{ id: "settled", name: "settled", type: DYN, mutable: false },
],
body: [
{
kind: "varDecl",
localId: "settled",
init: {
kind: "awaitExpr",
value: { kind: "varRef", localId: "promise", type: promiseT, loc },
type: DYN,
loc,
},
loc,
},
{
kind: "return",
value: {
kind: "dynCheck",
value: { kind: "varRef", localId: "settled", type: DYN, loc },
type: inner,
loc,
},
loc,
},
],
loc,
async: true,
});
}
return { kind: "call", callee: helper, args: [promise], type: declared, loc };
}
/** dns module calls: lookup(hostname[, options], callback) with Node's
* option forms, lookupService(address, port, callback), getServers() and
* get/setDefaultResultOrder(). The runtime validates the arguments as
@@ -1,4 +1,4 @@
import { refuseWebAssemblyUse } from "./builtins/webassembly.js";
import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js";
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js";
import { InternalCompilerError } from "../../errors.js";
/* Expression lowering: the expression dispatch (lowerExpr), literals
@@ -263,7 +263,10 @@ 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.
// WebAssembly API values lower; every other reached use of the API
// fences at its expression.
const webAssemblyValue = lowerWebAssemblyValue(lowerer, expr);
if (webAssemblyValue !== null) return webAssemblyValue;
refuseWebAssemblyUse(lowerer, expr);
if (ts.isNumericLiteral(expr)) {
@@ -3,15 +3,19 @@ import { DYN, STRING, arrayOf, type IrExpr } from "../../ir/ir.js";
import { strLit } from "../../ir/build.js";
import { locOf } from "../program.js";
import type { Lowerer } from "./lowerer.js";
import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
/** A stored global object has identity and survives module/function boundaries.
* Known globals without a value implementation refuse at runtime; absent host
* capabilities answer undefined. The checker supplies names, never values. */
export function lowerGlobalValue(lowerer: Lowerer, expr: ts.Expression): IrExpr {
const loc = locOf(expr);
// The temporal global is typed for the programs that name it (TS 7's
// lib slice) but absent in Node 24: reads answer undefined.
const names = lowerer.checker
.getPropertiesOfType(lowerer.typeOf(expr))
.map((property) => property.name);
.map((property) => property.name)
.filter((name) => name !== TEMPORAL_GLOBAL);
return {
kind: "libCall",
fn: "global.native",
@@ -42,6 +42,7 @@ import { boundIdentifiersOf } from "./lowerer.js";
import type { FileParts } from "./lower-modules.js";
import { locOf, resolveImport } from "../program.js";
import { F64, type IrExpr, type IrStmt, type IrType, STRING } from "../../ir/ir.js";
import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
/** True when this module declaration produces NO runtime construct at all:
* ambient (`declare namespace/module`, `declare global`, string-named
@@ -425,7 +426,7 @@ export function ambientUndefVarRootOf(lowerer: Lowerer, e: ts.Expression): ts.Id
const sym = lowerer.resolveValueSymbol(root);
if (!sym) return null;
if (lowerer.trapBindings.has(sym)) return root;
if (lowerer.isStdlibSymbol(sym)) return null;
if (lowerer.isStdlibSymbol(sym)) return isTemporalGlobalRoot(lowerer, root, sym) ? root : null;
if (ambientUndefinedFnSymbolOf(lowerer, root) !== null) return root;
const decls = sym.declarations;
if (decls.length === 0) return null;
@@ -440,6 +441,24 @@ export function ambientUndefVarRootOf(lowerer: Lowerer, e: ts.Expression): ts.Id
}
}
/** True when `ident` reads the ESNext temporal global, which the program
* type-checks against TS 7's lib slice (program.ts adds it only to programs
* that name it) but Node 24 does not define: Node's read throws the
* catchable ReferenceError "<name> is not defined", so every chain rooted
* here lowers to that throw (typeof answers "undefined" in lower-exprs). */
export function isTemporalGlobalRoot(
lowerer: Lowerer,
ident: ts.Identifier,
sym: ts.Symbol | undefined | null,
): boolean {
return (
ident.text === TEMPORAL_GLOBAL &&
sym !== undefined &&
sym !== null &&
lowerer.isStdlibSymbol(sym)
);
}
/** The declaration-classification form of ambientUndefVarRootOf: the root
* that makes `decl` a TRAP BINDING — its initializer provably throws the
* root's ReferenceError, so the binding never holds a value and needs no
@@ -53,7 +53,7 @@ import {
sideEffectFreeOptionValue,
TLS_SERVER_DOCUMENTED_OPTIONS,
} from "./surfaces.js";
import { conditionalSpreadOf } from "./expressions/object-literals.js";
import { conditionalSpreadOf, lowerDynObjectLiteral } from "./expressions/object-literals.js";
import { resultIsDiscarded } from "./call-position.js";
import { lowerCallbackArg as lowerCallbackArgShared } from "./callback-arg.js";
import { pairsSnapshotHelper } from "./pairs-snapshot.js";
@@ -1660,6 +1660,29 @@ function lowerNetServerMethodCall(
: [receiver, port, host, v6only, reusePort, cbV],
);
}
// listen(path[, cb]): a string first argument names a Unix socket
// (the runtime takes a numeric string as a port, Node's isPipeName).
if (lowerer.mapTypeOf(lowerer.typeOf(args[0]!))?.kind === "string") {
if (args.length > 2) {
lowerer.noLowering(
`listen(path) with ${args.length} arguments`,
call,
"the supported form is listen(path[, callback])",
);
}
const path = lowerer.lowerExprExpecting(args[0]!, STRING);
if (args.length === 1) return listenResult("net.listenPath", [receiver, path]);
const { cb } = lowerCallbackArg(
lowerer,
args[1]!,
"listen callbacks",
0,
() => false,
"use ()",
[],
);
return listenResult("net.listenPathCb", [receiver, path, cb]);
}
const port = lowerer.lowerExprExpecting(args[0]!, F64);
// The optional middle host — listen(port, '127.0.0.1'[, cb]): a
// string-typed second argument is the bind address (the net.connect
@@ -1715,6 +1738,21 @@ function lowerNetServerMethodCall(
if (args.length === 0) {
return { kind: "libCall", fn: "net.serverClose", args: [receiver], type: VOID, loc };
}
if (
lowerer.checker
.getCallSignatures(lowerer.typeOf(args[0]!))
.some((sig) => sig.getParameters().length > 0)
) {
// close((err) => ...): undefined once the server closes, or Node's
// ERR_SERVER_NOT_RUNNING when it was not listening.
return {
kind: "libCall",
fn: "net.serverCloseDyn",
args: [receiver, lowerSocketDynListener(lowerer, args[0]!, "close callbacks")],
type: VOID,
loc,
};
}
const { cb } = lowerCallbackArg(
lowerer,
args[0]!,
@@ -2088,6 +2126,32 @@ function lowerNetServerMethodCall(
const result = lowerer.mapTypeOf(lowerer.typeOf(call));
if (result && isAddressInfoRecord(lowerer, result)) {
const receiver = coerceToHandle(lowerer, access.expression, NETSERVER_T);
if (unixListenReachable(lowerer)) {
// A Unix-socket server's address() is its path, which this record
// cannot hold: the check throws for one before the record reads.
const local = lowerer.declareHiddenLocal("%addressServer", NETSERVER_T);
const server = (): IrExpr => varRef(local.id, NETSERVER_T, loc);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: local.id, init: receiver, loc },
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "net.serverAddressCheck",
args: [server()],
type: VOID,
loc,
},
loc,
},
],
result: { kind: "libCall", fn: "net.serverAddress", args: [server()], type: result, loc },
type: result,
loc,
};
}
return { kind: "libCall", fn: "net.serverAddress", args: [receiver], type: result, loc };
}
// @types/node's `string | AddressInfo | null`: the record while the
@@ -2107,20 +2171,50 @@ function lowerNetServerMethodCall(
const receiver = coerceToHandle(lowerer, access.expression, NETSERVER_T);
const local = lowerer.declareHiddenLocal("%addressServer", NETSERVER_T);
const server = varRef(local.id, NETSERVER_T, loc);
const recordArm: IrExpr = {
kind: "unionWrap",
unionId,
tag: lowerer.armTag(unionId, record),
value: { kind: "libCall", fn: "net.serverAddress", args: [server], type: record, loc },
type: result,
loc,
};
const stringTag = lowerer.armTag(unionId, STRING);
let listeningArm: IrExpr = recordArm;
const stmts: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: receiver, loc }];
if (stringTag >= 0 && unixListenReachable(lowerer)) {
// Node answers a Unix-socket server's address() with its path.
const pathLocal = lowerer.declareHiddenLocal("%addressPath", STRING);
const path = (): IrExpr => varRef(pathLocal.id, STRING, loc);
stmts.push({
kind: "varDecl",
localId: pathLocal.id,
init: { kind: "libCall", fn: "net.serverUnixPath", args: [server], type: STRING, loc },
loc,
});
listeningArm = {
kind: "ternary",
cond: {
kind: "strEq",
negated: true,
left: path(),
right: strLit("", loc),
type: BOOL,
loc,
},
then: { kind: "unionWrap", unionId, tag: stringTag, value: path(), type: result, loc },
else_: recordArm,
type: result,
loc,
};
}
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: local.id, init: receiver, loc }],
stmts,
result: {
kind: "ternary",
cond: { kind: "libCall", fn: "net.serverListening", args: [server], type: BOOL, loc },
then: {
kind: "unionWrap",
unionId,
tag: lowerer.armTag(unionId, record),
value: { kind: "libCall", fn: "net.serverAddress", args: [server], type: record, loc },
type: result,
loc,
},
then: listeningArm,
else_: {
kind: "unionWrap",
unionId,
@@ -2176,6 +2270,34 @@ function socketEncodingReachable(lowerer: Lowerer): boolean {
return reachable;
}
/** Whether some server in the program may listen on a Unix socket: a
* `.listen(x, ...)` call on a server (or an untyped value) whose first
* argument is string-typed. Only then does address() check for the socket
* path, so other programs keep the record-only lowering. */
function unixListenReachable(lowerer: Lowerer): boolean {
if (lowerer.unixListenReachable !== null) return lowerer.unixListenReachable;
let reachable = false;
for (const file of lowerer.program.getSourceFiles()) {
if (file.isDeclarationFile || !file.text.includes("listen")) continue;
ts.walkPreorder(file, (node) => {
if (reachable) return "skip";
if (!ts.isCallExpression(node) || node.arguments.nodes.length === 0) return undefined;
const callee = node.expression;
if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "listen") return undefined;
const kind = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(callee.expression))?.kind;
if (kind !== undefined && kind !== "netServer" && kind !== "dyn") return undefined;
const arg = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(node.arguments.nodes[0]!));
// Only a string-typed argument lowers to listen(path) (an untyped
// one checks into a port).
if (arg?.kind === "string") reachable = true;
return undefined;
});
if (reachable) break;
}
lowerer.unixListenReachable = reachable;
return reachable;
}
/** An inline 'data' listener whose chunk parameter is typed Buffer, for a
* socket that may have an encoding set: the chunk binds as a checked
* dynamic value, so the body sees what Node passes (a string after
@@ -2769,6 +2891,17 @@ function lowerNetSocketMethodCall(
: "net.sockOnConnect";
return { kind: "libCall", fn, args: [receiver, cb, once], type: VOID, loc };
}
if (event === "lookup") {
// (err, address, family, host) after the host name resolves, before
// the connect: once per resolved address, or once with the error.
return {
kind: "libCall",
fn: "net.sockOnLookup",
args: [receiver, lowerSocketDynListener(lowerer, args[1]!, "lookup listeners"), once],
type: VOID,
loc,
};
}
if (event === "finish") {
// Fires once when the FIN goes out — once either way (the event
// happens at most once per socket).
@@ -2828,7 +2961,7 @@ function lowerNetSocketMethodCall(
lowerer.noLowering(
`socket.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
args[0]!,
'"data", "end", "close", "error", "connect", "ready", "timeout", "readable", "secureConnect", and "session" are the supported socket events (as literals)',
'"data", "end", "close", "error", "connect", "lookup", "ready", "timeout", "readable", "secureConnect", and "session" are the supported socket events (as literals)',
);
}
lowerer.noLowering(
@@ -4004,6 +4137,30 @@ export function lowerServerProperty(
if (lowerer.mapTypeOf(lowerer.typeOf(inner.expression))?.kind !== "netServer") return null;
if (!lowerer.isStdlibMember(inner)) return null;
const receiver = coerceToHandle(lowerer, inner.expression, NETSERVER_T);
if (unixListenReachable(lowerer)) {
const local = lowerer.declareHiddenLocal("%addressServer", NETSERVER_T);
const server = (): IrExpr => varRef(local.id, NETSERVER_T, loc);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: local.id, init: receiver, loc },
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "net.serverAddressCheck",
args: [server()],
type: VOID,
loc,
},
loc,
},
],
result: { kind: "libCall", fn: "net.serverPort", args: [server()], type: F64, loc },
type: F64,
loc,
};
}
return { kind: "libCall", fn: "net.serverPort", args: [receiver], type: F64, loc };
}
@@ -5860,6 +6017,44 @@ function lowerHttp2ModuleCall(
);
}
/** The checked-dynamic options value of a request whose options the
* runtime reads: a non-literal options value (a RequestOptions variable —
* checked-dynamic storage — or a record that converts to one), or a
* literal the static walk cannot lower because it spreads another options
* value or sets socketPath. Null keeps the static literal walk (and its
* fences), which every other literal takes unchanged. */
function runtimeRequestOptions(
lowerer: Lowerer,
optsNode: ts.Expression,
secureish: boolean,
): IrExpr | null {
if (ts.isObjectLiteralExpression(optsNode)) {
const needsRuntime = optsNode.properties.nodes.some((prop) => {
if (ts.isSpreadAssignment(prop)) {
// the static walk's one spread: `...(c ? { rejectUnauthorized } : {})` on https
const cs = conditionalSpreadOf(prop.expression);
return !(
secureish &&
cs !== null &&
cs !== "unsupported" &&
cs.props.length === 1 &&
cs.props[0]!.name.text === "rejectUnauthorized"
);
}
return (
(ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) &&
(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
prop.name.text === "socketPath"
);
});
if (!needsRuntime) return null;
return lowerDynObjectLiteral(lowerer, optsNode);
}
const t = lowerer.mapTypeOf(lowerer.typeOf(optsNode));
if (!t || !(t.kind === "dyn" || (t.kind === "record" && lowerer.dynConvertible(t)))) return null;
return lowerer.coerceToExpected(lowerer.lowerExpr(optsNode), DYN);
}
/** http.request(options[, cb]) / http.get(options[, cb]) — the client.
* The options argument must be an OBJECT LITERAL; the lowered keys are
* hostname/host, port, path, method, timeout, and headers (a nested
@@ -5902,6 +6097,40 @@ function lowerHttpClientCall(
);
}
const optsNode = args[0]!;
const runtimeOptions =
binding === null ? runtimeRequestOptions(lowerer, optsNode, secureish) : null;
if (runtimeOptions !== null) {
// The options record is read at run time (the ClientRequest
// constructor's walk in the runtime): a RequestOptions variable, or a
// literal that spreads one or sets socketPath.
const autoEnd = boolLit(member === "get", loc);
const secureDyn = secure === true;
if (args.length === 1) {
return {
kind: "libCall",
fn: secureDyn ? "https.requestDyn" : "http.requestDyn",
args: [runtimeOptions, autoEnd],
type: HTTPCLIENTREQ_T,
loc,
};
}
const { cb } = lowerCallbackArg(
lowerer,
args[1]!,
"response callbacks",
1,
(p) => p.kind === "httpReq",
"use (res) or ()",
[HTTPREQ_T],
);
return {
kind: "libCall",
fn: secureDyn ? "https.requestDynCb" : "http.requestDynCb",
args: [runtimeOptions, autoEnd, cb],
type: HTTPCLIENTREQ_T,
loc,
};
}
if (!ts.isObjectLiteralExpression(optsNode)) {
const t = lowerer.mapTypeOf(lowerer.typeOf(optsNode));
if ((t?.kind === "string" || t?.kind === "url") && binding === null) {
@@ -7182,6 +7411,27 @@ function lowerHttpReqMethodCall(
const event = evT.isStringLiteralType() ? evT.value : null;
const receiver = coerceToHandle(lowerer, access.expression, HTTPREQ_T);
if (event === "data") {
const sigs = lowerer.checker.getCallSignatures(lowerer.typeOf(args[1]!));
const firstParam = sigs.length === 1 ? sigs[0]!.getParameters()[0] : undefined;
const param =
firstParam !== undefined
? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(firstParam))
: null;
if (param !== null && param.kind === "string") {
// A chunk typed string (the setEncoding('utf8') shape), as on a
// socket: the listener takes the chunk through a checked
// boundary, so a string arrives after setEncoding and a Buffer
// without it fails the check instead of being decoded.
const boxed = lowerSocketDynListener(lowerer, args[1]!, "data listeners");
const cb: IrExpr = { kind: "dynCheck", value: boxed, type: funcOf([DYN], VOID), loc };
return {
kind: "libCall",
fn: "http.reqOnData",
args: [receiver, cb, once],
type: VOID,
loc,
};
}
const { cb } = lowerCallbackArg(
lowerer,
args[1]!,
@@ -7744,6 +7994,30 @@ function lowerHttpResMethodCall(
if (cbArg !== null) return endWithCb(fn, [receiver, data], cbArg);
return { kind: "libCall", fn, args: [receiver, data], type: VOID, loc };
}
// end(payload ? text : undefined): an absent chunk ends without data,
// as end() does. The checked-dynamic end takes each arm at run time.
if (name === "end" && data.type.kind === "union" && lowerer.dynConvertible(data.type)) {
const arms = lowerer.unions.get(data.type.unionId)?.arms ?? [];
if (
arms.every(
(arm) =>
arm.kind === "string" ||
(arm.kind === "bytes" && arm.elem === "u8") ||
arm.kind === "undefinedT" ||
arm.kind === "nullT",
)
) {
const dynData: IrExpr = { kind: "dynFrom", value: data, type: DYN, loc: locOf(dataNode) };
if (cbArg !== null) return endWithCb("http.resEndDyn", [receiver, dynData], cbArg);
return {
kind: "libCall",
fn: "http.resEndDyn",
args: [receiver, dynData],
type: VOID,
loc,
};
}
}
lowerer.noLowering(
`${name} of '${lowerer.fmt(data.type)}' data`,
dataNode ?? call,
@@ -1466,7 +1466,8 @@ export function lowerStaticFetchCompanionCall(
}
const root = call.expression.expression;
const symbol = lowerer.resolveValueSymbol(root);
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
if ((!symbol || !lowerer.isStdlibSymbol(symbol)) && !isStreamWebReadableStream(lowerer, root))
return null;
const loc = locOf(call);
if (root.text === "AbortSignal") {
noteEventListeners(lowerer, loc);
@@ -1879,6 +1880,12 @@ export function lowerAbortControllerNew(lowerer: Lowerer, expr: ts.NewExpression
};
}
/** An import binding of node:stream/web's ReadableStream, which IS the
* global class (`import { ReadableStream } from "node:stream/web"`). */
function isStreamWebReadableStream(lowerer: Lowerer, ident: ts.Identifier): boolean {
return ident.text === "ReadableStream" && lowerer.isStdlibGlobal(ident, "ReadableStream");
}
export function lowerStaticReadableStreamNew(
lowerer: Lowerer,
expr: ts.NewExpression,
@@ -1887,7 +1894,11 @@ export function lowerStaticReadableStreamNew(
return null;
}
const symbol = lowerer.resolveValueSymbol(expr.expression);
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
if (
(!symbol || !lowerer.isStdlibSymbol(symbol)) &&
!isStreamWebReadableStream(lowerer, expr.expression)
)
return null;
const args = expr.arguments?.nodes ?? [];
if (args.length > 1) return null;
const loc = locOf(expr);
@@ -30,8 +30,13 @@ 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 {
lowerWebAssemblyCall,
lowerWebAssemblyNew,
refuseWebAssemblyUse,
} from "./builtins/webassembly.js";
import { discoverWasmModules, type WasmModuleReport } from "../../wasm/discover.js";
import { encodeBase64 } from "../../wasm/base64.js";
import { resolveRequireRuntime } from "../runtime-resolve.js";
import { lowerKeyObjectCall } from "./builtins/key-object.js";
import { fenceWebCryptoMember, lowerWebCryptoCall } from "./builtins/webcrypto.js";
@@ -185,6 +190,7 @@ import type {
IrGlobal,
IrLocal,
IrModule,
IrWasmModule,
IrParam,
IrRecordShape,
IrStmt,
@@ -1103,6 +1109,40 @@ export function lowerToIr(
diagnostics: [...result.diagnostics, ...contracts.diagnostics],
};
else if (contracts.count > 0) result = { ...result, contracts };
const discoverModules = (
onModule?: (sha256: string, bytes: Uint8Array) => void,
): WasmModuleReport[] =>
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;
},
...(onModule === undefined ? {} : { onModule }),
});
// A program that uses the WebAssembly API carries every module the build
// can see and translate (wasm/translate.ts); the rest fail at run time
// with the CompileError for untranslated bytes.
if (result.module !== null && resultLowerer.usesWebAssembly) {
const bytesByHash = new Map<string, Uint8Array>();
const reports = discoverModules((sha256, bytes) => bytesByHash.set(sha256, bytes));
const wasmModules: IrWasmModule[] = [];
for (const report of reports) {
const bytes = bytesByHash.get(report.sha256);
if (report.status !== "valid" || bytes === undefined) continue;
wasmModules.push({ sha256: report.sha256, bytes: encodeBase64(bytes) });
}
result.module.wasmModules = wasmModules;
timing("wasm-discovery", { modules: wasmModules.length });
}
if (options.coverage !== true) return result;
const remainder = new Lowerer(program, entry, moduleOrder, {
frontendServices: options.frontendServices,
@@ -1156,21 +1196,7 @@ 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;
},
});
const wasmModules = discoverModules();
timing("wasm-discovery", { modules: wasmModules.length });
if (wasmModules.length > 0) covered.wasmModules = wasmModules;
return covered;
@@ -1953,6 +1979,10 @@ export class Lowerer {
/** Lambdas lowered with checked-dynamic parameters (lambdaSignature):
* callbacks whose arguments are wider than their contextual overload. */
readonly dynParamLambdas = new Set<ts.Node>();
/** Whether some server in this program may listen on a Unix socket: a
* listen call whose first argument is a string. Computed once on first
* use (null until then); address() answers the path only when true. */
unixListenReachable: boolean | null = null;
/** IMPLICIT-ANY instances lowered EAGERLY at first demand (their return
* types are inferred from the body — the call site needs them settled),
* collected here for run()'s function list (the liftedFns discipline). */
@@ -2753,6 +2783,9 @@ export class Lowerer {
readonly workerTargets: readonly ts.SourceFile[];
readonly workerTargetIdByPath = new Map<string, number>();
usesWorkers = false;
/** The program uses the WebAssembly API (builtins/webassembly.ts): the
* build translates the modules it discovers. */
usesWebAssembly = false;
/** The program constructs a LifetimeRegion (lifetime-regions.ts). */
usesLifetimeRegions = false;
/** Per-directory answers of experimental-import.ts's declaration-file test. */
@@ -11950,6 +11983,8 @@ 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.
const webAssembly = lowerWebAssemblyCall(this, expr);
if (webAssembly !== null) return webAssembly;
refuseWebAssemblyUse(this, expr.expression);
return (
lowerLifetimeRegionCall(this, expr) ??
@@ -12178,6 +12213,8 @@ export class Lowerer {
}
lowerNew(expr: ts.NewExpression): IrExpr {
const webAssembly = lowerWebAssemblyNew(this, expr);
if (webAssembly !== null) return webAssembly;
refuseWebAssemblyUse(this, expr.expression, "new ");
const builtin =
lowerLifetimeRegionNew(this, expr) ??
@@ -12582,6 +12619,12 @@ export class Lowerer {
const callable = this.lowerBuiltinCallableValue(bi, loc);
if (callable) return callable;
}
// node:stream/web's exports are the web stream globals themselves: in
// JavaScript sources the same identity token as the global spelling
// (`require("stream/web").ReadableStream === globalThis.ReadableStream`).
if (bi.module === "stream/web" && isJsSourceFile(expr.getSourceFile())) {
return { kind: "strLit", value: `[builtin ${bi.member}]`, type: STRING, loc };
}
// A bare `fs.constants` read (not one of the baked bits above).
if (bi.member === "constants" && bi.module === "fs") return fsConstantsObject(loc);
if (builtinModuleFnOf(this, bi.module, bi.member)) {
@@ -24,6 +24,7 @@ import {
} from "../../ir/ir.js";
import { isJsSourceFile, isNodeTypesPath, requireSpecOf } from "../program.js";
import { ZLIB_CONSTANTS } from "./zlib-constants.js";
import { V8_MEMBER_REASON } from "../builtin-modules.js";
/** zlib stream instances: runtime Transform streams (scr_zlib_stream.c). */
const ZLIB_STREAM: IrType = { kind: "object", className: "%Transform" };
@@ -1908,6 +1909,18 @@ export const BUILTIN_MODULE_FNS: Record<
// special-cased); the keys exist for the same reason net/http's do.
dgram: {},
dns: {},
// node:dns/promises rides the same spoke (lookup's promise form).
"dns/promises": {},
// node:v8: getHeapStatistics answers allocator-derived figures in
// Node's shape (builtins/v8.ts — the entry carries the canonical
// shape); every other member fences with the by-design reason
// (builtinFenceHintOf).
v8: {
getHeapStatistics: { fn: "process.memoryUsageTake", params: [], result: VOID },
},
// node:stream/web re-exports the web stream globals (the same objects);
// its members lower through the globals' own paths.
"stream/web": {},
// node:assert lowers ENTIRELY through the assert spoke (lower-assert.ts
// — every call shape is special-cased: optional messages complete, the
// comparisons pick per-type libCalls, deep equality synthesizes
@@ -2811,7 +2824,11 @@ export const FS_CALLBACK_MEMBERS = new Set([
* inherited function into the diagnostic text). */
export function builtinFenceHintOf(module: string, member: string): string | undefined {
const mod = ownEntry(BUILTIN_MODULE_FENCE_HINTS, module);
return mod ? ownEntry(mod, member) : undefined;
const hint = mod ? ownEntry(mod, member) : undefined;
// node:v8's unlowered members observe the engine itself: the by-design
// reason, not a pending-surface hint (kept out of the hint table so the
// compatibility rows stay by-design rather than refused).
return hint === undefined && module === "v8" ? V8_MEMBER_REASON : hint;
}
/** BUILTIN_MODULE_CONSTS with the win32-target overrides applied — the
@@ -3441,6 +3458,10 @@ export function stdlibGlobalAliasNameOf(
"Number",
"String",
"Boolean",
// Code generation from strings throws Node's EvalError at every call
// (code-generation.ts); zod 4 probes `const F = Function; new F("")`.
"Function",
"eval",
].includes(name)
? name
: null;
+4 -1
View File
@@ -20,6 +20,7 @@ import {
trackedReadFile,
trackedRealpath,
} from "./input-tracker.js";
import { npmBuiltinAliasOf } from "./builtin-modules.js";
let activePackages: ReadonlySet<string> = new Set();
let declarationOverloads: ReadonlyMap<string, NpmStaticDeclarationOverloads> = new Map();
@@ -52,7 +53,9 @@ export function npmStaticLexerVisibleNames(path: string): readonly string[] {
}
export function setNpmStaticPackages<T extends Iterable<string>>(packages: T): void {
activePackages = new Set(packages);
// npm packages served as builtins (graceful-fs as fs) never compile
// their JavaScript, even when named.
activePackages = new Set([...packages].filter((name) => npmBuiltinAliasOf(name) === null));
declarationOverloads = new Map();
declarationProperties = new Map();
offenders.clear();
@@ -7,6 +7,7 @@ import { entryFunctionExports, type EntryExportInfo } from "./lib-exports.js";
import { entryContractFacts, type ContractFacts } from "./lib-contract.js";
import {
canonicalBuiltinModule,
npmBuiltinAliasOf,
checkPreflight,
isNodeTypesPath,
locOf,
@@ -198,6 +199,9 @@ function detectAutoPackages(
// never npm candidates. Auto keeps its original path (the
// @types/node answer skips them below), unchanged.
if (mode === "lib" && canonicalBuiltinModule(spec) !== null) continue;
// npm packages served as builtins (graceful-fs as fs) never compile
// their JavaScript.
if (npmBuiltinAliasOf(spec) !== null) continue;
// Explicit attempts name the runtime package. Types-first resolution
// may instead identify its @types twin, which has no runtime to admit.
if (judged?.has(packageNameOfSpecifier(spec))) continue;
+57
View File
@@ -101,8 +101,10 @@ import {
import {
builtinDefaultImportModule,
canonicalBuiltinModule,
npmBuiltinAliasOf,
SUPPORTED_NODE_MODULES,
unsupportedModuleFeatureOf,
TEMPORAL_GLOBAL,
} from "./builtin-modules.js";
import {
clearWorkspacePackages,
@@ -713,6 +715,42 @@ function entryPackageProgramRoots7(
return [...roots].sort();
}
/** TS 7's ESNext temporal declarations: a lib slice scriptc adds only for
* programs whose sources name the global (programNamesTemporal7). */
const TEMPORAL_LIB = "lib.esnext.temporal.d.ts";
/** True when a program source (user code or compiled npm JavaScript)
* contains the temporal global's name as a whole word. A cheap syntactic
* scan of the implementation files root discovery already materialized; a
* mention in a comment or string also counts, which only adds the lib
* slice. */
function programNamesTemporal7(program: ts.Program): boolean {
for (const sf of program.getImplementationSourceFiles()) {
const text = sf.text;
for (
let at = text.indexOf(TEMPORAL_GLOBAL);
at >= 0;
at = text.indexOf(TEMPORAL_GLOBAL, at + 8)
) {
if (!isIdentifierCharAt(text, at - 1) && !isIdentifierCharAt(text, at + 8)) return true;
}
}
return false;
}
function isIdentifierCharAt(text: string, index: number): boolean {
if (index < 0 || index >= text.length) return false;
const c = text.charCodeAt(index);
return (
(c >= 48 && c <= 57) ||
(c >= 65 && c <= 90) ||
(c >= 97 && c <= 122) ||
c === 36 ||
c === 95 ||
c > 127
);
}
/** "require" for a CommonJS edge (`require("x")`, `import x = require("x")`),
* "import" for every ESM form — the resolution kind preflight uses. */
function moduleSpecifierKind7(specifier: ts.StringLiteralLike): "import" | "require" {
@@ -792,6 +830,20 @@ function loadProgram7(
program = host.createProgram([...programRoots, overridesDtsPath()], options);
if (pass === 31) throw new Error("static program-root discovery did not converge");
}
// Node 24 ships no temporal global, but TS 7 types it in an ESNext
// lib slice. Programs that name it (feature probes such as smol-toml's
// typeof check) get that slice so they type-check, and lower to Node's
// answers (typeof "undefined", ReferenceError on use). Every other
// program keeps the identical lib set: the slice would add globals to
// the checker's world for everyone.
if (programNamesTemporal7(program)) {
program.dispose();
options = {
...options,
lib: [...((options.lib as string[] | undefined) ?? []), TEMPORAL_LIB],
};
program = host.createProgram([...programRoots, overridesDtsPath()], options);
}
const entry = program.getSourceFile(entryPath);
if (!entry) throw new Error(`could not load ${entryPath}`);
const externalTypeSpecifiersByFile = externalTypeFileClosure7(program, externalTypes);
@@ -5208,6 +5260,9 @@ function preflight7(load: LoadResult): {
continue;
}
}
// An npm package served as a builtin (graceful-fs as fs) is never
// a program-module edge, even when --npm-static names it.
if (npmBuiltinAliasOf(req.spec) !== null) continue;
const isRelative = isRelativeSpecifier(req.spec);
let dep = resolveImport7(program, sf, req.spec, "require");
if (dep === null && !isRelative) {
@@ -5271,6 +5326,7 @@ function preflight7(load: LoadResult): {
if (load.externalTypes.has(spec)) {
continue;
}
if (npmBuiltinAliasOf(spec) !== null) continue;
const projectDep = resolveImport7(program, sf, spec, "require");
if (projectDep !== null) {
// A bare require of a JSON document only parses it: nothing to
@@ -6046,6 +6102,7 @@ export { ambientDtsPath, fallbackDtsPath, isNodeTypesPath, overridesDtsPath } fr
export {
builtinDefaultImportModule,
canonicalBuiltinModule,
npmBuiltinAliasOf,
SUPPORTED_BUILTIN_MODULES,
} from "./builtin-modules.js";
@@ -1375,6 +1375,116 @@ function isLookupOptionsUnion(type: ts.Type, ctx: Pick<TypeMapperCtx, "isStdlibF
return all;
}
/** The binary and stream members of fetch's BodyInit (undici-types):
* NodeJS.ArrayBufferView expands to the typed arrays and DataView. */
const FETCH_BODY_MEMBERS: ReadonlySet<string> = new Set([
"ArrayBuffer",
"SharedArrayBuffer",
"AsyncIterable",
"Iterable",
"Blob",
"FormData",
"URLSearchParams",
"ReadableStream",
"DataView",
"Buffer",
"Int8Array",
"Uint8Array",
"Uint8ClampedArray",
"Int16Array",
"Uint16Array",
"Int32Array",
"Uint32Array",
"Float16Array",
"Float32Array",
"Float64Array",
"BigInt64Array",
"BigUint64Array",
]);
/** fetch's request body type, BodyInit, however TypeScript spells it
* (`RequestInit["body"]`, `NonNullable<...>`, an optional member): a
* union of strings, the stdlib binary and stream types and nullish
* values that includes both FormData and URLSearchParams. Native fetch
* reads such a body at run time, so it rides checked-dynamic storage. */
function isFetchBodyUnion(type: ts.Type, ctx: Pick<TypeMapperCtx, "isStdlibFile">): boolean {
if (!type.isUnionType()) return false;
let formData = false;
let searchParams = false;
for (const member of ts.constituentTypes(type)) {
if (member.flags & (ts.TypeFlags.StringLike | ts.TypeFlags.Null | ts.TypeFlags.Undefined))
continue;
const symbol = member.getSymbol();
if (
symbol === undefined ||
!FETCH_BODY_MEMBERS.has(symbol.name) ||
!symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile()))
)
return false;
if (symbol.name === "FormData") formData = true;
if (symbol.name === "URLSearchParams") searchParams = true;
}
return formData && searchParams;
}
/** A mapped object type (Omit, Pick, Partial) whose members all come from
* one stdlib interface that maps to the checked dynamic. */
function isDynInterfaceSubset(type: ts.Type, ctx: TypeMapperCtx): boolean {
if (!(type.flags & ts.TypeFlags.Object) || type.isClassOrInterface()) return false;
const alias = type.getAliasSymbol();
if (alias === undefined) return false;
const props = ctx.checker.getPropertiesOfType(type);
if (props.length === 0) return false;
let owner: ts.InterfaceDeclaration | undefined;
for (const prop of props) {
const decl = prop.declarations[0];
const parent = decl?.parent;
if (
parent === undefined ||
!ts.isInterfaceDeclaration(parent) ||
!ctx.isStdlibFile(parent.getSourceFile()) ||
(owner !== undefined && owner !== parent)
)
return false;
owner = parent;
}
return (
owner !== undefined && mapType(ctx.checker.getTypeAtLocation(owner.name), ctx)?.kind === "dyn"
);
}
/** fetch's HeadersInit (undici-types): a union of the stdlib Headers
* class with header-pair arrays and/or a header record, plus nullish
* values. Native fetch reads it at run time like the init around it. */
function isFetchHeadersUnion(type: ts.Type, ctx: Pick<TypeMapperCtx, "isStdlibFile">): boolean {
if (!type.isUnionType()) return false;
let headers = false;
let other = false;
for (const member of ts.constituentTypes(type)) {
if (member.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) continue;
const symbol = member.getSymbol();
if (
symbol?.name === "Headers" &&
symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile()))
) {
headers = true;
continue;
}
const alias = member.getAliasSymbol()?.name;
if (
symbol?.name === "Array" ||
symbol?.name === "ReadonlyArray" ||
alias === "Record" ||
alias === "HeaderRecord"
) {
other = true;
continue;
}
return false;
}
return headers && other;
}
function isReceiveMessageResult(
type: ts.Type,
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
@@ -1542,6 +1652,25 @@ function classExprNeedsContext(decl: ts.ClassLikeDeclaration): boolean {
return false;
}
/** True for the type of a TypeScript object literal with no contextual
* type (`const options = { ...location, path }`) that spreads a value
* mapping to the checked dynamic (a stdlib options type such as
* http.RequestOptions). A literal with a contextual type is built in that
* type's representation instead, and JavaScript literals keep their own
* rules. */
function spreadsCheckedDynamic(symbol: ts.Symbol | undefined, ctx: TypeMapperCtx): boolean {
if (symbol === undefined || (symbol.flags & ts.SymbolFlags.ObjectLiteral) === 0) return false;
const decl = symbol.declarations[0];
if (decl === undefined || !ts.isObjectLiteralExpression(decl)) return false;
if (isJsSourceFile(decl.getSourceFile())) return false;
if (ctx.checker.getContextualType(decl)) return false;
return decl.properties.nodes.some(
(prop) =>
ts.isSpreadAssignment(prop) &&
mapType(ctx.checker.getTypeAtLocation(prop.expression), ctx)?.kind === "dyn",
);
}
function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
const { checker, unions, classNamer, resolveTypeParam } = ctx;
// TypeScript's intrinsic globalThis symbol has no declaration. A stored
@@ -1562,9 +1691,20 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
const moduleId = ctx.moduleNamespaceId?.(type) ?? null;
if (moduleId !== null) return { kind: "moduleNs", moduleId };
}
// A TypeScript object literal that spreads a checked-dynamic value
// (`{ ...requestOptions, path, timeout }`): its keys are the source's
// runtime keys, which no record shape holds, so the literal rides
// checked-dynamic storage like its source.
if (spreadsCheckedDynamic(intrinsic, ctx)) return DYN;
// worker_threads.receiveMessageOnPort's `{ message: any } | undefined`:
// the runtime's { message } object, a checked-dynamic value.
if (isReceiveMessageResult(type, ctx)) return DYN;
if (isFetchBodyUnion(type, ctx) || isFetchHeadersUnion(type, ctx)) return DYN;
// Omit/Pick/Partial of a stdlib interface that rides checked-dynamic
// storage (`Omit<RequestInit, "body" | "headers">`): every member comes
// from that interface, so the value is a subset of the same runtime-read
// options object and rides the same storage.
if (isDynInterfaceSubset(type, ctx)) return DYN;
// The lookup-function option union (`LookupOneOptions | LookupAllOptions
// | number | undefined`): with a LookupAllOptions arm it is an option bag
// dns.lookup reads at run time, so it rides checked-dynamic storage.
@@ -1604,6 +1744,18 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// 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;
// JavaScript inference merges WebAssembly.instantiate's overloads into
// `Promise<WebAssemblyInstantiatedSource> & Promise<Instance>`: a promise
// of a WebAssembly value either way.
if (
type.isIntersectionType() &&
ts.constituentTypes(type).every((t) => {
if (t.getSymbol()?.name !== "Promise") return false;
const arg = ctx.checker.getTypeArguments(t as ts.TypeReference)[0];
return arg !== undefined && isWebAssemblyType(arg, ctx);
})
)
return { kind: "promise", inner: DYN };
// Immutable<T> (@scriptc/types) is a deep-readonly view: the value is
// still the T it maps.
const immutableSource = immutableSourceType(type, ctx);
@@ -3873,6 +4025,34 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
),
};
}
// v8.getHeapStatistics()'s record, in Node's key order (builtins/v8.ts).
if (isStdlibInterface("HeapInfo")) {
const names = [
"total_heap_size",
"total_heap_size_executable",
"total_physical_size",
"total_available_size",
"used_heap_size",
"heap_size_limit",
"malloced_memory",
"peak_malloced_memory",
"does_zap_garbage",
"number_of_native_contexts",
"number_of_detached_contexts",
"total_global_handles_size",
"used_global_handles_size",
"external_memory",
];
return {
kind: "record",
shapeId: ctx.shapes.intern(
[...names].sort().map((name) => ({ name, type: F64 })),
false,
undefined,
names,
),
};
}
// process.cpuUsage()/threadCpuUsage()'s {user, system} microsecond
// record and resourceUsage()'s getrusage record — stdlib data
// interfaces interned explicitly (the RemoteInfo pattern; declared
+4
View File
@@ -535,6 +535,7 @@ async function compileExecutableNative(
fileHandle: features.fileHandle,
fetch: features.fetch,
...(features.eventTarget ? { eventTarget: true } : {}),
...(features.wasm ? { wasm: true } : {}),
...(features.warningReplace ? { warningReplace: true } : {}),
...(features.stdioExtended ? { stdioExtended: true } : {}),
zlib: features.zlib,
@@ -556,6 +557,7 @@ async function compileExecutableNative(
http: features.http,
http2: features.http2,
dgram: features.dgram,
dnsPromises: features.dnsPromises,
watch: features.watch,
foreignFfi: features.foreignFfi,
...(features.workers ? { workers: true } : {}),
@@ -729,6 +731,7 @@ async function prepareExecutableInput(
wasi: buildPlatform === "wasi",
runtimeAbiMarker: outputKind === "obj",
objectAudit: opts.sanitize === true,
...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}),
...inlineRcOption(opts.optimization, opts.sanitize === true),
});
} catch (err) {
@@ -794,6 +797,7 @@ async function prepareExecutableInput(
wasi: buildPlatform === "wasi",
runtimeAbiMarker: useRuntimePack,
objectAudit: opts.sanitize === true,
...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}),
...inlineRcOption(opts.optimization, opts.sanitize === true),
...(opts.detachedExit === true ? { detachedExit: true } : {}),
});
+2 -2
View File
@@ -58,10 +58,10 @@ export function dynUndefinedExpr(loc: SrcLoc): IrExpr {
}
/** An always-throwing Node error with the replaced expression's type.
* Kinds: 0 Error, 1 TypeError, 2 RangeError, 5 ReferenceError. An empty
* Kinds: 0 Error, 1 TypeError, 2 RangeError, 5 ReferenceError, 6 EvalError. An empty
* code omits the error code property. */
export function nodeThrowExpr(
kind: 0 | 1 | 2 | 5,
kind: 0 | 1 | 2 | 5 | 6,
code: string,
message: string,
type: IrType,
@@ -133,6 +133,12 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"blob.new",
"blob.fileNew",
"blob.slice",
"wasm.compile",
"wasm.validate",
"wasm.instantiate",
"wasm.moduleNew",
"wasm.instanceNew",
"wasm.errorNew",
"events.addAbortListener",
"fetch.streamNew",
"fetch.streamFrom",
@@ -263,6 +269,14 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
// ("Invalid URL") or a non-http scheme (ERR_INVALID_PROTOCOL).
"http.requestUrl",
"http.requestUrlCb",
// The runtime options walk throws Node's validation errors and the
// fence for documented options it does not implement.
"http.requestDyn",
"http.requestDynCb",
"https.requestDyn",
"https.requestDynCb",
// address() of a Unix-socket server read as the AddressInfo record.
"net.serverAddressCheck",
// The h2 client's throwing entries: connect on a bad/non-http
// authority (ERR_INVALID_URL / ERR_INVALID_PROTOCOL), request on a
// closed/destroyed session (ERR_HTTP2_INVALID_SESSION), setEncoding on
@@ -795,6 +809,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
// Node's argument checks (ERR_INVALID_ARG_TYPE/VALUE, ERR_SOCKET_BAD_PORT).
"dns.lookupDyn",
"dns.lookupService",
"dns.promisesLookup",
"dns.promisesLookupService",
"dns.setDefaultResultOrder",
"net.connect",
"net.connectCb",
@@ -108,6 +108,27 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"blob.bytes": { argTypes: [DYN], result: { kind: "promise", inner: BYTES_U8 } },
"blob.arrayBuffer": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"blob.is": { argTypes: [DYN, F64], result: BOOL },
/** WebAssembly (scr_wasm.c): modules translated at build time
* (wasm/translate.ts), found at run time by the SHA-256 of their bytes.
* compile and instantiate over bytes settle on a later loop turn;
* instantiate over a Module settles now, as in Node. `wasm.is` answers
* instanceof for Module, Instance, Memory, Table and Global (kinds 0-4)
* and CompileError, LinkError and RuntimeError (5-7); `wasm.errorNew`
* constructs one of the error classes (kind 5-7, message, options). */
"wasm.compile": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"wasm.validate": { argTypes: [DYN], result: BOOL },
"wasm.instantiate": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
"wasm.moduleNew": { argTypes: [DYN], result: DYN },
"wasm.instanceNew": { argTypes: [DYN, DYN], result: DYN },
"wasm.is": { argTypes: [DYN, F64], result: BOOL },
"wasm.errorNew": { argTypes: [F64, DYN, DYN], result: DYN },
/** WebAssembly.compile (0), instantiate (1) and validate (2) as function
* values, and the namespace object holding them. */
"wasm.function": { argTypes: [F64], result: DYN },
"wasm.namespace": { argTypes: [], result: DYN },
/** The global object viewed through a cast to a type with a WebAssembly
* member: an object holding the namespace. */
"wasm.globalView": { argTypes: [], result: DYN },
"abort.timeout": { argTypes: [DYN], result: DYN },
"abort.now": { argTypes: [DYN], result: DYN },
"abort.any": { argTypes: [DYN], result: DYN },
@@ -1280,6 +1301,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"net.createServer": { argTypes: [], result: NETSERVER_T },
"net.createServerCb": { argTypes: [null], result: NETSERVER_T },
"net.listen": { argTypes: [NETSERVER_T, F64], result: VOID },
/** listen(path[, cb]): a Unix-socket server; the socket file is removed
* when the server closes. Failures are the async 'error'. */
"net.listenPath": { argTypes: [NETSERVER_T, STRING], result: VOID },
"net.listenPathCb": {
argTypes: [NETSERVER_T, STRING, { kind: "func", params: [], ret: VOID }],
result: VOID,
},
"net.listenCb": {
argTypes: [NETSERVER_T, F64, { kind: "func", params: [], ret: VOID }],
result: VOID,
@@ -1322,6 +1350,15 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* answers null (the serverPort stance). */
"net.serverAddress": { argTypes: [NETSERVER_T], result: VOID },
"net.serverClose": { argTypes: [NETSERVER_T], result: VOID },
/** server.close((err) => ...): the callback (a dyn function) gets
* undefined once the server closes, or Node's ERR_SERVER_NOT_RUNNING
* when it was not listening. */
"net.serverCloseDyn": { argTypes: [NETSERVER_T, DYN], result: VOID },
/** server.address() in programs that may listen on a Unix socket: the
* path a listening server answers ("" for a TCP server), and the check
* a record-typed address() makes first (throws for a Unix server). */
"net.serverUnixPath": { argTypes: [NETSERVER_T], result: STRING },
"net.serverAddressCheck": { argTypes: [NETSERVER_T], result: VOID },
"net.serverCloseCb": {
argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }],
result: VOID,
@@ -1490,6 +1527,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* callback) and lookupService(address, port, callback). */
"dns.lookupDyn": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID },
"dns.lookupService": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID },
/** node:dns/promises: lookup(hostname, options) and lookupService(address,
* port) over the callback forms; the promise fulfills with the dyn
* { address, family } (array for all: true) / { hostname, service }.
* Argument errors throw synchronously (may-throw), unlike the
* util.promisify forms below. */
"dns.promisesLookup": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
"dns.promisesLookupService": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
"dns.getServers": { argTypes: [], result: DYN },
"dns.getDefaultResultOrder": { argTypes: [], result: STRING },
"dns.setDefaultResultOrder": { argTypes: [DYN], result: VOID },
@@ -1909,6 +1953,17 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* non-http scheme. */
"http.requestUrl": { argTypes: [STRING, STRING, BOOL], result: HTTPCLIENTREQ_T },
"http.requestUrlCb": { argTypes: [STRING, STRING, BOOL, null], result: HTTPCLIENTREQ_T },
/** request/get with a runtime options record: a RequestOptions
* variable, or a literal that spreads one or sets socketPath. Args
* (options, autoEnd[, cb]). The runtime reads the record in the
* ClientRequest constructor's order (host/port or socketPath, path,
* method, headers, timeout, agent, signal, maxHeaderSize) with Node's
* errors, and documented options it does not implement throw the
* runtime fence. The "https." pair adds the TLS options. MAY THROW. */
"http.requestDyn": { argTypes: [DYN, BOOL], result: HTTPCLIENTREQ_T },
"http.requestDynCb": { argTypes: [DYN, BOOL, null], result: HTTPCLIENTREQ_T },
"https.requestDyn": { argTypes: [DYN, BOOL], result: HTTPCLIENTREQ_T },
"https.requestDynCb": { argTypes: [DYN, BOOL, null], result: HTTPCLIENTREQ_T },
/** The paused-mode demux surface (portless's first-byte TLS peek:
* once('readable') + read(1) + unshift + emit('connection')).
* sockOnReadable registers a zero-param listener (a consumer: arrived
@@ -1954,6 +2009,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** Listeners with a parameter, as dyn functions: close(hadError),
* write's cb(err) and end's cb. */
"net.sockOnCloseDyn": { argTypes: [NETSOCKET_T, DYN, BOOL], result: VOID },
/** socket.on/once('lookup', listener): the listener (a dyn function)
* gets (err, address, family, host) once per resolved address, or once
* with the lookup error, before the connect. */
"net.sockOnLookup": { argTypes: [NETSOCKET_T, DYN, BOOL], result: VOID },
"net.sockOnWriteFlush": {
argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }],
result: VOID,
+1 -1
View File
@@ -238,7 +238,7 @@ interface FunctionFacts {
hoisted: Map<string, IrStmt>;
/** Bindings called through `callValue` without a receiver. */
callees: Set<string>;
/** Temporal-dead-zone bindings: the statement list holding both the
/** TDZ (temporal dead zone) bindings: the statement list holding both the
* empty `varDecl` and the initializing `assign`, with their indexes. */
tdzInits: Map<string, { body: readonly IrStmt[]; declared: number; initialized: number }>;
classCaptured: Set<string>;
@@ -212,6 +212,8 @@ const MODULE_FIELDS: Record<StringFields<Omit<IrModule, "functions">>, Role> = {
"contracts.acyclic[]": "other",
"contracts.immortalGlobals[]": "other",
"contracts.programLifetime[]": "other",
"wasmModules[].sha256": "other",
"wasmModules[].bytes": "other",
"threadSlotKeys[]": "other",
};
+14
View File
@@ -1044,6 +1044,20 @@ export interface IrModule {
* write in the program (ir/contract-check.ts). Backends may rely on them
* unconditionally. Absent when the program writes no contract. */
contracts?: IrContracts;
/** The WebAssembly modules translated into this program (wasm/
* translate.ts), present exactly when the program uses the WebAssembly
* API (possibly empty). The backend appends each module's translation
* and emits the registry the runtime looks modules up in. */
wasmModules?: IrWasmModule[];
}
/** A WebAssembly module whose bytes the build saw, validated within the
* translatable feature level. */
export interface IrWasmModule {
/** Hex SHA-256 of the bytes. */
sha256: string;
/** The bytes, base64. */
bytes: string;
}
/** Verified contract facts, keyed by IR names. */
+23 -3
View File
@@ -131,6 +131,10 @@ const LEGACY_TEXT_DECODER_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
/** Signal-taking readline, stream and timers/promises entry points, which register abort listeners. */
const ABORT_SIGNAL_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"http.clientSignal",
"http.requestDyn",
"http.requestDynCb",
"https.requestDyn",
"https.requestDynCb",
"rl.createSignal",
"rl.questionAsyncSignal",
"sp.finishedOptsSignal",
@@ -463,7 +467,12 @@ const RUNTIME_FEATURE_RULES = {
* queueMicrotask scheduling goes through the runtime's *_als entry
* points, which carry the active stores into each callback. */
asyncContext: rule({ fns: [...ASYNC_CONTEXT_LIB_FNS] }),
bigint: rule({ fns: ["process.hrtimeValue"], prefixes: ["bigint."], types: ["bigint"] }),
/** scr_wasm.c converts i64 values through BigInt. */
bigint: rule({
fns: ["process.hrtimeValue"],
prefixes: ["bigint.", "wasm."],
types: ["bigint"],
}),
childProcess: rule({
fns: [...CHILD_PROCESS_LIB_FNS],
prefixes: CHILD_PROCESS_LIB_PREFIXES,
@@ -481,6 +490,8 @@ const RUNTIME_FEATURE_RULES = {
dc: rule({ prefixes: ["dc."] }),
/** dns.lookup lives in the dgram unit. */
dgram: rule({ prefixes: ["dgram.", "dns."], types: ["dgramSocket"] }),
/** node:dns/promises: scr_dns_promises.c over the dgram unit. */
dnsPromises: rule({ fns: ["dns.promisesLookup", "dns.promisesLookupService"] }),
dynAsync: rule({
initial: (mod) =>
mod.workers === true ||
@@ -488,14 +499,15 @@ const RUNTIME_FEATURE_RULES = {
(fn) => fn.async === true && fn.generator === undefined && fn.returnType.kind === "dyn",
),
// The legacy url functions and crypto.createCipheriv emit their
// deprecation warnings through the dyn-async unit.
// deprecation warnings through the dyn-async unit; scr_wasm.c boxes its
// handles and promises through it.
fns: [
...DYN_ASYNC_LIB_FNS,
...READABLE_DYN_ASYNC_LIB_FNS,
...LEGACY_URL_LIB_FNS,
"crypto.cipherNew",
],
prefixes: READABLE_DYN_ASYNC_PREFIXES,
prefixes: [...READABLE_DYN_ASYNC_PREFIXES, "wasm."],
expr: (node, tables) =>
(node.kind === "awaitExpr" && node.type.kind === "dyn") ||
// Rejection reasons cross as dyn values (allSettled's records, any's
@@ -539,6 +551,9 @@ const RUNTIME_FEATURE_RULES = {
fetch: rule({ prefixes: ["fetch."] }),
fileHandle: rule({ types: ["fileHandle"] }),
fsWatch: rule({ prefixes: ["fs.watch", "watcher."], types: ["fsWatcher"] }),
/** A socket 'lookup' listener: the C main installs the net unit's
* lookup records (scr_net_lookup_install). */
netLookup: rule({ fns: ["net.sockOnLookup"] }),
http: rule({
prefixes: ["http.", "tls.", "https.", "http2."],
types: ["httpReq", "httpRes", "httpClientReq", "secureCtx"],
@@ -631,6 +646,8 @@ const RUNTIME_FEATURE_RULES = {
tlsCa: rule({ prefixes: ["tlsca."] }),
/** process.emitWarning is assigned (process.setEmitWarning). */
warningReplace: rule({ fns: ["process.setEmitWarning"] }),
/** The WebAssembly API (scr_wasm.c). */
wasm: rule({ prefixes: ["wasm."] }),
/** crypto.subtle importKey/sign/verify/digest and CryptoKey:
* scr_webcrypto.c. */
webcrypto: rule({ prefixes: ["webcrypto."] }),
@@ -656,6 +673,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
copying: false,
dc: false,
dgram: false,
dnsPromises: false,
dynAsync: false,
dynInvoke: false,
dynTier: false,
@@ -664,6 +682,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
fetch: false,
fileHandle: false,
fsWatch: false,
netLookup: false,
http: false,
http2: false,
inspect: false,
@@ -682,6 +701,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
tls: false,
tlsCa: false,
warningReplace: false,
wasm: false,
webcrypto: false,
zlib: false,
};
+10 -6
View File
@@ -4427,7 +4427,9 @@ function validateFunction(
e.fn === "http.clientOnResponse" ||
e.fn === "http.requestAgentCb" ||
e.fn === "https.requestAgentCb" ||
e.fn === "https.requestUrlCb"
e.fn === "https.requestUrlCb" ||
e.fn === "http.requestDynCb" ||
e.fn === "https.requestDynCb"
) {
// The response listener: void, no params or exactly (res: httpReq).
const cbT =
@@ -4438,11 +4440,13 @@ function validateFunction(
? 9
: e.fn === "http.requestUrlCb" || e.fn === "https.requestUrlCb"
? 3
: e.fn === "http.requestAgentCb"
? 8
: e.fn === "https.requestAgentCb"
? 10
: 1
: e.fn === "http.requestDynCb" || e.fn === "https.requestDynCb"
? 2
: e.fn === "http.requestAgentCb"
? 8
: e.fn === "https.requestAgentCb"
? 10
: 1
]?.type;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 1;
if (ok && cbT?.kind === "func" && cbT.params.length === 1) {
+1
View File
@@ -95,6 +95,7 @@ async function main(): Promise<number> {
http: false,
http2: false,
dgram: false,
dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
+1
View File
@@ -149,6 +149,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures {
"http",
"http2",
"dgram",
"dnsPromises",
"watch",
"foreignFfi",
"nodeTest",
@@ -75,6 +75,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
workers: false,
youngNursery: false,
eventTarget: false,
wasm: false,
dynTier: false,
warningReplace: false,
stdioExtended: false,
@@ -101,6 +102,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
http: false,
http2: false,
dgram: false,
dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
+61
View File
@@ -0,0 +1,61 @@
/* Base64 for WebAssembly module bytes: decoding inline modules found in
* source text (discover.ts) and carrying translated modules on the IR
* (IrModule.wasmModules). Plain TypeScript, so the native compiler
* compiles it too. */
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);
}
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/** Standard base64 with padding. */
export function encodeBase64(bytes: Uint8Array): string {
const parts: string[] = [];
let chunk = "";
for (let i = 0; i < bytes.length; i += 3) {
const a = bytes[i]!;
const b = i + 1 < bytes.length ? bytes[i + 1]! : 0;
const c = i + 2 < bytes.length ? bytes[i + 2]! : 0;
const n = (a << 16) | (b << 8) | c;
chunk += BASE64_ALPHABET[(n >> 18) & 63]! + BASE64_ALPHABET[(n >> 12) & 63]!;
chunk += i + 1 < bytes.length ? BASE64_ALPHABET[(n >> 6) & 63]! : "=";
chunk += i + 2 < bytes.length ? BASE64_ALPHABET[n & 63]! : "=";
if (chunk.length >= 65536) {
parts.push(chunk);
chunk = "";
}
}
parts.push(chunk);
return parts.join("");
}
+7 -16
View File
@@ -20,6 +20,7 @@
* The decoder never evaluates anything and keeps no module contents beyond
* the counts and the feature set it reports. It is plain TypeScript that
* the native compiler also compiles. */
import { readMemargFlags } from "./module.js";
/** Proposals the summary can name, in report order. The first group is the
* translatable set; the rest are reported as unsupported. */
@@ -1244,19 +1245,9 @@ class FunctionValidator {
/** A memarg. A memory other than 0 stops typing (multi-memory). */
memarg(naturalAlign: number): void {
const r = this.r;
let align = r.u32();
let multi = false;
if (align >= 64 && align < 128) {
align -= 64;
multi = true;
} else if (align >= 64) {
r.malformed("malformed memop flags");
}
let memory = 0;
if (multi) {
memory = r.u32();
if (memory !== 0) this.otherMemory(memory);
}
const { align, memory } = readMemargFlags(r);
if (align < 0) r.malformed("malformed memop flags");
if (memory !== 0) this.otherMemory(memory);
if (this.m.memory64) r.u64();
else r.u32();
if (!this.typed) return;
@@ -1840,9 +1831,9 @@ class FunctionValidator {
scanMemarg(): void {
const r = this.r;
const align = r.u32();
if (align >= 128) r.malformed("malformed memop flags");
if (align >= 64 && r.u32() !== 0) this.m.feature("multi-memory");
const { align, memory } = readMemargFlags(r);
if (align < 0) r.malformed("malformed memop flags");
if (memory !== 0) this.m.feature("multi-memory");
if (this.m.memory64) r.u64();
else r.u32();
}
+7 -34
View File
@@ -28,6 +28,9 @@ import {
trackedReadFile,
} from "../frontend/input-tracker.js";
import { analyzeWasm, isWasmHeader, type WasmModuleSummary } from "./binary.js";
import { decodeBase64 } from "./base64.js";
export { decodeBase64 };
export type WasmSourceKind = "path" | "inline" | "package" | "config";
@@ -56,6 +59,9 @@ export interface WasmDiscoveryInput {
configured: readonly string[];
/** require.resolve from `fromFile`, or null when it does not resolve. */
resolveRequest: (fromFile: string, specifier: string) => string | null;
/** Receives each distinct module's bytes once, when it is first found
* (builds translate them). */
onModule?: (sha256: string, bytes: Uint8Array) => void;
}
const SCAN_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
@@ -110,6 +116,7 @@ class Discovery {
};
this.byHash.set(sha256, report);
this.order.push(sha256);
this.input.onModule?.(sha256, bytes);
}
if (report.path === undefined && path !== undefined) report.path = path;
addSource(report, source);
@@ -351,40 +358,6 @@ function byteArrayLiteral(node: ts.ArrayLiteralExpression): Uint8Array | null {
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 {
+675
View File
@@ -0,0 +1,675 @@
/* The structure of a WebAssembly module, for the translator.
*
* binary.ts validates a module and keeps nothing but counts. This parser
* reads the parts translation needs (types, imports, functions, tables,
* memories, globals, exports, the start function, element and data
* segments, function bodies and custom sections) from bytes that binary.ts
* already accepted, so it checks nothing beyond what it must to stay in
* bounds. Constant expressions and function bodies are kept as byte ranges
* that the translator reads itself.
*
* i64 values never become JavaScript numbers: the translator reads their
* LEB128 encodings into two 32-bit halves. Plain TypeScript, so the native
* compiler compiles it too. */
export const VAL_I32 = 0x7f;
export const VAL_I64 = 0x7e;
export const VAL_F32 = 0x7d;
export const VAL_F64 = 0x7c;
export const VAL_FUNCREF = 0x70;
export const VAL_EXTERNREF = 0x6f;
export const KIND_FUNC = 0;
export const KIND_TABLE = 1;
export const KIND_MEMORY = 2;
export const KIND_GLOBAL = 3;
export interface WasmFuncType {
params: number[];
results: number[];
}
/** A byte range of the module: a constant expression (through its `end`
* opcode) or a payload. */
export interface WasmRange {
start: number;
end: number;
}
export interface WasmImport {
module: string;
name: string;
kind: number;
/** Functions: the type index. Tables and globals: the value type. */
type: number;
min: number;
max: number | null;
mutable: boolean;
}
export interface WasmTable {
elemType: number;
min: number;
max: number | null;
imported: boolean;
/** A defined table's initializer (reference-types table with an init
* expression), or null for the null reference. */
init: WasmRange | null;
}
export interface WasmMemory {
min: number;
max: number | null;
imported: boolean;
}
export interface WasmGlobal {
type: number;
mutable: boolean;
imported: boolean;
init: WasmRange | null;
}
export interface WasmExport {
name: string;
kind: number;
index: number;
}
export interface WasmElem {
mode: "active" | "passive" | "declarative";
table: number;
offset: WasmRange | null;
elemType: number;
/** One initializer per element: a function index (>= 0), or -1 when the
* element is a constant expression in `exprs`. */
funcs: number[];
exprs: (WasmRange | null)[];
}
export interface WasmData {
mode: "active" | "passive";
memory: number;
offset: WasmRange | null;
payload: WasmRange;
}
export interface WasmCode {
/** Declared locals, expanded (not including parameters). */
locals: number[];
body: WasmRange;
}
export interface WasmCustom {
name: string;
payload: WasmRange;
}
export interface WasmModule {
bytes: Uint8Array;
types: WasmFuncType[];
imports: WasmImport[];
/** The type index of every function, imported functions first. */
funcTypes: number[];
importedFuncs: number;
tables: WasmTable[];
memories: WasmMemory[];
globals: WasmGlobal[];
exports: WasmExport[];
start: number | null;
elems: WasmElem[];
datas: WasmData[];
codes: WasmCode[];
customs: WasmCustom[];
/** Functions referenced outside a body (exports, element segments,
* ref.func in constant expressions): only these need a function
* reference record. Bodies add their own ref.func targets, which a valid
* module also declares in one of these places. */
declaredRefs: Set<number>;
}
/** A byte cursor over a module that binary.ts validated. */
export class WasmCursor {
bytes: Uint8Array;
pos: number;
constructor(bytes: Uint8Array, pos: number) {
this.bytes = bytes;
this.pos = pos;
}
u8(): number {
const b = this.bytes[this.pos];
if (b === undefined) throw new Error("wasm: unexpected end of module");
this.pos++;
return b;
}
u32(): number {
let result = 0;
let scale = 1;
for (;;) {
const b = this.u8();
result += (b & 0x7f) * scale;
if ((b & 0x80) === 0) return result;
scale *= 128;
}
}
/** A signed LEB128 of at most 33 bits. */
s33(): number {
let result = 0;
let scale = 1;
for (;;) {
const b = this.u8();
result += (b & 0x7f) * scale;
scale *= 128;
if ((b & 0x80) === 0) {
if (b & 0x40) result -= scale;
return result;
}
}
}
/** A signed 32-bit LEB128 as a signed 32-bit number. */
s32(): number {
return this.s33() | 0;
}
/** A signed 64-bit LEB128 as its two's-complement halves [lo, hi], each
* an unsigned 32-bit number. */
s64(): [number, number] {
let lo = 0;
let hi = 0;
let shift = 0;
let b = 0;
do {
b = this.u8();
const bits = b & 0x7f;
if (shift < 32) {
lo = (lo | (bits << shift)) >>> 0;
if (shift > 25) hi = (hi | (bits >>> (32 - shift))) >>> 0;
} else if (shift < 64) {
hi = (hi | (bits << (shift - 32))) >>> 0;
}
shift += 7;
} while (b & 0x80);
if (shift < 64 && b & 0x40) {
// Sign-extend from bit `shift`.
if (shift < 32) {
lo = (lo | (~0 << shift)) >>> 0;
hi = 0xffffffff;
} else {
hi = (hi | (~0 << (shift - 32))) >>> 0;
}
}
return [lo, hi];
}
u32le(): number {
const b = this.bytes;
const p = this.pos;
this.pos += 4;
return (b[p]! | (b[p + 1]! << 8) | (b[p + 2]! << 16) | (b[p + 3]! << 24)) >>> 0;
}
name(): string {
const length = this.u32();
const start = this.pos;
this.pos += length;
return decodeUtf8(this.bytes, start, this.pos);
}
/** Skips a constant expression through its `end` and answers its range. */
constExpr(): WasmRange {
const start = this.pos;
for (;;) {
const op = this.u8();
if (op === 0x0b) break;
skipImmediates(this, op);
}
return { start, end: this.pos };
}
valType(): number {
return this.u8();
}
/** A block type: -1 for the empty type, a value type, or a type index. */
blockType():
| { kind: "empty" }
| { kind: "value"; type: number }
| { kind: "index"; index: number } {
const b = this.bytes[this.pos]!;
if (b === 0x40) {
this.pos++;
return { kind: "empty" };
}
if (
b === VAL_I32 ||
b === VAL_I64 ||
b === VAL_F32 ||
b === VAL_F64 ||
b === VAL_FUNCREF ||
b === VAL_EXTERNREF
) {
this.pos++;
return { kind: "value", type: b };
}
return { kind: "index", index: this.s33() };
}
}
function decodeUtf8(bytes: Uint8Array, start: number, end: number): string {
let out = "";
let i = start;
while (i < end) {
const b = bytes[i]!;
let cp = b;
let need = 0;
if (b >= 0xf0) {
cp = b & 0x07;
need = 3;
} else if (b >= 0xe0) {
cp = b & 0x0f;
need = 2;
} else if (b >= 0xc0) {
cp = b & 0x1f;
need = 1;
}
for (let k = 1; k <= need; k++) cp = cp * 64 + (bytes[i + k]! & 0x3f);
out += String.fromCodePoint(cp);
i += need + 1;
}
return out;
}
/** The flags of a memarg (the immediates of loads, stores and atomics):
* the alignment exponent and the memory index. Flags with bit 6 set
* (multi-memory) are followed by an explicit memory index, which may be
* 0; flags of 128 or more are malformed and answer align -1 without
* reading further. The offset follows. Validation (binary.ts), translation
* and skipImmediates all read memargs through this function, so they agree
* on where each instruction ends. */
export function readMemargFlags(r: { u32(): number }): { align: number; memory: number } {
const flags = r.u32();
if (flags >= 128) return { align: -1, memory: 0 };
if (flags >= 64) return { align: flags - 64, memory: r.u32() };
return { align: flags, memory: 0 };
}
/** Reads the immediates of opcode `op` (already consumed). Covers the
* translatable feature level; anything else was refused before
* translation. */
export function skipImmediates(c: WasmCursor, op: number): void {
switch (op) {
case 0x02:
case 0x03:
case 0x04:
c.blockType();
return;
case 0x0c:
case 0x0d:
case 0x10:
case 0x20:
case 0x21:
case 0x22:
case 0x23:
case 0x24:
case 0x25:
case 0x26:
case 0xd2:
c.u32();
return;
case 0x0e: {
const n = c.u32();
for (let i = 0; i <= n; i++) c.u32();
return;
}
case 0x11:
c.u32();
c.u32();
return;
case 0x1c: {
const n = c.u32();
for (let i = 0; i < n; i++) c.valType();
return;
}
case 0x3f:
case 0x40:
// The memory index (a LEB128 zero; V8 accepts long encodings).
c.u32();
return;
case 0x41:
c.s32();
return;
case 0x42:
c.s64();
return;
case 0x43:
c.pos += 4;
return;
case 0x44:
c.pos += 8;
return;
case 0xd0:
c.valType();
return;
case 0xfc: {
const sub = c.u32();
switch (sub) {
case 8:
c.u32();
c.u32();
return;
case 9:
case 13:
case 15:
case 16:
case 17:
c.u32();
return;
case 10:
c.u32();
c.u32();
return;
case 11:
c.u32();
return;
case 12:
case 14:
c.u32();
c.u32();
return;
default:
return;
}
}
default:
if (op >= 0x28 && op <= 0x3e) {
const flags = readMemargFlags(c);
if (flags.align < 0 || flags.memory !== 0)
throw new Error(`wasm: memarg of memory ${flags.memory} is not supported`);
c.u32();
}
return;
}
}
function limits(c: WasmCursor): { min: number; max: number | null } {
const flags = c.u8();
const min = c.u32();
const max = flags & 0x01 ? c.u32() : null;
return { min, max };
}
/** Parses a module binary.ts reported as valid within the translatable
* feature level. */
export function parseWasmModule(bytes: Uint8Array): WasmModule {
const m: WasmModule = {
bytes,
types: [],
imports: [],
funcTypes: [],
importedFuncs: 0,
tables: [],
memories: [],
globals: [],
exports: [],
start: null,
elems: [],
datas: [],
codes: [],
customs: [],
declaredRefs: new Set<number>(),
};
const c = new WasmCursor(bytes, 8);
while (c.pos < bytes.length) {
const id = c.u8();
const size = c.u32();
const end = c.pos + size;
switch (id) {
case 0: {
const name = c.name();
m.customs.push({ name, payload: { start: c.pos, end } });
break;
}
case 1: {
const n = c.u32();
for (let i = 0; i < n; i++) {
c.u8(); // 0x60
const params: number[] = [];
const np = c.u32();
for (let k = 0; k < np; k++) params.push(c.valType());
const results: number[] = [];
const nr = c.u32();
for (let k = 0; k < nr; k++) results.push(c.valType());
m.types.push({ params, results });
}
break;
}
case 2: {
const n = c.u32();
for (let i = 0; i < n; i++) {
const module = c.name();
const name = c.name();
const kind = c.u8();
const imp: WasmImport = {
module,
name,
kind,
type: 0,
min: 0,
max: null,
mutable: false,
};
if (kind === KIND_FUNC) {
imp.type = c.u32();
m.funcTypes.push(imp.type);
m.importedFuncs++;
} else if (kind === KIND_TABLE) {
imp.type = c.valType();
const l = limits(c);
imp.min = l.min;
imp.max = l.max;
m.tables.push({
elemType: imp.type,
min: l.min,
max: l.max,
imported: true,
init: null,
});
} else if (kind === KIND_MEMORY) {
const l = limits(c);
imp.min = l.min;
imp.max = l.max;
m.memories.push({ min: l.min, max: l.max, imported: true });
} else {
imp.type = c.valType();
imp.mutable = c.u8() === 1;
m.globals.push({ type: imp.type, mutable: imp.mutable, imported: true, init: null });
}
m.imports.push(imp);
}
break;
}
case 3: {
const n = c.u32();
for (let i = 0; i < n; i++) m.funcTypes.push(c.u32());
break;
}
case 4: {
const n = c.u32();
for (let i = 0; i < n; i++) {
let init: WasmRange | null = null;
if (c.bytes[c.pos] === 0x40) {
// table with an initializer: 0x40 0x00 tabletype expr
c.pos += 2;
const elemType = c.valType();
const l = limits(c);
init = c.constExpr();
m.tables.push({ elemType, min: l.min, max: l.max, imported: false, init });
continue;
}
const elemType = c.valType();
const l = limits(c);
m.tables.push({ elemType, min: l.min, max: l.max, imported: false, init });
}
break;
}
case 5: {
const n = c.u32();
for (let i = 0; i < n; i++) {
const l = limits(c);
m.memories.push({ min: l.min, max: l.max, imported: false });
}
break;
}
case 6: {
const n = c.u32();
for (let i = 0; i < n; i++) {
const type = c.valType();
const mutable = c.u8() === 1;
const init = c.constExpr();
noteConstRefs(m, init);
m.globals.push({ type, mutable, imported: false, init });
}
break;
}
case 7: {
const n = c.u32();
for (let i = 0; i < n; i++) {
const name = c.name();
const kind = c.u8();
const index = c.u32();
m.exports.push({ name, kind, index });
if (kind === KIND_FUNC) m.declaredRefs.add(index);
}
break;
}
case 8:
m.start = c.u32();
break;
case 9: {
const n = c.u32();
for (let i = 0; i < n; i++) m.elems.push(elemSegment(m, c));
break;
}
case 10: {
const n = c.u32();
for (let i = 0; i < n; i++) {
const size = c.u32();
const bodyEnd = c.pos + size;
const locals: number[] = [];
const groups = c.u32();
for (let g = 0; g < groups; g++) {
const count = c.u32();
const type = c.valType();
for (let k = 0; k < count; k++) locals.push(type);
}
m.codes.push({ locals, body: { start: c.pos, end: bodyEnd } });
c.pos = bodyEnd;
}
break;
}
case 11: {
const n = c.u32();
for (let i = 0; i < n; i++) {
const flags = c.u32();
if (flags === 1) {
const size = c.u32();
m.datas.push({
mode: "passive",
memory: 0,
offset: null,
payload: { start: c.pos, end: c.pos + size },
});
c.pos += size;
continue;
}
const memory = flags === 2 ? c.u32() : 0;
const offset = c.constExpr();
const size = c.u32();
m.datas.push({
mode: "active",
memory,
offset,
payload: { start: c.pos, end: c.pos + size },
});
c.pos += size;
}
break;
}
default:
// 12 (data count) and anything binary.ts accepted without content
// translation needs.
break;
}
c.pos = end;
}
return m;
}
function elemSegment(m: WasmModule, c: WasmCursor): WasmElem {
const flags = c.u32();
const passiveOrDeclarative = (flags & 1) !== 0;
const explicitTable = (flags & 2) !== 0;
const usesExprs = (flags & 4) !== 0;
let mode: WasmElem["mode"] = "active";
if (passiveOrDeclarative) mode = explicitTable ? "declarative" : "passive";
let table = 0;
let offset: WasmRange | null = null;
if (!passiveOrDeclarative) {
if (explicitTable) table = c.u32();
offset = c.constExpr();
}
let elemType = VAL_FUNCREF;
if (passiveOrDeclarative || explicitTable) {
// elemkind (0x00 = funcref) or a reference type
const b = c.u8();
elemType = usesExprs ? b : VAL_FUNCREF;
}
const n = c.u32();
const funcs: number[] = [];
const exprs: (WasmRange | null)[] = [];
for (let i = 0; i < n; i++) {
if (usesExprs) {
const expr = c.constExpr();
const direct = refFuncOf(m.bytes, expr);
if (direct !== null) {
funcs.push(direct);
exprs.push(null);
m.declaredRefs.add(direct);
} else {
noteConstRefs(m, expr);
funcs.push(-1);
exprs.push(expr);
}
} else {
const f = c.u32();
funcs.push(f);
exprs.push(null);
m.declaredRefs.add(f);
}
}
return { mode, table, offset, elemType, funcs, exprs };
}
/** The function of a `ref.func f; end` expression, or null. */
function refFuncOf(bytes: Uint8Array, expr: WasmRange): number | null {
if (bytes[expr.start] !== 0xd2) return null;
const c = new WasmCursor(bytes, expr.start + 1);
const f = c.u32();
return c.pos === expr.end - 1 && bytes[c.pos] === 0x0b ? f : null;
}
function noteConstRefs(m: WasmModule, expr: WasmRange): void {
const c = new WasmCursor(m.bytes, expr.start);
while (c.pos < expr.end) {
const op = c.u8();
if (op === 0xd2) {
m.declaredRefs.add(c.u32());
continue;
}
skipImmediates(c, op);
}
}
@@ -0,0 +1,196 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { analyzeWasm } from "./binary.js";
import { decodeBase64, encodeBase64 } from "./base64.js";
import { WasmCursor, parseWasmModule } from "./module.js";
import {
signatureHash,
stackCheckedFunctions,
translateWasmModule,
wasmSymbolPrefix,
} from "./translate.js";
const SHA = new Uint8Array(32).fill(0xab);
/** (module (func (export "add") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add)
* (memory (export "mem") 1) (data (i32.const 8) "hi")) */
const ADD = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 5, 3, 1, 0, 1, 7, 13,
2, 3, 97, 100, 100, 0, 0, 3, 109, 101, 109, 2, 0, 10, 9, 1, 7, 0, 32, 0, 32, 1, 106, 11, 11, 8, 1,
0, 65, 8, 11, 2, 104, 105,
]);
/** Every top-level line must be one the native helper's text partitioner
* accepts (native/llvm-codegen/src/partition.cpp), so translated modules
* keep the program on its concurrent parsing path. */
function topLevelLines(text: string[]): string[] {
const lines: string[] = [];
for (const part of text) {
let inFunction = false;
for (const line of part.split("\n")) {
if (inFunction) {
if (line === "}") inFunction = false;
continue;
}
if (line.startsWith("define ")) inFunction = true;
lines.push(line);
}
}
return lines;
}
describe("translateWasmModule", () => {
test("emits functions, thunks, data and the descriptor in the helper's grammar", () => {
const out = translateWasmModule(ADD, SHA, { boundsChecks: false });
const prefix = wasmSymbolPrefix(SHA);
expect(prefix).toBe("scr_wasm_abababababababab");
expect(out.descriptor).toBe(`@${prefix}_desc`);
const text = out.text.join("\n");
expect(text).toContain(`define internal i32 @${prefix}_f0(ptr %inst, i32 %p0, i32 %p1)`);
expect(text).toContain(`define internal void @${prefix}_x0(ptr %inst, ptr %a, ptr %r)`);
expect(text).toContain(`c"hi"`);
expect(text).toContain(`"disable-tail-calls"="true"`);
for (const line of topLevelLines(out.text)) {
expect(
line === "" || /^(define internal |@[\w.$-]+ = (private|internal) )/.test(line),
line,
).toBe(true);
}
for (const decl of out.decls) expect(decl).toMatch(/^declare /);
});
test("checks memory accesses explicitly only when asked", () => {
const guarded = translateWasmModule(ADD, SHA, { boundsChecks: false }).text.join("\n");
const checked = translateWasmModule(ADD, SHA, { boundsChecks: true }).text.join("\n");
expect(guarded).not.toContain("%msize");
expect(checked).toContain("%msize");
});
test("translates every valid module of the WebAssembly spec test suite", () => {
const cases = readFileSync(join(import.meta.dirname, "../../test/wasm-spec-cases.txt"), "utf8")
.split("\n")
.filter((line) => line !== "" && !line.startsWith("#"));
let translated = 0;
for (const line of cases) {
const [, , where, b64] = line.split(" ");
const bytes = decodeBase64(b64!)!;
if (analyzeWasm(bytes).status !== "valid") continue;
try {
translateWasmModule(bytes, SHA, { boundsChecks: false });
} catch (error) {
throw new Error(`${where}: ${error instanceof Error ? error.message : String(error)}`);
}
translated++;
}
expect(translated).toBeGreaterThan(800);
});
});
/** A module from its sections: `[id, ...payload]`, each payload under 128
* bytes. */
function wasmModule(...sections: number[][]): Uint8Array {
const out = [0, 97, 115, 109, 1, 0, 0, 0];
for (const [id, ...payload] of sections) out.push(id!, payload.length, ...payload);
return new Uint8Array(out);
}
/** (module (memory 1) (func (export "f") (param i32) (result i32)
* local.get 0 i32.load <memarg>)) with the memarg's bytes as given. */
function loadModule(memarg: number[]): Uint8Array {
const body = [0, 0x20, 0, 0x28, ...memarg, 0x0b];
return wasmModule(
[1, 1, 0x60, 1, 0x7f, 1, 0x7f],
[3, 1, 0],
[5, 1, 0, 1],
[7, 1, 1, 0x66, 0, 0],
[10, 1, body.length, ...body],
);
}
describe("memargs and operand types", () => {
test("a memarg naming memory 0 explicitly translates as the plain form", () => {
// Flags 0x42: alignment 2 with bit 6 set, then memory index 0 and
// offset 4. Validation and translation read the same three immediates.
// Only the descriptor (which records the module's byte length) differs.
const code = (memarg: number[]): string[] =>
translateWasmModule(loadModule(memarg), SHA, { boundsChecks: true })
.text.join("\n")
.split("\n")
.filter((line) => !line.startsWith(`@${wasmSymbolPrefix(SHA)}_desc =`));
expect(code([0x42, 0, 4])).toEqual(code([0x02, 4]));
});
test("a memarg naming another memory is refused", () => {
expect(() =>
translateWasmModule(loadModule([0x42, 1, 4]), SHA, { boundsChecks: true }),
).toThrow(/memory 1/);
});
test("local.tee of an i32 into an externref local is refused", () => {
// (func (local externref) i32.const 1 local.tee 0 drop): the operand
// never reaches a reference slot.
const body = [1, 1, 0x6f, 0x41, 1, 0x22, 0, 0x1a, 0x0b];
const bytes = wasmModule([1, 1, 0x60, 0, 0], [3, 1, 0], [10, 1, body.length, ...body]);
expect(() => translateWasmModule(bytes, SHA, { boundsChecks: false })).toThrow(
/local\.tee operand type does not match/,
);
});
});
describe("module parsing", () => {
test("reads the parts translation needs", () => {
const m = parseWasmModule(ADD);
expect(m.types).toEqual([{ params: [0x7f, 0x7f], results: [0x7f] }]);
expect(m.exports.map((e) => e.name)).toEqual(["add", "mem"]);
expect(m.memories).toEqual([{ min: 1, max: null, imported: false }]);
expect(m.datas[0]!.mode).toBe("active");
expect([...m.declaredRefs]).toEqual([0]);
});
test("decodes signed 64-bit LEB128 into two's-complement halves", () => {
const halves = (bytes: number[]): [number, number] =>
new WasmCursor(new Uint8Array(bytes), 0).s64();
expect(halves([0x7f])).toEqual([0xffffffff, 0xffffffff]);
expect(halves([0x80, 0x80, 0x80, 0x80, 0x10])).toEqual([0, 1]);
expect(halves([0xff, 0xff, 0xff, 0xff, 0x0f])).toEqual([0xffffffff, 0]);
expect(halves([0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x7f])).toEqual([
0, 0x80000000,
]);
expect(halves([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00])).toEqual([
0xffffffff, 0x7fffffff,
]);
});
});
describe("stack checks", () => {
test("only functions through which the stack can grow without bound check it", () => {
// f0 <-> f1 recurse mutually, f2 recurses on itself, f3 calls f0 once.
const calls = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 5, 4, 0, 0, 0, 0, 10, 21, 4, 4, 0, 16, 1,
11, 4, 0, 16, 0, 11, 4, 0, 16, 2, 11, 4, 0, 16, 0, 11,
]);
expect([...stackCheckedFunctions(parseWasmModule(calls))].sort()).toEqual([0, 1, 2]);
expect(stackCheckedFunctions(parseWasmModule(ADD)).size).toBe(0);
// Only code without the fault handler checks explicitly.
const checked = translateWasmModule(calls, SHA, { boundsChecks: true }).text.join("\n");
expect(checked.split("@scr_wasm_stack_limit").length - 1).toBe(3);
const guarded = translateWasmModule(calls, SHA, { boundsChecks: false }).text.join("\n");
expect(guarded).not.toContain("@scr_wasm_stack_limit");
});
});
describe("helpers", () => {
test("equal signatures share their identity across modules", () => {
expect(signatureHash("ii:i")).toBe(signatureHash("ii:i"));
expect(signatureHash("ii:i")).not.toBe(signatureHash("i:i"));
expect(signatureHash(":")).toMatch(/^u0x[0-9a-f]{16}$/);
});
test("base64 round-trips module bytes", () => {
expect(encodeBase64(ADD)).toBe(Buffer.from(ADD).toString("base64"));
expect(decodeBase64(encodeBase64(ADD))).toEqual(ADD);
expect(encodeBase64(new Uint8Array([1]))).toBe("AQ==");
expect(encodeBase64(new Uint8Array([1, 2]))).toBe("AQI=");
});
});
File diff suppressed because it is too large Load Diff
+30 -4
View File
@@ -20,7 +20,7 @@
"name": "package imports",
"status": "unsupported",
"code": "SC1010",
"note": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported"
"note": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported"
},
{
"id": "diagnostic.sc1012",
@@ -823,6 +823,13 @@
"status": "static",
"note": "recognized module (bare and node:-prefixed specifiers)"
},
{
"id": "node-builtin.dns.promises",
"kind": "node-builtin",
"name": "dns/promises",
"status": "static",
"note": "recognized module (bare and node:-prefixed specifiers)"
},
{
"id": "node-builtin.events",
"kind": "node-builtin",
@@ -2275,6 +2282,13 @@
"status": "static",
"note": "recognized module (bare and node:-prefixed specifiers)"
},
{
"id": "node-builtin.stream.web",
"kind": "node-builtin",
"name": "stream/web",
"status": "static",
"note": "recognized module (bare and node:-prefixed specifiers)"
},
{
"id": "node-builtin.string_decoder",
"kind": "node-builtin",
@@ -2805,6 +2819,19 @@
"name": "util/types.isWeakSet",
"status": "static"
},
{
"id": "node-builtin.v8",
"kind": "node-builtin",
"name": "v8",
"status": "static",
"note": "recognized module (bare and node:-prefixed specifiers)"
},
{
"id": "node-builtin.v8.getHeapStatistics",
"kind": "node-builtin",
"name": "v8.getHeapStatistics",
"status": "static"
},
{
"id": "node-builtin.worker_threads",
"kind": "node-builtin",
@@ -5672,9 +5699,8 @@
"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)"
"status": "static",
"note": "compile, instantiate, validate, Module, Instance, instanceof, the error classes, exports, memories, tables, globals and Module transfer to workers, over the modules translated at build time; the rest refuses with SC2020 (the Memory, Table and Global constructors, Module.imports, exports and customSections, and the streaming functions are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations))"
},
{
"id": "stdlib.webcrypto.CryptoKey",
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6440-http-socket-path.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6441-http-request-options-record.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6442-https-request-options-record.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6443-net-lookup-http-close-text.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/code-generation-disallowed.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/code-generation-disallowed.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/dns-promises-lookup.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/dns-promises-untyped.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/graceful-fs-alias/main.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/stream-web-readable.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/stream-web-require.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/temporal-absent-probe.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/temporal-absent.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/v8-heap-statistics.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/v8-require-optional.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/webassembly-api.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/webassembly-externref-lifetimes.mjs"
],
"diags": []
}
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/tests/wasm-spec/runner.mjs",
"<repo>/tests/corpus/webassembly-spec-control/data.mjs",
"<repo>/tests/corpus/webassembly-spec-control/main.mjs"
],
"diags": []
}

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