Merge base branch

This commit is contained in:
Chris Tate
2026-10-25 19:32:26 -05:00
286 changed files with 13248 additions and 885 deletions
+33
View File
@@ -354,6 +354,37 @@ jobs:
- run: tar -xzf "$RUNNER_TEMP/native-linux-x64-gnu.tar.gz"
- run: pnpm test tests/harness/effect.test.ts tests/harness/effect4.test.ts
# The sanitized three.js nodes build alone: beside the san corpus's other
# workers it took 860 to more than 1200 seconds of its shard
# (SCRIPTC_THREE_SANITIZED_NODES_JOB in scripts/ci-test-plan.mjs). It keeps
# the corpus's LeakSanitizer settings.
three_nodes_san:
needs: [validation_scope, lanes, native_linux]
if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }}
name: test (Linux, san, three.js nodes)
runs-on: ubuntu-24.04
timeout-minutes: 40
env:
SCRIPTC_SAN: "1"
ASAN_OPTIONS: detect_leaks=1
LSAN_OPTIONS: suppressions=${{ github.workspace }}/tests/harness/lsan.supp:print_suppressions=0
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 11.1.3
- uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: pnpm
- run: pnpm install --frozen-lockfile
- uses: actions/download-artifact@v4
with:
name: native-linux-x64-gnu
path: ${{ runner.temp }}
- run: tar -xzf "$RUNNER_TEMP/native-linux-x64-gnu.tar.gz"
- run: pnpm test tests/harness/three.test.ts -t "'nodes' runs statically through LLVM on 'native'"
# Build each instrumented seed once. Downstream contracts retain the same
# compiler and source artifacts without repeating seed construction.
bootstrap_seed:
@@ -958,6 +989,7 @@ jobs:
tests_macos,
native_linux,
effects,
three_nodes_san,
bootstrap_seed,
bootstrap,
linux_host_clang,
@@ -990,6 +1022,7 @@ jobs:
lane tests_macos "${{ needs.tests_macos.result }}" "${{ needs.lanes.outputs.macos }}"
lane native_linux "${{ needs.native_linux.result }}" "${{ needs.lanes.outputs.linux }}"
lane effects "${{ needs.effects.result }}" "${{ needs.lanes.outputs.effects }}"
lane three_nodes_san "${{ needs.three_nodes_san.result }}" "${{ needs.lanes.outputs.linux }}"
lane bootstrap_seed "${{ needs.bootstrap_seed.result }}" "${{ needs.lanes.outputs.bootstrap }}"
lane bootstrap "${{ needs.bootstrap.result }}" "${{ needs.lanes.outputs.bootstrap }}"
lane linux_host_clang "${{ needs.linux_host_clang.result }}" "${{ needs.lanes.outputs.linux }}"
+2
View File
@@ -46,6 +46,8 @@ SCRIPTC_TEST_WORKERS=4 SCRIPTC_SAN=1 pnpm test # sanitized lane
Corpus programs are differential tests against Node: every program runs under Node and as a compiled native binary, and stdout, stderr, and exit codes must match byte-for-byte. A new feature lands with corpus programs that pin its behavior both ways.
Name a new corpus program after what it tests, without a number: lowercase words joined by hyphens, such as `tests/corpus/intersection-restated-members.ts`, or a directory such as `tests/corpus/<name>/main.ts` for a multi-file program. Parallel changes kept taking the same next number; with names, two changes collide only when they add the same name, which git reports as a conflict. Leave existing numbered programs as they are: never rename or renumber them, and name a new program even when it extends a numbered family.
Record a new or remeasured program's CI cost (used to balance the test shards) in `tests/harness/corpus-costs/<program>.json`, not in `tests/harness/corpus-costs.json`; see the README there.
Before opening a PR that adds or changes corpus, diagnostic, or frontend fixtures, run `pnpm test:ts7 --baselines-only`. This fast completeness check uses the same fixture discovery as the parity suite; it does not type-check or replace behavioral parity. Generate missing preflight/order baselines, review their diagnostics and module order, and run the affected `order-parity.test.ts` batches before pushing. Keep existing baseline entries unchanged unless the corresponding behavior intentionally changes. The full `pnpm test:ts7` command also checks completeness before starting its sweep.
+19
View File
@@ -172,6 +172,25 @@ Each key names a package and a semver range of the installed versions the patch
A declaration is named by its path: `Options.name` for a member of a type, `parse()` for a function's return type, `parse(text)` for a parameter, `list()[]` for an array's elements, and `Env[string]` for an index signature. `export=()` names the return type of the function the module exports, `default` its default export, and a file prefix such as `index.js:parse()` names a declaration inside one of the package's files. One patch applies to the package's own declarations, its JSDoc, and its `@types/` package. The patched type then holds everywhere, so your code has to handle the `undefined` that a widened declaration allows. An optional `"note"` string can link the upstream issue.
For larger fixes, replace a whole declaration file, or give a package without declarations its own:
```json:package.json
{
"scriptc": {
"typePatches": {
"some-client@^3.2.0": {
"files": { "dist/types/options.d.ts": "./type-patches/options.d.ts" }
},
"untyped-helper@1.x": {
"types": "./type-patches/untyped-helper.d.ts"
}
}
}
}
```
`files` maps a declaration file inside the package, or inside its `@types/` package written as `@types/<name>/<path>`, to a local `.d.ts` file. `types` supplies the package's declaration entry. Paths of local files are relative to the `package.json`, and a replaced file can import the package's other declaration files with the same relative paths as the file it replaces.
Patches change only declarations, never the package's code, and values are still checked against the patched types. A build warns when a patch doesn't apply as written: when no range covers the installed version, when a declaration path matches nothing, when the declaration already has the type, or when a path is ambiguous. Report wrong declarations to the package too, so the patch can be removed later.
## Provenance sources (experimental)
@@ -14,7 +14,7 @@ A class can extend `require("events")` directly.
Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere.
`util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), the `zlib` callback one-shots `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip` and `unzip` (with or without options), the `crypto` callback functions `randomBytes`, `pbkdf2`, `scrypt`, `hkdf`, `randomFill`, `randomInt` and `generateKey`, `dns.lookup` (settling with `{ address, family }`, or the address array with `all: true`) and `dns.lookupService` (settling with `{ hostname, service }`), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call.
`util.promisify` over an untyped (`any`) function returns a native wrapper that follows Node's callback rules and carries the original's `name` and `length`.
`util.promisify` over an untyped function (a value typed `any`, a function whose parameters are untyped, or any other target in JavaScript) returns a native wrapper that follows Node's callback rules and carries the original's `name` and `length`, and throws Node's `ERR_INVALID_ARG_TYPE` for values that are not functions.
Such a function with other own properties, `util.promisify.custom` among them, throws `SC2020` where `util.promisify` runs, and the wrapper does not report Node's DEP0174 warning when the original returns a promise.
`util.styleText` supports standard formats and aliases, `#RGB` and `#RRGGBB` hex colors, arrays of formats, nested ANSI resets, and explicit color enabling with `{ validateStream: false }`.
@@ -1,6 +1,13 @@
`console.log`, `info`, and `debug` write to stdout; `error` and `warn` write to stderr.
They accept `unknown`, printing strings verbatim and other values through native `util.inspect`.
Direct calls support global `console` and `node:console` imports.
Console output, `util.format`, and `util.formatWithOptions` with a format string known only at run time, spread arguments, or an options object format their arguments at run time.
There, `%s` of a function or of a class instance whose class defines `toString` and `%o` of an object throw `SC2020`.
`util.inspect` with `colors`, with option values known only at run time, or in the `inspect(value, showHidden, depth, colors)` form renders the value's untyped view in the runtime with Node's styles.
Colored output of errors, promises, regular expressions, `DataView`, `ArrayBuffer`, weak collections, iterators, and other native objects throws `SC2020`.
Inspect options other than `depth`, `colors`, `customInspect`, `compact` (a number or `false`), `breakLength`, and `sorted` (a boolean) throw `SC2020` when they are set to a value other than Node's default.
`util.inspect` calls `[util.inspect.custom]` methods of classes and objects with Node's depth, options, and `util.inspect` arguments, and `console.dir` and `customInspect: false` skip them.
A class instance with such a method that is held as an untyped value throws `SC2020` when it is inspected, and a class whose subclasses declare the method without declaring it itself is refused.
Stored global console values and detached output methods retain identity and accept ordinary arguments.
Stored methods do not support format specifiers with substitution arguments.
Other stored console APIs, custom `Console` instances, and console mutation are unsupported.
@@ -25,3 +25,10 @@ Index-signature records list declared fields before the other keys.
Optional fields and properties added through a record's `unknown` view can be deleted.
A required field has no absent state, so deleting it through the view throws `TypeError: Cannot delete property 'x' of #<Object>` and leaves the record unchanged, where Node.js removes the property.
Records inherit `toString`, `toLocaleString`, `valueOf`, `hasOwnProperty`, `isPrototypeOf` and `propertyIsEnumerable` from `Object.prototype`.
A call of one of these methods by name runs the built-in with the record as its receiver, unless the record holds an own function under that name.
A keyed read such as `handlers[name]` of a record whose values are functions answers the built-in when the record lacks the key, so truthiness, `typeof` and calls match Node.js.
A keyed read of a record whose values are not functions, such as `counts["toString"]` on a `Record<string, number>`, answers `undefined` where Node.js answers the inherited function, so `counts[word] ?? 0` counts such a word as new.
Calling a built-in read from a record of functions throws a `TypeError` when its answer does not fit the record's function type, for example `valueOf` read from a `Record<string, (x: number) => number>`.
Other inherited names, such as `constructor`, read `undefined` from typed records.
+72 -72
View File
@@ -21382,9 +21382,9 @@
"anchor": "err_invalid_mime_syntax",
"nodeStability": null,
"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"
}
},
{
@@ -23520,9 +23520,9 @@
"anchor": "err_throttled",
"nodeStability": null,
"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"
}
},
{
@@ -71946,9 +71946,9 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -71969,9 +71969,9 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -71992,9 +71992,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"
}
},
{
@@ -72015,9 +72015,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"
}
},
{
@@ -72038,9 +72038,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"
}
},
{
@@ -72061,9 +72061,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"
}
},
{
@@ -72176,9 +72176,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"
}
},
{
@@ -72199,9 +72199,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"
}
},
{
@@ -72222,9 +72222,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"
}
},
{
@@ -72245,9 +72245,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"
}
},
{
@@ -72268,9 +72268,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"
}
},
{
@@ -72291,9 +72291,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"
}
},
{
@@ -72314,9 +72314,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"
}
},
{
@@ -72337,9 +72337,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"
}
},
{
@@ -72360,9 +72360,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"
}
},
{
@@ -72636,9 +72636,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 call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -72751,9 +72751,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 call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -72774,9 +72774,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 call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -72797,9 +72797,9 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -73027,9 +73027,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"
}
},
{
@@ -73050,9 +73050,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"
}
},
{
@@ -73395,9 +73395,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 call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -25806,8 +25806,12 @@
"anchor": "dep0044-utilisarray",
"nodeStability": null,
"static": {
"status": "not-applicable",
"evidence": "compiler-chapter-policy:deprecations"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util.isArray",
"tests": [
"tests/corpus/7700-util-deprecated-helpers.cjs",
"tests/corpus/7701-util-deprecated-helpers.ts"
]
},
"anchorSource": "exact"
},
@@ -26094,8 +26098,12 @@
"anchor": "dep0060-util_extend",
"nodeStability": null,
"static": {
"status": "not-applicable",
"evidence": "compiler-chapter-policy:deprecations"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util._extend",
"tests": [
"tests/corpus/7700-util-deprecated-helpers.cjs",
"tests/corpus/7701-util-deprecated-helpers.ts"
]
},
"anchorSource": "exact"
},
@@ -32763,6 +32771,8 @@
"tests/corpus/6702-ecdh-errors.js",
"tests/corpus/6708-dh-errors.js",
"tests/corpus/7440-tls-server-add-context.ts",
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7711-util-throttle.cjs",
"tests/corpus/7714-zlib-brotli-compress.cjs",
"tests/corpus/caught-system-error-fields.ts",
"tests/corpus/error-stack-frames.ts",
@@ -36688,8 +36698,11 @@
"anchor": "err_invalid_mime_syntax",
"nodeStability": null,
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:errors"
"status": "partial",
"evidence": "compiler-feature:errors.ERR_INVALID_MIME_SYNTAX",
"tests": [
"tests/corpus/7707-util-mime-type.cjs"
]
},
"anchorSource": "exact"
},
@@ -38826,8 +38839,11 @@
"anchor": "err_throttled",
"nodeStability": null,
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:errors"
"status": "partial",
"evidence": "compiler-feature:errors.ERR_THROTTLED",
"tests": [
"tests/corpus/7711-util-throttle.cjs"
]
},
"anchorSource": "exact"
},
@@ -101568,6 +101584,25 @@
"tests/corpus/5835-util-format-integer-specifiers.ts",
"tests/corpus/5884-inspect-layout-options.ts",
"tests/corpus/6428-promisify-callback-functions.ts",
"tests/corpus/7520-util-format-runtime.ts",
"tests/corpus/7521-util-format-runtime.cjs",
"tests/corpus/7522-promisify-untyped.cjs",
"tests/corpus/7523-promisify-untyped.ts",
"tests/corpus/7524-inspect-custom.ts",
"tests/corpus/7525-inspect-custom.cjs",
"tests/corpus/7526-inspect-options-runtime.ts",
"tests/corpus/7700-util-deprecated-helpers.cjs",
"tests/corpus/7701-util-deprecated-helpers.ts",
"tests/corpus/7702-util-signal-exit-code.cjs",
"tests/corpus/7703-util-aborted.cjs",
"tests/corpus/7704-util-aborted.ts",
"tests/corpus/7705-util-diff.cjs",
"tests/corpus/7706-util-diff.ts",
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts",
"tests/corpus/7710-util-debounce.cjs",
"tests/corpus/7711-util-throttle.cjs",
"tests/corpus/7712-util-debounce-throttle.ts",
"tests/corpus/7747-text-codec-untyped-slot.js",
"tests/corpus/node24-deep-equality-properties.js",
"tests/corpus/node24-deep-equality.cjs",
@@ -101654,8 +101689,12 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util._extend",
"tests": [
"tests/corpus/7700-util-deprecated-helpers.cjs",
"tests/corpus/7701-util-deprecated-helpers.ts"
]
},
"anchorSource": "exact"
},
@@ -101677,8 +101716,12 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util.isArray",
"tests": [
"tests/corpus/7700-util-deprecated-helpers.cjs",
"tests/corpus/7701-util-deprecated-helpers.ts"
]
},
"anchorSource": "exact"
},
@@ -101700,8 +101743,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.util.MIMEType",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101723,8 +101770,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mime.toString",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101746,8 +101797,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mime.toJSON",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101769,8 +101824,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.MIMEType.parse",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101884,8 +101943,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.util.MIMEParams",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101907,8 +101970,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mimeParams.delete",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101930,8 +101997,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mimeParams.entries",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101953,8 +102024,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mimeParams.get",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101976,8 +102051,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mimeParams.has",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -101999,8 +102078,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mimeParams.keys",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -102022,8 +102105,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mimeParams.set",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -102045,8 +102132,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mimeParams.values",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -102068,8 +102159,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.mimeParams",
"tests": [
"tests/corpus/7707-util-mime-type.cjs",
"tests/corpus/7708-util-mime-type.ts"
]
},
"anchorSource": "exact"
},
@@ -102468,8 +102563,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util.convertProcessSignalToExitCode",
"tests": [
"tests/corpus/7702-util-signal-exit-code.cjs"
]
},
"anchorSource": "exact"
},
@@ -102603,8 +102701,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util.debounce",
"tests": [
"tests/corpus/7710-util-debounce.cjs",
"tests/corpus/7712-util-debounce-throttle.ts"
]
},
"anchorSource": "exact"
},
@@ -102626,8 +102728,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util.throttle",
"tests": [
"tests/corpus/7711-util-throttle.cjs",
"tests/corpus/7712-util-debounce-throttle.ts"
]
},
"anchorSource": "exact"
},
@@ -102649,8 +102755,12 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util.diff",
"tests": [
"tests/corpus/7705-util-diff.cjs",
"tests/corpus/7706-util-diff.ts"
]
},
"anchorSource": "exact"
},
@@ -102677,7 +102787,9 @@
"tests": [
"tests/corpus/1636-util-format.ts",
"tests/corpus/2440-console-inspect-args.ts",
"tests/corpus/5835-util-format-integer-specifiers.ts"
"tests/corpus/5835-util-format-integer-specifiers.ts",
"tests/corpus/7520-util-format-runtime.ts",
"tests/corpus/7521-util-format-runtime.cjs"
]
},
"anchorSource": "exact"
@@ -102705,7 +102817,9 @@
"tests": [
"tests/corpus/1636-util-format.ts",
"tests/corpus/2440-console-inspect-args.ts",
"tests/corpus/5835-util-format-integer-specifiers.ts"
"tests/corpus/5835-util-format-integer-specifiers.ts",
"tests/corpus/7520-util-format-runtime.ts",
"tests/corpus/7521-util-format-runtime.cjs"
]
},
"anchorSource": "exact"
@@ -102896,7 +103010,8 @@
"tests/corpus/1632-inspect-records.ts",
"tests/corpus/1637-inspect-dyn.ts",
"tests/corpus/2485-inspect-circular-refs.ts",
"tests/corpus/5884-inspect-layout-options.ts"
"tests/corpus/5884-inspect-layout-options.ts",
"tests/corpus/7526-inspect-options-runtime.ts"
]
},
"anchorSource": "exact"
@@ -102919,8 +103034,11 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.util.inspect",
"tests": [
"tests/corpus/7526-inspect-options-runtime.ts"
]
},
"anchorSource": "exact"
},
@@ -102942,8 +103060,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "compiler-feature:util.custom",
"tests": [
"tests/corpus/7524-inspect-custom.ts",
"tests/corpus/7525-inspect-custom.cjs"
]
},
"anchorSource": "ancestor"
},
@@ -103260,6 +103382,8 @@
"tests/fixtures/node-types/promisify-dns.ts",
"tests/corpus/6428-promisify-callback-functions.ts",
"tests/fixtures/node-types/promisify-callback-functions.ts",
"tests/corpus/7522-promisify-untyped.cjs",
"tests/corpus/7523-promisify-untyped.ts",
"tests/harness/project-config.test.ts"
]
},
@@ -103475,8 +103599,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:util"
"status": "partial",
"evidence": "surface-manifest:node-builtin.util.aborted",
"tests": [
"tests/corpus/7703-util-aborted.cjs",
"tests/corpus/7704-util-aborted.ts"
]
},
"anchorSource": "exact"
},
+16 -3
View File
@@ -187,6 +187,13 @@
"node-builtin.util.callbackify": ["tests/corpus/5190-util-callbackify.cjs", "tests/corpus/5191-util-callbackify.ts", "tests/fixtures/node-types/util-callables.ts"],
"node-builtin.util.debuglog": ["tests/corpus/5192-util-debuglog.cjs", "tests/corpus/5193-util-debuglog.ts", "tests/fixtures/node-types/util-callables.ts"],
"node-builtin.util.debug": ["tests/corpus/5192-util-debuglog.cjs", "tests/corpus/5193-util-debuglog.ts"],
"node-builtin.util.isArray": ["tests/corpus/7700-util-deprecated-helpers.cjs", "tests/corpus/7701-util-deprecated-helpers.ts"],
"node-builtin.util._extend": ["tests/corpus/7700-util-deprecated-helpers.cjs", "tests/corpus/7701-util-deprecated-helpers.ts"],
"node-builtin.util.convertProcessSignalToExitCode": ["tests/corpus/7702-util-signal-exit-code.cjs"],
"node-builtin.util.aborted": ["tests/corpus/7703-util-aborted.cjs", "tests/corpus/7704-util-aborted.ts"],
"node-builtin.util.diff": ["tests/corpus/7705-util-diff.cjs", "tests/corpus/7706-util-diff.ts"],
"node-builtin.util.debounce": ["tests/corpus/7710-util-debounce.cjs", "tests/corpus/7712-util-debounce-throttle.ts"],
"node-builtin.util.throttle": ["tests/corpus/7711-util-throttle.cjs", "tests/corpus/7712-util-debounce-throttle.ts"],
"node-builtin.util": ["tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2912-promisify-fs-readfile.ts"],
"node-builtin.worker_threads": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-references.ts"],
"node-builtin.zlib": ["tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", "tests/corpus/3115-zlib-static-compression-levels.ts", "tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5378-zlib-sync-options.cjs", "tests/corpus/5379-zlib-callback-options.ts", "tests/corpus/5415-zlib-dynamic-inputs.cjs"],
@@ -253,9 +260,11 @@
{"chapter": "worker_threads", "symbols": ["resourceLimits"], "status": "partial", "evidence": ["tests/corpus/5806-worker-resource-limits.ts"]},
{"chapter": "worker_threads", "symbols": ["BroadcastChannel extends EventTarget", "broadcastChannel.close", "broadcastChannel.postMessage", "broadcastChannel.ref", "broadcastChannel.unref", "onmessage"], "status": "partial", "evidence": ["tests/corpus/5800-broadcast-channel.ts", "tests/corpus/5801-broadcast-channel.cjs", "tests/corpus/5802-broadcast-channel-workers.ts"]},
{"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject"], "status": "partial", "evidence": ["tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]},
{"chapter": "util", "symbols": ["util.format", "util.formatWithOptions"], "status": "partial", "evidence": ["tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/5835-util-format-integer-specifiers.ts"]},
{"chapter": "util", "symbols": ["util.format", "util.formatWithOptions"], "status": "partial", "evidence": ["tests/corpus/1636-util-format.ts", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/5835-util-format-integer-specifiers.ts", "tests/corpus/7520-util-format-runtime.ts", "tests/corpus/7521-util-format-runtime.cjs"]},
{"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject", "util.types.isDate", "util.types.isSharedArrayBuffer", "util.types.isMapIterator", "util.types.isSetIterator", "util.types.isKeyObject", "util.types.isNumberObject", "util.types.isStringObject", "util.types.isBooleanObject", "util.types.isBigIntObject", "util.types.isSymbolObject", "util.types.isBoxedPrimitive", "util.types.isBigInt64Array", "util.types.isBigUint64Array", "util.types.isFloat16Array", "util.types.isCryptoKey", "util.types.isExternal"], "status": "partial", "evidence": ["tests/corpus/5831-util-types-brands.ts", "tests/corpus/5832-util-types-brands-dynamic.js", "tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]},
{ "chapter": "util", "signatures": ["`util.inspect(object[, options])`"], "status": "partial", "evidence": ["tests/corpus/1630-inspect-scalars.ts", "tests/corpus/1632-inspect-records.ts", "tests/corpus/1637-inspect-dyn.ts", "tests/corpus/2485-inspect-circular-refs.ts", "tests/corpus/5884-inspect-layout-options.ts"] },
{ "chapter": "util", "signatures": ["`util.inspect(object[, options])`"], "status": "partial", "evidence": ["tests/corpus/1630-inspect-scalars.ts", "tests/corpus/1632-inspect-records.ts", "tests/corpus/1637-inspect-dyn.ts", "tests/corpus/2485-inspect-circular-refs.ts", "tests/corpus/5884-inspect-layout-options.ts", "tests/corpus/7526-inspect-options-runtime.ts"] },
{ "chapter": "util", "signatures": ["`util.inspect(object[, showHidden[, depth[, colors]]])`"], "status": "partial", "evidence": ["tests/corpus/7526-inspect-options-runtime.ts"] },
{ "chapter": "util", "signatures": ["`custom` Type: {symbol} that can be used to declare custom inspect functions."], "status": "partial", "evidence": ["tests/corpus/7524-inspect-custom.ts", "tests/corpus/7525-inspect-custom.cjs"] },
{ "chapter": "util", "symbols": ["util.styleText"], "status": "partial", "evidence": ["tests/corpus/node24-style-text.ts", "tests/corpus/node24-style-text-validation.js", "tests/corpus/node24-style-text-environment.js", "tests/corpus/node24-style-text.cjs", "tests/fixtures/node-types/style-text.ts"] },
{ "chapter": "util", "symbols": ["util.isDeepStrictEqual"], "status": "partial", "evidence": ["tests/corpus/node24-deep-equality.ts", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality-properties.js", "tests/fixtures/node-types/deep-equality.ts", "packages/compiler/test/deep-equality.test.ts"] },
{ "chapter": "deprecations", "symbols": ["util.toUSVString"], "status": "not-applicable" },
@@ -265,6 +274,10 @@
{ "chapter": "buffer", "symbols": ["buffer.isAscii", "buffer.isUtf8", "buffer.transcode"], "status": "partial", "evidence": ["tests/corpus/node24-buffer-encoding.ts", "tests/corpus/node24-buffer-encoding-errors.ts", "tests/fixtures/node-types/text-encoding.ts"] },
{ "chapter": "util", "parent": "Class: `util.TextEncoder`", "signatures": ["`textEncoder.encodeInto(src, dest)`", "`encoding` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/node24-text-encode-into.ts", "tests/corpus/node24-text-codec-properties.ts", "tests/fixtures/node-types/text-encoding.ts"] },
{ "chapter": "util", "parent": "Class: `util.TextDecoder`", "signatures": ["`encoding` Type: {string}", "`fatal` Type: {boolean}", "`ignoreBOM` Type: {boolean}"], "status": "partial", "evidence": ["tests/corpus/node24-text-codec-properties.ts", "tests/fixtures/node-types/text-encoding.ts"] },
{ "chapter": "util", "symbols": ["util.MIMEType", "util.MIMEParams", "MIMEType.parse", "mime.toString", "mime.toJSON", "mimeParams.delete", "mimeParams.entries", "mimeParams.get", "mimeParams.has", "mimeParams.keys", "mimeParams.set", "mimeParams.values"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs", "tests/corpus/7708-util-mime-type.ts"] },
{ "chapter": "util", "signatures": ["`mimeParams[Symbol.iterator]()`"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs", "tests/corpus/7708-util-mime-type.ts"] },
{ "chapter": "errors", "symbols": ["ERR_INVALID_MIME_SYNTAX"], "status": "partial", "evidence": ["tests/corpus/7707-util-mime-type.cjs"] },
{ "chapter": "errors", "symbols": ["ERR_THROTTLED"], "status": "partial", "evidence": ["tests/corpus/7711-util-throttle.cjs"] },
{ "chapter": "util", "symbols": ["util.parseEnv"], "status": "partial", "evidence": ["tests/corpus/node24-env-parse.ts", "tests/corpus/node24-env-parse-errors.ts", "tests/corpus/node24-env-imports.cjs", "tests/fixtures/node-types/env-files.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "process", "symbols": ["process.memoryUsage.rss"], "status": "partial", "evidence": ["tests/corpus/5210-process-memory-usage.ts"] },
{ "chapter": "perf_hooks", "symbols": ["performance.now"], "status": "partial", "evidence": ["tests/corpus/2427-perf-hooks-now.ts", "tests/corpus/2446-performance-global.ts"] },
@@ -409,7 +422,7 @@
{ "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.before", "context.after", "context.beforeEach", "context.afterEach", "context.plan", "context.fullName"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "symbols": ["getTestContext"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/get-test-context.ts", "tests/fixtures/node-test/cases/get-test-context-cancelled.ts", "tests/harness/node-test.test.ts"] },
{ "chapter": "test", "symbols": ["describe.skip", "describe.todo", "describe.only", "suite.skip", "suite.todo", "suite.only", "it.skip", "it.todo", "it.only"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/only-nested.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts"] },
{ "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": "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/corpus/7522-promisify-untyped.cjs", "tests/corpus/7523-promisify-untyped.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": ["Class: `zlib.BrotliCompress`", "Class: `BrotliOptions`"], "status": "partial", "evidence": ["tests/corpus/7714-zlib-brotli-compress.cjs"] },
+3 -1
View File
@@ -50,9 +50,11 @@ test.runIf(process.platform === "linux" && process.arch === "x64")(
const expected = `${(await execFileAsync(process.execPath, [entry])).stdout}`;
const baseline = await build([]);
expect(baseline).not.toContain('"target-cpu"');
// Only --sanitize builds mark the functions for ASan instrumentation.
expect(baseline).not.toContain("sanitize_address");
expect(await build(["--target-cpu=baseline"])).toBe(baseline);
const v3 = await build(["--target-cpu=x86-64-v3"]);
expect(v3).toMatch(/^attributes #0 = \{ sanitize_address "target-cpu"="x86-64-v3" \}$/m);
expect(v3).toMatch(/^attributes #0 = \{ "target-cpu"="x86-64-v3" \}$/m);
if (runsV3) expect((await execFileAsync(outPath)).stdout).toBe(expected);
// The bootstrap's cached route builds without a level.
expect(await build([], true)).toBe(baseline);
+131 -5
View File
@@ -3961,6 +3961,85 @@ declare module "util" {
): string;
export function parseEnv(content: string): { [key: string]: string | undefined };
export function inherits(constructor: unknown, superConstructor: unknown): void;
export class MIMEType {
constructor(input: string | { toString: () => string });
static parse(input: string | { toString: () => string }): MIMEType | null;
type: string;
subtype: string;
readonly essence: string;
readonly params: MIMEParams;
toString(): string;
toJSON(): string;
}
export class MIMEParams {
delete(name: string): void;
entries(): IterableIterator<[name: string, value: string]>;
get(name: string): string | null;
has(name: string): boolean;
keys(): IterableIterator<string>;
set(name: string, value: string): void;
values(): IterableIterator<string>;
toString(): string;
toJSON(): string;
[Symbol.iterator](): IterableIterator<[name: string, value: string]>;
}
export type DiffEntry = [operation: -1 | 0 | 1, value: string];
export function diff(
actual: string | readonly string[],
expected: string | readonly string[],
): DiffEntry[];
export interface DebounceOptions {
leading?: boolean;
rejectOnCancel?: boolean;
signal?: AbortSignal;
}
/** Every call answers a promise of the result of the call it waits for. */
export interface DebouncedFunction<T extends (...args: any[]) => any> {
(...args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
cancel(reason?: unknown): void;
flush(): void;
ref(): this;
unref(): this;
readonly pending: Promise<Awaited<ReturnType<T>>> | null;
readonly pendingCount: number;
}
export function debounce<T extends (...args: any[]) => any>(
fn: T,
wait: number,
options?: DebounceOptions,
): DebouncedFunction<T>;
export interface ThrottleOptions {
concurrency?: number;
maxPending?: number;
overflow?: "queue" | "drop";
signal?: AbortSignal;
strict?: boolean;
}
export interface ThrottledFunction<T extends (...args: any[]) => any> {
(...args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
cancel(reason?: unknown): void;
hasImmediateCapacity(): boolean;
ref(): this;
unref(): this;
readonly pending: Promise<Awaited<ReturnType<T>>> | null;
readonly pendingCount: number;
readonly activeCount: number;
}
export function throttle<T extends (...args: any[]) => any>(
fn: T,
limit: number,
interval: number,
options?: ThrottleOptions,
): ThrottledFunction<T>;
/** Resolves with the abort event once `signal` aborts, or with undefined
* when it already has. */
export function aborted(signal: AbortSignal, resource: any): Promise<unknown>;
/** 128 plus the number of the signal, the exit code of a process it terminates. */
export function convertProcessSignalToExitCode(signalCode: string): number;
/** @deprecated Since v4.0.0 - Use `Array.isArray()` instead. */
export function isArray(object: unknown): object is unknown[];
/** @deprecated Since v6.0.0 - Use `Object.assign()` instead. Answers target itself. */
export function _extend<T extends object>(target: T, source: object | null | undefined): T;
export interface DeprecateOptions {
modifyPrototype?: boolean;
}
@@ -4018,7 +4097,13 @@ declare module "util" {
export type TextDecoder = globalThis.TextDecoder;
export var TextEncoder: typeof globalThis.TextEncoder;
export var TextDecoder: typeof globalThis.TextDecoder;
/* stream.pipeline's overload comes first: its __promisify__ member keeps
/* An untyped function: an `any` value, or a function whose parameters
* are untyped (JavaScript's `(value, callback) => ...`), the only
* parameters that accept this object type. The promisified function is
* untyped too. It comes first because such functions match every other
* overload. */
export function promisify(fn: (...args: { readonly "scriptc:untyped": true }[]) => unknown): any;
/* stream.pipeline's overload comes next: its __promisify__ member keeps
* every other target out of it. */
export function promisify(
fn: typeof import("node:stream").pipeline,
@@ -4139,15 +4224,56 @@ declare module "util" {
): (address: string, port: number) => Promise<{ hostname: string; service: string }>;
/* Declared surface without full lowering (the type-level tolerance
* story: guarded/diagnostic-path code must TYPECHECK; a reached use
* without a lowering fences at its site). inspect's options carry the
* documented knobs through an index signature. */
* without a lowering fences at its site). */
export interface InspectOptions {
showHidden?: boolean | undefined;
depth?: number | null | undefined;
colors?: boolean | undefined;
customInspect?: boolean | undefined;
showProxy?: boolean | undefined;
maxArrayLength?: number | null | undefined;
maxStringLength?: number | null | undefined;
breakLength?: number | undefined;
compact?: boolean | number | undefined;
sorted?: boolean | ((a: string, b: string) => number) | undefined;
getters?: "get" | "set" | boolean | undefined;
numericSeparator?: boolean | undefined;
}
export type Style =
| "special"
| "number"
| "bigint"
| "boolean"
| "undefined"
| "null"
| "string"
| "symbol"
| "date"
| "regexp"
| "module";
export interface InspectOptionsStylized extends InspectOptions {
stylize(text: string, styleType: Style): string;
}
export type CustomInspectFunction = (
depth: number,
options: InspectOptionsStylized,
inspect: typeof import("node:util").inspect,
) => any;
export function inspect(
object: unknown,
options?: { depth?: number | null; colors?: boolean; [k: string]: unknown },
showHidden?: boolean,
depth?: number | null,
color?: boolean,
): string;
export function inspect(object: unknown, options?: InspectOptions): string;
export namespace inspect {
/* The key of an object's custom inspect function:
* Symbol.for('nodejs.util.inspect.custom'). */
const custom: unique symbol;
}
export function format(format?: unknown, ...args: unknown[]): string;
export function formatWithOptions(
inspectOptions: { depth?: number | null; colors?: boolean; [k: string]: unknown },
inspectOptions: InspectOptions,
format?: unknown,
...args: unknown[]
): string;
@@ -34,6 +34,8 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
...(features.declAdopt ? { declAdopt: true } : {}),
...(features.dynForAwait ? { dynForAwait: true } : {}),
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
...(features.mime ? { mime: true } : {}),
...(features.utilTiming ? { utilTiming: true } : {}),
...(features.dynBuffer ? { dynBuffer: true } : {}),
...(features.builtinValues ? { builtinValues: true } : {}),
// Island code's signal and exit listeners join the events unit's
+156 -118
View File
@@ -198,15 +198,17 @@ function readRange(dir: string, range: number): { result: ChunkResult; texts: st
/** The attribute groups of the emitted functions: #0 for every function,
* #1 for coroutine bodies and #2 for out-of-line record clones. Each carries
* the build's `--target-cpu` level. */
* the build's `--target-cpu` level, and `sanitize_address` in sanitized
* builds. A group needs one attribute: without either, #0 holds a string
* attribute that LLVM ignores. */
function functionAttributes(emitter: LlEmitter, hasNoInlineRecordClone: boolean): string[] {
const cpu = targetCpuAttribute(emitter.targetCpu);
const asan = emitter.sanitizeAttributes ? " sanitize_address" : "";
const base = `${asan}${cpu}` || ` "scriptc-function"`;
return [
`attributes #0 = { sanitize_address${cpu} }`,
...(emitter.usesCoroutines
? [`attributes #1 = { sanitize_address presplitcoroutine${cpu} }`]
: []),
...(hasNoInlineRecordClone ? [`attributes #2 = { noinline sanitize_address${cpu} }`] : []),
`attributes #0 = {${base} }`,
...(emitter.usesCoroutines ? [`attributes #1 = {${asan} presplitcoroutine${cpu} }`] : []),
...(hasNoInlineRecordClone ? [`attributes #2 = { noinline${asan}${cpu} }`] : []),
];
}
@@ -1204,9 +1206,9 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
);
for (const line of emitter.inlineRcTail(flushedDecls)) out.push(line);
out.push(
// sanitize_address is inert under the plain pipeline; the sanitized
// lane's -fsanitize=address link activates instrumentation over the
// emitted functions too (the runtime TUs get theirs from clang).
// The sanitized lane's -fsanitize=address link instruments the
// functions marked sanitize_address (the runtime TUs get theirs from
// clang).
...functionAttributes(emitter, hasNoInlineRecordClone),
``,
);
@@ -1235,6 +1237,126 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
return out;
}
/** The out-of-line hashed probe of a number-key map (scr_map_find_f64):
* `keyLines` define the normalized key word %kb from the parameter, and
* `runtimeKey` is the double passed to the runtime lookup. A map with at
* most SCR_MAP_LINEAR_LIMIT entries keeps no bucket table: the absent-key
* filter is tested before its entries are scanned. A bucket table is
* probed without the filter: the filter keeps two of its 64 bits per key,
* so past a few dozen keys it rejects few absent keys, and the probe's
* first bucket read answers those. Compact tables tag each 24-bit entry
* index (+1) with the hash's top byte; wider tables take the runtime
* lookup. */
function hashedNumberProbe(
name: string,
param: string,
sz: string,
keyLines: string[],
runtimeKey: string,
): string[] {
return [
`define internal ptr @${name}(ptr %m, ${param}) noinline ${FN_ATTRS} {`,
`entry:`,
...keyLines,
` %x1s = lshr i64 %kb, 30`,
` %x1 = xor i64 %kb, %x1s`,
` %x2 = mul i64 %x1, -4658895280553007687`,
` %x3s = lshr i64 %x2, 27`,
` %x3 = xor i64 %x2, %x3s`,
` %x4 = mul i64 %x3, -7723592293110705685`,
` %x5s = lshr i64 %x4, 31`,
` %x5 = xor i64 %x4, %x5s`,
` %hz = icmp eq i64 %x5, 0`,
` %h = select i1 %hz, i64 1, i64 %x5`,
` %nbp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 13`,
...(sz === "i64"
? [` %nb32 = load i32, ptr %nbp`, ` %nb = zext i32 %nb32 to i64`]
: [` %nb = load i32, ptr %nbp`]),
` %eps = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
` %ents = load ptr, ptr %eps`,
` %linear = icmp eq ${sz} %nb, 0`,
` br i1 %linear, label %filter, label %buckets`,
`filter:`,
` %f1s = lshr i64 %h, 44`,
` %f1a = and i64 %f1s, 63`,
` %f1 = shl i64 1, %f1a`,
` %f2s = lshr i64 %h, 50`,
` %f2a = and i64 %f2s, 63`,
` %f2 = shl i64 1, %f2a`,
` %fb = or i64 %f1, %f2`,
` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`,
` %filter.bits = load i64, ptr %fp`,
` %fm = and i64 %filter.bits, %fb`,
` %fin = icmp eq i64 %fm, %fb`,
` br i1 %fin, label %scan, label %miss`,
// A number-key entry has no cached hash; a dead entry's key matches no key.
`scan:`,
` %nep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`,
...(sz === "i64"
? [` %ne32 = load i32, ptr %nep`, ` %ne = zext i32 %ne32 to i64`]
: [` %ne = load i32, ptr %nep`]),
` br label %scan.loop`,
`scan.loop:`,
` %si = phi ${sz} [ 0, %scan ], [ %si.next, %scan.next ]`,
` %sdone = icmp eq ${sz} %si, %ne`,
` br i1 %sdone, label %miss, label %scan.entry`,
`scan.entry:`,
` %se = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %si`,
` %sk = load i64, ptr %se`,
` %skq = icmp eq i64 %sk, %kb`,
` br i1 %skq, label %scan.found, label %scan.next`,
`scan.found:`,
` ret ptr %se`,
`scan.next:`,
` %si.next = add ${sz} %si, 1`,
` br label %scan.loop`,
`buckets:`,
` %wide = icmp ugt ${sz} %nb, 16777216`,
` br i1 %wide, label %runtime, label %probe`,
`probe:`,
` %bp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 14`,
` %bk = load ptr, ptr %bp`,
` %mask = sub ${sz} %nb, 1`,
` %ht = lshr i64 %h, 32`,
` %ht32 = trunc i64 %ht to i32`,
` %tag = and i32 %ht32, -16777216`,
sz === "i64" ? ` %hi = add i64 %h, 0` : ` %hi = trunc i64 %h to ${sz}`,
` %i0 = and ${sz} %hi, %mask`,
` br label %probe.loop`,
`probe.loop:`,
` %pi = phi ${sz} [ %i0, %probe ], [ %pi.next, %probe.next ]`,
` %bep = getelementptr inbounds i32, ptr %bk, ${sz} %pi`,
` %b = load i32, ptr %bep`,
` %bempty = icmp eq i32 %b, 0`,
` br i1 %bempty, label %miss, label %probe.tag`,
`probe.tag:`,
` %bt = and i32 %b, -16777216`,
` %btq = icmp eq i32 %bt, %tag`,
` br i1 %btq, label %probe.entry, label %probe.next`,
`probe.entry:`,
` %bix = and i32 %b, 16777215`,
` %bix1 = sub i32 %bix, 1`,
sz === "i32" ? ` %bi = add i32 %bix1, 0` : ` %bi = zext i32 %bix1 to ${sz}`,
` %pe = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %bi`,
` %pk = load i64, ptr %pe`,
` %pkq = icmp eq i64 %pk, %kb`,
` br i1 %pkq, label %probe.found, label %probe.next`,
`probe.found:`,
` ret ptr %pe`,
`probe.next:`,
` %pi.inc = add ${sz} %pi, 1`,
` %pi.next = and ${sz} %pi.inc, %mask`,
` br label %probe.loop`,
`miss:`,
` ret ptr null`,
`runtime:`,
` %r = call ptr @scr_map_entry_f64(ptr %m, double ${runtimeKey}) memory(read) nounwind willreturn`,
` ret ptr %r`,
`}`,
``,
];
}
/** The shared abort helpers (emitted only when referenced): the OOM
* abort of untraced shape allocation and the invalid-union-tag abort —
* both print the runtime ABI’s exact message on fd 2 and abort. */
@@ -1401,118 +1523,34 @@ export function helperDefs(emitter: LlEmitter): string[] {
` %dense.any = icmp ne i64 %nd, 0`,
` br i1 %dense.any, label %miss, label %slow`,
`slow:`,
` %hashed = call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)`,
` %hashed = call ptr @sc_map_entry_f64_hashed_int(ptr %m, i64 %ki)`,
` ret ptr %hashed`,
`}`,
``,
// Hashing (scr_map_find_f64): the SameValueZero key word, its
// scr_key_mix hash (zero remapped to one) and the absent-key filter.
`define internal ptr @sc_map_entry_f64_hashed(ptr %m, double %k) noinline ${FN_ATTRS} {`,
`entry:`,
` %nan = fcmp uno double %k, %k`,
` %zero = fcmp oeq double %k, 0.0`,
` %kz = select i1 %zero, double 0.0, double %k`,
` %kzb = bitcast double %kz to i64`,
` %kb = select i1 %nan, i64 9221120237041090560, i64 %kzb`,
` %x1s = lshr i64 %kb, 30`,
` %x1 = xor i64 %kb, %x1s`,
` %x2 = mul i64 %x1, -4658895280553007687`,
` %x3s = lshr i64 %x2, 27`,
` %x3 = xor i64 %x2, %x3s`,
` %x4 = mul i64 %x3, -7723592293110705685`,
` %x5s = lshr i64 %x4, 31`,
` %x5 = xor i64 %x4, %x5s`,
` %hz = icmp eq i64 %x5, 0`,
` %h = select i1 %hz, i64 1, i64 %x5`,
` %f1s = lshr i64 %h, 44`,
` %f1a = and i64 %f1s, 63`,
` %f1 = shl i64 1, %f1a`,
` %f2s = lshr i64 %h, 50`,
` %f2a = and i64 %f2s, 63`,
` %f2 = shl i64 1, %f2a`,
` %fb = or i64 %f1, %f2`,
` %fp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10`,
` %filter = load i64, ptr %fp`,
` %fm = and i64 %filter, %fb`,
` %fin = icmp eq i64 %fm, %fb`,
` br i1 %fin, label %table, label %miss`,
`table:`,
` %nbp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 13`,
...(sz === "i64"
? [` %nb32 = load i32, ptr %nbp`, ` %nb = zext i32 %nb32 to i64`]
: [` %nb = load i32, ptr %nbp`]),
` %eps = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
` %ents = load ptr, ptr %eps`,
` %linear = icmp eq ${sz} %nb, 0`,
` br i1 %linear, label %scan, label %buckets`,
// Small maps keep no bucket table: scan the entries' keys. A number-key
// entry has no cached hash; a dead entry's key matches no key.
`scan:`,
` %nep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 8`,
...(sz === "i64"
? [` %ne32 = load i32, ptr %nep`, ` %ne = zext i32 %ne32 to i64`]
: [` %ne = load i32, ptr %nep`]),
` br label %scan.loop`,
`scan.loop:`,
` %si = phi ${sz} [ 0, %scan ], [ %si.next, %scan.next ]`,
` %sdone = icmp eq ${sz} %si, %ne`,
` br i1 %sdone, label %miss, label %scan.entry`,
`scan.entry:`,
` %se = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %si`,
` %sk = load i64, ptr %se`,
` %skq = icmp eq i64 %sk, %kb`,
` br i1 %skq, label %scan.found, label %scan.next`,
`scan.found:`,
` ret ptr %se`,
`scan.next:`,
` %si.next = add ${sz} %si, 1`,
` br label %scan.loop`,
// Compact tables tag each 24-bit entry index (+1) with the hash's
// top byte; wider tables take the runtime lookup.
`buckets:`,
` %wide = icmp ugt ${sz} %nb, 16777216`,
` br i1 %wide, label %runtime, label %probe`,
`probe:`,
` %bp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 14`,
` %bk = load ptr, ptr %bp`,
` %mask = sub ${sz} %nb, 1`,
` %ht = lshr i64 %h, 32`,
` %ht32 = trunc i64 %ht to i32`,
` %tag = and i32 %ht32, -16777216`,
sz === "i64" ? ` %hi = add i64 %h, 0` : ` %hi = trunc i64 %h to ${sz}`,
` %i0 = and ${sz} %hi, %mask`,
` br label %probe.loop`,
`probe.loop:`,
` %pi = phi ${sz} [ %i0, %probe ], [ %pi.next, %probe.next ]`,
` %bep = getelementptr inbounds i32, ptr %bk, ${sz} %pi`,
` %b = load i32, ptr %bep`,
` %bempty = icmp eq i32 %b, 0`,
` br i1 %bempty, label %miss, label %probe.tag`,
`probe.tag:`,
` %bt = and i32 %b, -16777216`,
` %btq = icmp eq i32 %bt, %tag`,
` br i1 %btq, label %probe.entry, label %probe.next`,
`probe.entry:`,
` %bix = and i32 %b, 16777215`,
` %bix1 = sub i32 %bix, 1`,
sz === "i32" ? ` %bi = add i32 %bix1, 0` : ` %bi = zext i32 %bix1 to ${sz}`,
` %pe = getelementptr inbounds %ScrMapNumEntry, ptr %ents, ${sz} %bi`,
` %pk = load i64, ptr %pe`,
` %pkq = icmp eq i64 %pk, %kb`,
` br i1 %pkq, label %probe.found, label %probe.next`,
`probe.found:`,
` ret ptr %pe`,
`probe.next:`,
` %pi.inc = add ${sz} %pi, 1`,
` %pi.next = and ${sz} %pi.inc, %mask`,
` br label %probe.loop`,
`miss:`,
` ret ptr null`,
`runtime:`,
` %r = call ptr @scr_map_entry_f64(ptr %m, double %k) memory(read) nounwind willreturn`,
` ret ptr %r`,
`}`,
``,
// Hashing (scr_map_find_f64): the SameValueZero key word and its
// scr_key_mix hash. sc_map_entry_f64_int's probe converts the integer
// view to the key word, which is never NaN or -0, and so skips the
// normalization.
...hashedNumberProbe(
"sc_map_entry_f64_hashed",
"double %k",
sz,
[
` %nan = fcmp uno double %k, %k`,
` %zero = fcmp oeq double %k, 0.0`,
` %kz = select i1 %zero, double 0.0, double %k`,
` %kzb = bitcast double %kz to i64`,
` %kb = select i1 %nan, i64 9221120237041090560, i64 %kzb`,
],
"%k",
),
...hashedNumberProbe(
"sc_map_entry_f64_hashed_int",
"i64 %ki",
sz,
[` %kf = sitofp i64 %ki to double`, ` %kb = bitcast double %kf to i64`],
"%kf",
),
);
}
// Map/Set iteration steps read the entries array inline: an index is
@@ -50,6 +50,7 @@ import {
matchIntegerBytesForLoop,
matchIntegerCountedForLoop,
matchIntegerPairedForLoop,
steppedLoopCounter,
} from "../../ir/integer-loops.js";
import { emitByteWindowGuard, matchByteWindow } from "./byte-windows.js";
import {
@@ -1218,7 +1219,24 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
// has no exit besides its own bound, which LLVM can vectorize. A typed
// array's length is fixed, so each block can take it as its limit when
// the loop does not assign the array's binding.
//
// Inside another loop of this function, which polls at least once
// every WORKER_POLL_BLOCK of its own iterations, a loop needs less:
// - a loop whose counters' ranges bound its runs, and those of the
// loops in its body, to WORKER_POLL_BLOCK^2 needs no poll;
// - an innermost counted loop is strip-mined, typed arrays or not,
// and does not poll after its last block. A run of at most
// WORKER_POLL_BLOCK iterations then never polls, and the countdown
// leaves its body. A run of at most WORKER_UNPOLLED_RUN iterations
// is one block, polled once as it leaves.
const enclosed =
emitter.mod.workers === true &&
emitter.jumpTargets.some((target) => target.kind === "loop");
const bounded =
enclosed && boundedLoopRuns(emitter, s) <= WORKER_POLL_BLOCK * WORKER_POLL_BLOCK;
const innermost = enclosed && !containsLoop(s.body);
const stripMined =
!bounded &&
capturedLets.length === 0 &&
integerSlot !== null &&
((countedLoop !== null &&
@@ -1226,16 +1244,21 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
Math.abs(countedLoop.step) * WORKER_POLL_BLOCK <= 2 ** 52) ||
(integerLoop !== null && integerArrayLoop === null && integerLoop.stableReceiver)) &&
emitter.workerLoopPolls(s) &&
loopAccessesTypedArrays(s);
const workerBudget = stripMined ? null : emitter.workerLoopBudget(s);
(loopAccessesTypedArrays(s) || (countedLoop !== null && innermost));
const workerBudget = stripMined || bounded ? null : emitter.workerLoopBudget(s);
// One slot for every copy of the loop: a speculative copy resumes an
// iteration in another copy's body, which then reads this bound.
let blockBound: string | null = null;
let runSlot: string | null = null;
if (stripMined) {
blockBound = B.slot();
B.entryAllocas.push(
`${blockBound} = alloca ${countedLoop ? "i64" : emitter.sizeType} ; cancellation poll block bound`,
);
if (innermost) {
runSlot = B.slot();
B.entryAllocas.push(`${runSlot} = alloca i1 ; cancellation poll after the run`);
}
}
freshenBindings();
emitter.emitLoopCopies(s, capturedLets.length === 0, le, (exit) => {
@@ -1271,7 +1294,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
return len;
},
};
emitStripMinedCountedLoop(emitter, s, counter, blockBound!, exit);
emitStripMinedCountedLoop(emitter, s, counter, blockBound!, exit, runSlot);
return;
}
const lc = B.newLabel("loop.c");
@@ -2628,6 +2651,60 @@ function emitInt32KindLoop(
/** Iterations between cancellation polls of a strip-mined counted loop. */
const WORKER_POLL_BLOCK = 64;
/** The longest run of a strip-mined loop inside another loop that polls
* only as it leaves. */
const WORKER_UNPOLLED_RUN = 16384;
/** Whether a step of `step` from every value in `range` gives a different
* value. Beyond 2^53 a double can stop changing (`j + 1 === j` from 2^53),
* so a counter there can run without end inside a range of a few values. */
function exactSteps(range: { min: number; max: number }, step: number): boolean {
const size = Math.abs(step);
return range.min >= size - 2 ** 53 && range.max <= 2 ** 53 - size;
}
/** At most how many times a `for` loop and the loops in its body run their
* bodies per entry, from the counters' proven ranges: its iterations times
* one plus the nested loops' runs. Infinity when a count has no bound, or
* depends on a run-time guard (the guard's slow copy steps a double). */
function boundedLoopRuns(emitter: LlEmitter, loop: IrStmt): number {
if (loop.kind !== "for") return Infinity;
const counted = matchIntegerCountedForLoop(
loop,
emitter.numericLocals,
emitter.integerRanges,
LOOPS.of(emitter).unaliasedArrays,
);
let trips = Infinity;
if (counted && !counted.guarded && exactSteps(counted.range, counted.step))
trips = Math.floor((counted.range.max - counted.range.min) / Math.abs(counted.step)) + 1;
else if (!counted) {
// Each iteration closes the gap between the two counters by at least two.
const paired = matchIntegerPairedForLoop(loop, emitter.numericLocals, emitter.integerRanges);
if (paired && !paired.guarded && exactSteps(paired.range, 1))
trips = Math.floor((paired.range.max - paired.range.min) / 2) + 1;
}
if (trips === Infinity) {
// A counter with a proven range where the condition reads it, such as
// one compared with a module constant.
const stepped = steppedLoopCounter(loop, emitter.currentLocals);
const range = stepped && emitter.integerRanges.get(stepped.ref);
if (stepped && range && exactSteps(range, stepped.step))
trips = Math.floor((range.max - range.min) / Math.abs(stepped.step)) + 1;
}
if (!(trips <= WORKER_POLL_BLOCK * WORKER_POLL_BLOCK)) return Infinity;
let nested = 0;
const visit = (s: IrStmt): boolean => {
if (s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf") {
nested += boundedLoopRuns(emitter, s);
return nested !== Infinity;
}
return everyStmtChild(s, () => true, visit);
};
if (!loop.body.every(visit)) return Infinity;
return trips * (1 + nested);
}
/** The counter of a strip-mined loop: its slot, the LLVM integer type and
* signedness of its values, its step, the counters stepped beside it, and
* the limit, emitted in the outer header before each block. */
@@ -2645,13 +2722,20 @@ interface StripMinedCounter {
* tests the counter against the loop's limit, each block runs the body up
* to the nearer of the limit and WORKER_POLL_BLOCK steps on, and the poll
* follows the block. It polls as often as the budget it replaces, and the
* inner loop's only exit besides `break` and `return` is its bound. */
* inner loop's only exit besides `break` and `return` is its bound.
*
* With `runSlot` (for a loop that another loop of the function encloses), a
* block that ends at the limit leaves the loop without polling, and a run
* of at most WORKER_UNPOLLED_RUN iterations is one block, polled once as
* it leaves when it took more than WORKER_POLL_BLOCK iterations. `runSlot`
* holds whether the current block is such a run. */
function emitStripMinedCountedLoop(
emitter: LlEmitter,
s: Extract<IrStmt, { kind: "for" }>,
counter: StripMinedCounter,
boundSlot: string,
exit: string,
runSlot: string | null,
): void {
const B = emitter.B;
const { slot, type, signed, step } = counter;
@@ -2684,7 +2768,27 @@ function emitStripMinedCountedLoop(
`${stepped} = call ${type} @${saturating}(${type} ${start}, ${type} ${step * WORKER_POLL_BLOCK})`,
);
B.line(`${bound} = call ${type} @${nearer}(${type} ${stepped}, ${type} ${limit})`);
B.line(`store ${type} ${bound}, ptr ${boundSlot}`);
if (runSlot === null) B.line(`store ${type} ${bound}, ptr ${boundSlot}`);
else {
// The steps left to the limit: positive, as the header tested.
const left = B.tmp(),
whole = B.tmp(),
long = B.tmp(),
polled = B.tmp(),
end = B.tmp();
const stride = Math.abs(step);
B.line(
ascending
? `${left} = sub ${wrap} ${type} ${limit}, ${start}`
: `${left} = sub ${wrap} ${type} ${start}, ${limit}`,
);
B.line(`${whole} = icmp ule ${type} ${left}, ${stride * WORKER_UNPOLLED_RUN}`);
B.line(`${long} = icmp ugt ${type} ${left}, ${stride * WORKER_POLL_BLOCK}`);
B.line(`${polled} = and i1 ${whole}, ${long}`);
B.line(`${end} = select i1 ${whole}, ${type} ${limit}, ${type} ${bound}`);
B.line(`store ${type} ${end}, ptr ${boundSlot}`);
B.line(`store i1 ${polled}, ptr ${runSlot}`);
}
B.br(bc);
B.startBlock(bc);
const index = B.tmp();
@@ -2693,7 +2797,8 @@ function emitStripMinedCountedLoop(
B.line(`${index} = load ${type}, ptr ${slot}`);
B.line(`${end} = load ${type}, ptr ${boundSlot}`);
B.line(`${inBlock} = icmp ${compare} ${type} ${index}, ${end}`);
B.condBr(inBlock, lb, poll);
const after = runSlot === null ? poll : B.newLabel("loop.end");
B.condBr(inBlock, lb, after);
B.startBlock(lb);
emitter.jumpTargets.push({
kind: "loop",
@@ -2716,11 +2821,45 @@ function emitStripMinedCountedLoop(
B.line(`store ${type} ${next}, ptr ${stepSlot}`);
}
B.br(bc);
if (runSlot !== null) {
// A speculative copy may enter the body without passing the header, so
// the limit is read again here.
B.startBlock(after);
const at = B.tmp();
const more = B.tmp();
const run = B.tmp();
const done = B.newLabel("loop.done");
const last = B.newLabel("loop.poll.run");
B.line(`${at} = load ${type}, ptr ${slot}`);
B.line(`${more} = icmp ${compare} ${type} ${at}, ${counter.limit()}`);
B.condBr(more, poll, done);
B.startBlock(done);
B.line(`${run} = load i1, ptr ${runSlot}`);
B.condBr(run, last, exit);
B.startBlock(last);
emitter.emitWorkerPoll();
B.br(exit);
}
B.startBlock(poll);
emitter.emitWorkerPoll();
B.br(lc);
}
/** Whether `body` holds a loop outside its closures. */
function containsLoop(body: IrStmt[]): boolean {
const inStmt = (s: IrStmt): boolean =>
s.kind === "while" ||
s.kind === "doWhile" ||
s.kind === "for" ||
s.kind === "forOf" ||
!everyStmtChild(
s,
() => true,
(child) => !inStmt(child),
);
return body.some(inStmt);
}
function enterLoopHeader(emitter: LlEmitter, header: string): void {
const dispatch = emitter.speculationHeader;
emitter.speculationHeader = null;
@@ -211,6 +211,19 @@ test("a target CPU level reaches every function attribute group and nothing else
expect(emitLlvmModule(module, { targetCpu: "baseline" })).toBe(plain);
});
test("only sanitized builds mark the emitted functions sanitize_address", () => {
const module = moduleFor([call()]);
const sanitized = emitLlvmModule(module);
expect(emitLlvmModule(module, { sanitizeAttributes: true })).toBe(sanitized);
expect(sanitized).toMatch(/^attributes #0 = \{ sanitize_address \}$/m);
const plain = emitLlvmModule(module, { sanitizeAttributes: false });
expect(plain).not.toContain("sanitize_address");
// A group needs an attribute: an ignored string attribute stands in.
expect(plain).toMatch(/^attributes #0 = \{ "scriptc-function" \}$/m);
const v3 = emitLlvmModule(module, { sanitizeAttributes: false, targetCpu: "x86-64-v3" });
expect(v3).toMatch(/^attributes #0 = \{ "target-cpu"="x86-64-v3" \}$/m);
});
test("inline RC inlines every fast path at speed and keeps box releases as calls in release", () => {
for (const boxed of [false, true]) {
const module = moduleFor(Array.from({ length: 4 }, call), { boxed });
@@ -511,6 +511,12 @@ export interface LlvmTargetOptions {
inlineRc?: InlineRcMode;
/** Define the inline RC fast-path helpers `noinline` (sanitized builds). */
rcHelpersNoinline?: boolean;
/** Mark every emitted function `sanitize_address`, which the sanitized
* lane's `-fsanitize=address` link needs to instrument it. Without a
* sanitizer pass the attribute still turns off shrink-wrapping and
* speculative loads, so plain builds pass false. Defaults to true for
* direct emission. */
sanitizeAttributes?: boolean;
/** Define `scr_detached_exit` so the Linux runtime hands the address-space
* teardown after `process.exit` to a helper process (`--detached-exit`).
* Executables only; the runtime's weak default keeps it off. */
@@ -1273,6 +1279,8 @@ export class LlEmitter {
readonly detachedExit: boolean;
readonly nodeIslandModuleData: Uint8Array | null;
readonly targetCpu: TargetCpu | undefined;
/** Emitted functions carry `sanitize_address` (LlvmTargetOptions). */
readonly sanitizeAttributes: boolean;
readonly wasmBoundsChecks: boolean;
/** Interned string literals: UTF-8 text → { symbol, byte length } —
* first-use order, the runtime ABI’s determinism discipline. */
@@ -1976,6 +1984,7 @@ export class LlEmitter {
this.detachedExit = options.detachedExit === true;
this.nodeIslandModuleData = options.nodeIslandModuleData ?? null;
this.targetCpu = options.targetCpu;
this.sanitizeAttributes = options.sanitizeAttributes !== false;
this.wasmBoundsChecks =
options.wasmBoundsChecks === true ||
wasmBoundsChecks({
@@ -2,11 +2,12 @@ import { emitBorrowedInput } from "./borrowed-inputs.js";
/* Focused LLVM expression emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { undefinedArmTag } from "../../ir/analysis.js";
import { isRefCounted, isUnitType, typeEquals, type IrExpr } from "../../ir/ir.js";
import { isRefCounted, isUnitType, typeEquals, type IrExpr, type IrType } from "../../ir/ir.js";
import { DYN_KIND } from "./dyn.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
import { f64Lit } from "./common.js";
import { emitRecordKeyGetOr } from "./expr-records.js";
import { NULLABLE_NULL, SHAPE_WORD_INDEX, shapeWordHeader } from "./nullable-unions.js";
import { emitUnionTag } from "./union-repr.js";
@@ -397,6 +398,13 @@ export function emitControlExpr(
throw new InternalCompilerError("llvm emitter bug: nullish union lacks unit arms");
const fused = emitNullishOptionalChain(host, e);
if (fused) return fused;
if (
e.left.kind === "recordKeyGet" &&
e.left.missNaN !== true &&
typeEquals(e.type, e.left.type) &&
storesNoUnit(host, e.left)
)
return emitRecordKeyGetOr(host, e.left, e.right);
if (
!typeEquals(e.type, e.left.type) &&
(e.left.kind === "varRef" ||
@@ -700,3 +708,15 @@ function emitUnionTagSet(host: LlvmEmitterContext, set: TagSet): string {
B.line(`${t} = phi i1 [ true, %${yes} ], [ false, %${no} ]`);
return t;
}
/** Every value a keyed read can hit is present (no stored undefined or
* null), so only a missing key yields the read's unit arm. */
function storesNoUnit(host: LlvmEmitterContext, e: ExprOf<"recordKeyGet">): boolean {
const shape = host.recordShape(e.shapeId);
const holdsUnit = (t: IrType): boolean =>
isUnitType(t) ||
t.kind === "dyn" ||
(t.kind === "union" && (host.unionsById.get(t.unionId)?.arms ?? []).some(isUnitType));
if (shape.indexValue !== undefined && holdsUnit(shape.indexValue)) return false;
return e.overflowOnly === true || !shape.fields.some((f) => holdsUnit(f.type));
}
@@ -379,6 +379,39 @@ export function emitRegexIntrinsic(
}
}
/** `r[k] ?? d` in its pass-through shape: a missing key evaluates `d`
* where the read would store undefined, so a hit costs nothing extra. */
export function emitRecordKeyGetOr(
host: LlvmEmitterContext,
e: IrExpr & { kind: "recordKeyGet" },
fallback: IrExpr,
): LlValue {
const B = host.B;
const obj = host.emitExpr(e.obj);
const k = host.emitExpr(e.key);
const ty = host.llType(e.type);
const slot = B.slot();
B.entryAllocas.push(`${slot} = alloca ${ty}`);
const join = B.newLabel("rkg.j");
keyedRecordReadInto(
host,
slot,
join,
obj.name,
k.name,
e.shapeId,
e.type,
e.overflowOnly === true,
e.loc,
false,
() => host.emitBranchInto(slot, fallback),
);
B.startBlock(join);
const t = B.tmp();
B.line(`${t} = load ${ty}, ptr ${slot}`);
return host.own({ name: t, type: e.type });
}
export function emitRecordKeyGet(
host: LlvmEmitterContext,
e: IrExpr & { kind: "recordKeyGet" },
@@ -526,6 +559,7 @@ export function keyedRecordReadInto(
overflowOnly: boolean,
loc?: SrcLoc,
missNaN = false,
missInto: (() => void) | null = null,
): void {
const B = host.B;
const shape = host.recordShape(shapeId);
@@ -728,8 +762,10 @@ export function keyedRecordReadInto(
}
}
if (def !== null && resultType.kind === "union" && undefTag >= 0) {
// The miss path: the result union's undefined arm.
B.line(`store ptr ${host.unitInstanceRef(resultType.unionId, undefTag)}, ptr ${slot}`);
// The miss path: the result union's undefined arm, or the caller's
// own value for a miss (missInto stores it into the slot).
if (missInto !== null) missInto();
else B.line(`store ptr ${host.unitInstanceRef(resultType.unionId, undefTag)}, ptr ${slot}`);
B.br(join);
return;
}
@@ -1151,6 +1151,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "punycode":
case "constants":
case "promise":
case "mime":
return host.emitGenericLibCall(e);
case "nodeIsland":
return host.emitGenericLibCall(e);
@@ -86,6 +86,17 @@ export const LIB_FN_SYMS: Record<string, string> = {
"util.inherits": "scr_util_inherits",
"util.inheritsPrototype": "scr_util_inherits_prototype",
"util.deprecate": "scr_util_deprecate",
"util.isArrayDeprecated": "scr_util_is_array_deprecated",
"util.extend": "scr_util_extend",
"util.signalExitCode": "scr_util_signal_exit_code",
"util.aborted": "scr_util_aborted",
"util.diff": "scr_util_diff",
"util.debounce": "scr_util_debounce",
"util.throttle": "scr_util_throttle",
"mime.typeNew": "scr_mime_type_new",
"mime.typeParse": "scr_mime_type_parse",
"mime.paramsNew": "scr_mime_params_new",
"mime.is": "scr_mime_is",
"util.callbackify": "scr_util_callbackify",
"util.promisifyDyn": "scr_util_promisify",
"util.debuglog": "scr_util_debuglog",
@@ -1084,6 +1095,26 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.generatorThis": "scr_gen_receiver",
"insp.dyn": "scr_insp_dyn",
"insp.dynS": "scr_insp_dyn_s",
"insp.format": "scr_insp_format_pack",
"insp.formatPrimitives": "scr_insp_format_primitives",
"insp.formatList": "scr_insp_format_list",
"insp.formatWithOptions": "scr_insp_format_with_options",
"insp.inspect": "scr_insp_inspect",
"insp.customEnabled": "scr_insp_custom_enabled",
"insp.registerCustom": "scr_insp_register_custom",
"insp.customInspect": "scr_insp_custom_inspect",
"insp.depthKind": "scr_insp_depth_kind",
"insp.customDepth": "scr_insp_custom_depth",
"insp.customOptions": "scr_insp_custom_options",
"insp.inspectFunction": "scr_insp_inspect_function",
"insp.customText": "scr_insp_custom_text",
"insp.customIsSelf": "scr_insp_custom_is_self",
"insp.customResult": "scr_insp_custom_result",
"insp.customCallBegin": "scr_insp_custom_call_begin",
"insp.customCallEnd": "scr_insp_custom_call_end",
"insp.formatSpecifies": "scr_insp_format_specifies",
"insp.formatSpecifiesDyn": "scr_insp_format_specifies_dyn",
"insp.dynFormatS": "scr_insp_dyn_format_s",
"insp.dynHidden": "scr_insp_dyn_hidden",
"fs.readFileSyncDyn": "scr_fs_read_file_sync_dyn",
"fs.readFileEncoded": "scr_fs_read_file_encoded",
@@ -1744,9 +1775,14 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"buffer.deprecation",
// The wrappers schedule warnings and callbacks on the tick queue.
"util.deprecate",
"util.isArrayDeprecated",
"util.extend",
"util.callbackify",
"util.promisifyDyn",
"util.debuglog",
// The wrapped calls arm timers and settle promises.
"util.debounce",
"util.throttle",
"worker.new",
"worker.parentPort",
"worker.channel",
@@ -168,8 +168,10 @@ test("hashed number-key lookups probe inline and call the runtime only for wide
expect(hashed).toContain("select i1 %nan, i64 9221120237041090560, i64 %kzb");
expect(hashed).toContain("mul i64 %x1, -4658895280553007687");
expect(hashed).toContain("mul i64 %x3, -7723592293110705685");
// Filter (field 10), small-map scan of the entry count (field 8), and
// compact buckets (fields 13 and 14) tagged with the hash's top byte.
// Filter (field 10) before the small-map scan of the entry count (field
// 8) only, and compact buckets (fields 13 and 14) tagged with the hash's
// top byte.
expect(hashed).toContain("br i1 %linear, label %filter, label %buckets");
expect(hashed).toContain("getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10");
expect(hashed).toContain(`load i32, ptr %nep`);
expect(hashed).toContain(`load i32, ptr %nbp`);
@@ -192,11 +194,23 @@ test("a key with an integer view tests the direct index on the integer", () => {
integral.module,
)![0];
// An unsigned compare also rejects negative keys; the hashed probe takes
// the double.
// the integer.
expect(helper).toContain("icmp ult i64 %ki, %nd");
expect(helper).toContain(`getelementptr inbounds i32, ptr %dense, ${sz} %i`);
expect(helper).toContain("call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)");
expect(helper).toContain("call ptr @sc_map_entry_f64_hashed_int(ptr %m, i64 %ki)");
expect(helper).not.toContain("fptosi");
// Its key word is the integer's double, which needs no NaN or -0
// normalization; the runtime lookup gets that double.
const hashed =
/^define internal ptr @sc_map_entry_f64_hashed_int\(ptr %m, i64 %ki\) noinline [^]*?^}/m.exec(
integral.module,
)![0];
expect(hashed).toContain("sitofp i64 %ki to double");
expect(hashed).not.toContain("fcmp");
expect(hashed).toContain("mul i64 %x1, -4658895280553007687");
expect(hashed).toContain(
"call ptr @scr_map_entry_f64(ptr %m, double %kf) memory(read) nounwind willreturn",
);
// A fractional key and a key without a proof keep the double lookup.
for (const key of [{ kind: "numLit", value: 0.5, type: F64, loc } as IrExpr, ref("k", F64)]) {
const { work } = emitted(lookupFixture(key), bits);
@@ -4,9 +4,10 @@
* direct index in the runtime (scr_map.c). Reads go through the internal
* sc_map_entry_f64 helper (emitter.ts helperDefs), which LLVM inlines: it
* resolves directly indexed maps without a call and other maps through
* the internal sc_map_entry_f64_hashed probe, which hashes the key as the
* runtime does and defers to scr_map_entry_f64 only for wide bucket
* tables. Both only read memory. The generated code then reads the entry's
* the internal sc_map_entry_f64_hashed probe (sc_map_entry_f64_hashed_int
* for a key with an integer view), which hashes the key as the runtime
* does and defers to scr_map_entry_f64 only for wide bucket tables. Both
* only read memory. The generated code then reads the entry's
* value slot itself and retains a reference value with the value type's own
* retain, which is the adapter the collection was constructed with.
* String-keyed reads use the same entry contract through the internal
@@ -44,6 +44,7 @@ test("sort-heavy programs emit equal LLVM from memory and from serialized IR", a
// compile() emits Linux textual LLVM with the unwinding ABI.
unwindAbi: process.platform === "linux",
objectAudit: sanitize,
sanitizeAttributes: sanitize,
...inlineRcOption(options.optimization, sanitize),
});
const text = typeof emitted === "string" ? emitted : emitted.join("");
+3 -2
View File
@@ -216,8 +216,9 @@ export interface RegionHooks {
classDefinitions(cls: IrClassDef, sizeOf: string): string[];
}
/** Every emitted function/helper carries #0 = { sanitize_address } — see
* the emitter header. */
/** Every emitted function/helper carries attribute group #0: the build's
* target CPU level, and `sanitize_address` in sanitized builds (see
* emit-module.ts functionAttributes). */
export const FN_ATTRS = "#0";
export { computeTraced } from "../cycle-analysis.js";
+2 -2
View File
@@ -110,8 +110,8 @@ export function loopTestsEveryIteration(loop: Loop, tested: (call: IrExpr) => bo
/** True when the body of `loop`, outside its nested loops and closures,
* reads or writes a typed-array element: the loops whose cancellation poll
* the emitter moves between blocks of iterations, so that LLVM can
* vectorize them. Other loops keep the budget in their body, which costs
* less in a loop of a few iterations. */
* vectorize them. Outside another loop, other loops keep the budget in
* their body, which costs less in a loop of a few iterations. */
export function loopAccessesTypedArrays(loop: Loop): boolean {
const inExpr = (e: IrExpr): boolean => {
if (e.kind === "bytesIntrinsic" && e.method === "get") return true;
@@ -64,6 +64,10 @@ export interface NativeLinkFeatures {
/** events.once with options: scr_events_once_signal.c. Absent (not
* false) for programs without it. */
eventsOnceSignal?: boolean;
/** util.MIMEType and util.MIMEParams: scr_mime.c. */
mime?: boolean;
/** util.debounce and util.throttle: scr_util_timing.c. */
utilTiming?: boolean;
/** Buffer.prototype methods called on checked-dynamic values link
* scr_bytes_dyn.c; absent (false) for programs without them. */
dynBuffer?: boolean;
@@ -131,6 +131,10 @@ export interface CcOptions {
/** events.once with options (events.onceSignal): compiles
* scr_events_once_signal.c. */
eventsOnceSignal?: boolean;
/** util.MIMEType and util.MIMEParams: compiles scr_mime.c. */
mime?: boolean;
/** util.debounce and util.throttle: compiles scr_util_timing.c. */
utilTiming?: boolean;
/** Buffer.prototype methods called on checked-dynamic values:
* compiles scr_bytes_dyn.c. */
dynBuffer?: boolean;
@@ -249,6 +249,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
// events.once with options registers through the emitter and
// event-target units.
unit((u) => u.opts.eventsOnceSignal === true, "scr_events_once_signal.c"),
unit((u) => u.opts.mime === true, "scr_mime.c"),
unit((u) => u.opts.utilTiming === true, "scr_util_timing.c"),
// The checked-dynamic HANDLE support unit (listener gate + runtime
// adapter closures): every referencing unit is one of the emitter or
// net families (http implies net), so handle-free binaries keep
@@ -89,6 +89,8 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
declAdopt: boolean;
dynForAwait: boolean;
eventsOnceSignal: boolean;
mime: boolean;
utilTiming: boolean;
dynBuffer: boolean;
builtinValues: boolean;
warningReplace: boolean;
@@ -290,6 +292,8 @@ export function effectiveRuntimeFeatures(
declAdopt: features.declAdopt === true,
dynForAwait: features.dynForAwait === true,
eventsOnceSignal: features.eventsOnceSignal === true,
mime: features.mime === true,
utilTiming: features.utilTiming === true,
dynBuffer: features.dynBuffer === true,
builtinValues: features.builtinValues === true,
warningReplace: features.warningReplace === true,
@@ -77,6 +77,8 @@ export interface EarlyExecutableNativeFeatures {
declAdopt?: boolean;
dynForAwait?: boolean;
eventsOnceSignal?: boolean;
mime?: boolean;
utilTiming?: boolean;
dynBuffer?: boolean;
builtinValues?: boolean;
events: boolean;
@@ -519,6 +519,41 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
return undefined;
}
};
const stringIndex = (checker: ts.TypeChecker, type: ts.Type): boolean => {
try {
return checker
.getIndexInfosOfType(type)
.some((info) => (info.keyType.flags & ts.TypeFlags.String) !== 0);
} catch {
return false;
}
};
/** Whether the JavaScript world types a package value as an object
* literal that lacks a member its declaration promises: a property (an
* expando `out.max = ...` on a non-empty literal, which the JavaScript
* checker does not add to the literal's type) or a string index
* signature (`Record<string, string>` over a literal). Lowering the value
* at the narrower literal type would drop the member or fix the key set,
* so it crosses at its declared type instead. */
const missesDeclaredMembers = (jsType: ts.Type, declType: ts.Type): boolean => {
if (!(jsType.flags & ts.TypeFlags.Object) || !(declType.flags & ts.TypeFlags.Object))
return false;
const literal = jsType.getSymbol();
if (literal === undefined || (literal.flags & ts.SymbolFlags.ObjectLiteral) === 0) return false;
try {
if (declChecker.isArrayType(declType) || declChecker.isTupleType(declType)) return false;
if (stringIndex(declChecker, declType) && !stringIndex(jsChecker, jsType)) return true;
return declChecker
.getPropertiesOfType(declType)
.some(
(prop) =>
!String(prop.escapedName).startsWith("__@") &&
jsChecker.getPropertyOfType(jsType, String(prop.escapedName)) === undefined,
);
} catch {
return false;
}
};
const namesPackageSymbol = (node: ts.Node): boolean => {
let found = false;
const visit = (n: ts.Node): void => {
@@ -540,7 +575,7 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
const fileEdits: Edit[] = [];
/** Candidate expressions (by js node) with their declared type, named
* only for the casts that survive suppression. */
const exprCandidates = new Map<ts.Node, ts.Type>();
const exprCandidates = new Map<ts.Node, ts.Type | "any">();
const exprOrder: ts.Node[] = [];
/** Reads whose projectable receiver the JavaScript world types `any`:
* kept only when nothing types the receiver. */
@@ -610,7 +645,14 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
return;
}
const jsType = typeAt(jsChecker, jsNode);
if (!isAnyish(jsType)) return;
if (jsType === null) return;
// A typed JavaScript value crosses only where its object type lacks a
// member the declaration promises (missesDeclaredMembers).
const typedJs = !isAnyish(jsType);
if (typedJs) {
const declType = typeAt(declChecker, declNode);
if (declType === null || !missesDeclaredMembers(jsType, declType)) return;
}
// Package-produced?
const d = declNode as ts.Expression;
let produced = false;
@@ -686,7 +728,18 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
// no contextual type
}
const declType = typeAt(declChecker, d);
if (declType === null || !printer.projectable(declType)) return;
if (declType === null) return;
if (!printer.projectable(declType)) {
// An index signature over values that do not project (package
// interfaces): the literal's fixed key set would still answer for
// keys it lacks, so the value stays untyped and every read is the
// JavaScript read.
if (typedJs && stringIndex(declChecker, declType)) {
exprCandidates.set(jsNode, "any");
exprOrder.push(jsNode);
}
return;
}
exprCandidates.set(jsNode, declType);
exprOrder.push(jsNode);
};
@@ -890,7 +943,8 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
)
continue;
}
const alias = printer.project(exprCandidates.get(e)!);
const candidate = exprCandidates.get(e)!;
const alias = candidate === "any" ? "any" : printer.project(candidate);
if (alias === null) continue;
const start = ts.tokenStartOfNode(e);
const stmt = e.parent;
@@ -1,3 +1,4 @@
import { widensReducedRecords } from "../function-returns.js";
import { keyedHelperName } from "../stable-names.js";
import { isDateJsonSource } from "./dates.js";
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
@@ -96,7 +97,14 @@ export function lowerJsonMethodCall(
!isDateJsonSource(lowerer, argNode)
) {
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(argNode));
if (narrowed && !isUnitType(narrowed) && narrowed.kind !== "void") {
// A union of record types the checker's subtype reduction spelled as
// one of them serializes each value by its own layout.
if (
narrowed &&
!isUnitType(narrowed) &&
narrowed.kind !== "void" &&
!widensReducedRecords(lowerer, narrowed, value.type)
) {
const helper =
narrowed.kind === "union"
? lowerer.narrowedRetagHelper(argNode, value.type.unionId, narrowed.unionId, loc)
@@ -0,0 +1,180 @@
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
import { dynUndefinedExpr, numLit } from "../../../ir/build.js";
import { BOOL, DYN, type IrExpr, type IrLibFn } from "../../../ir/ir.js";
import { builtinImportOf } from "./module-bindings.js";
/* node:util's MIMEType and MIMEParams are native checked-dynamic handles
* (scr_mime.c): both types map to dyn, their getters and setters are
* handle property reads and writes, their methods dispatch through the
* handle, and these helpers lower the uses a dyn value cannot answer by
* itself (construction, MIMEType.parse and instanceof). */
type MimeClass = "MIMEType" | "MIMEParams";
/** The MIMEType or MIMEParams class in any node:util spelling. */
export function mimeClassOf(lowerer: Lowerer, node: ts.Expression): MimeClass | null {
const builtin = ts.isIdentifier(node)
? builtinImportOf(lowerer, node)
: ts.isPropertyAccessExpression(node) && !node.questionDotToken
? lowerer.builtinMemberOf(node)
: null;
if (builtin?.module !== "util") return null;
return builtin.member === "MIMEType" || builtin.member === "MIMEParams" ? builtin.member : null;
}
/** The class of a stdlib MIMEType- or MIMEParams-typed value. */
function mimeValueClass(lowerer: Lowerer, node: ts.Expression): MimeClass | null {
const type = lowerer.typeOf(node);
const sym = type.getAliasSymbol() ?? type.getSymbol();
if (!sym) return null;
const name = ts.symbolName(sym);
if (name !== "MIMEType" && name !== "MIMEParams") return null;
return lowerer.isStdlibSymbol(sym) ? name : null;
}
/** The call of `fn` over the first `count` arguments, as checked-dynamic
* values (missing ones as undefined). Every argument evaluates in order
* before the call, as in JavaScript: with surplus arguments, the used ones
* are held in hidden locals while the others evaluate. */
function mimeCall(
lowerer: Lowerer,
fn: IrLibFn,
nodes: readonly ts.Expression[],
count: number,
loc: ReturnType<typeof locOf>,
): IrExpr {
const values = nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN));
if (values.length <= count) {
const args = [...values];
while (args.length < count) args.push(dynUndefinedExpr(loc));
return { kind: "libCall", fn, args, type: DYN, loc };
}
const locals = values.slice(0, count).map((value) => ({
value,
local: lowerer.declareHiddenLocal("%mimeArg", DYN),
}));
return {
kind: "seqExpr",
stmts: [
...locals.map(({ value, local }) => ({
kind: "varDecl" as const,
localId: local.id,
init: value,
loc,
})),
...values.slice(count).map((arg) => ({ kind: "exprStmt" as const, expr: arg, loc: arg.loc })),
],
result: {
kind: "libCall",
fn,
args: locals.map(({ local }): IrExpr => ({
kind: "varRef",
localId: local.id,
type: DYN,
loc,
})),
type: DYN,
loc,
},
type: DYN,
loc,
};
}
/** `new MIMEType(string[, noThrow])` and `new MIMEParams()`. */
export function lowerMimeNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
const cls = mimeClassOf(lowerer, expr.expression);
if (cls === null) return null;
const nodes = expr.arguments?.nodes ?? [];
if (nodes.some(ts.isSpreadElement)) return null;
const loc = locOf(expr);
const fn: IrLibFn = cls === "MIMEType" ? "mime.typeNew" : "mime.paramsNew";
return mimeCall(lowerer, fn, nodes, cls === "MIMEType" ? 2 : 0, loc);
}
const INSTANCE_METHODS: Record<MimeClass, ReadonlySet<string>> = {
MIMEType: new Set(["toString", "toJSON"]),
MIMEParams: new Set([
"get",
"has",
"set",
"delete",
"entries",
"keys",
"values",
"toString",
"toJSON",
]),
};
/** MIMEType.parse(), and the instance methods, which the handle answers
* as methods, so they dispatch through dynInvoke rather than as a member
* read and a call. */
export function lowerMimeCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null;
const access = call.expression;
if (access.questionDotToken || call.arguments.nodes.some(ts.isSpreadElement)) return null;
const name = access.name.text;
const loc = locOf(call);
if (name === "parse" && mimeClassOf(lowerer, access.expression) === "MIMEType") {
return mimeCall(lowerer, "mime.typeParse", call.arguments.nodes, 1, loc);
}
const cls = mimeValueClass(lowerer, access.expression);
if (cls === null || !INSTANCE_METHODS[cls].has(name)) return null;
return {
kind: "dynInvoke",
recv: lowerer.lowerExprExpecting(access.expression, DYN),
method: name,
calleeName: ts.getTextOfNode(access),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
};
}
/** Reports SC2020 when `access` reads a static member of MIMEType or
* MIMEParams (`MIMEType.prototype`, `MIMEType.name`); null otherwise.
* JavaScript sources hold either class as an identity token, which has no
* members, so the read would answer undefined. MIMEType.parse calls lower
* in lowerMimeCall and never reach the member read. */
export function fenceMimeStaticRead(lowerer: Lowerer, access: ts.PropertyAccessExpression): null {
const cls = mimeClassOf(lowerer, access.expression);
if (cls !== null) lowerer.noLowering(`util.${cls}.${access.name.text}`, access);
return null;
}
/** `v instanceof MIMEType` / `v instanceof MIMEParams`. */
export function lowerMimeInstanceof(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr | null {
const cls = mimeClassOf(lowerer, expr.right);
if (cls === null) return null;
const loc = locOf(expr);
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
return {
kind: "libCall",
fn: "mime.is",
args: [value, numLit(cls === "MIMEType" ? 0 : 1, loc)],
type: BOOL,
loc,
};
}
/** `params`, `params.keys()`, `params.values()` or `params.entries()` over a
* MIMEParams: its checked-dynamic view iterates the live entry list, like
* URLSearchParams (isSearchParamsIterable). */
export function isMimeParamsIterable(lowerer: Lowerer, node: ts.Expression): boolean {
while (ts.isParenthesizedExpression(node)) node = node.expression;
if (mimeValueClass(lowerer, node) === "MIMEParams") return true;
if (
!ts.isCallExpression(node) ||
node.arguments.nodes.length !== 0 ||
!ts.isPropertyAccessExpression(node.expression)
)
return false;
const name = node.expression.name.text;
return (
(name === "keys" || name === "values" || name === "entries") &&
mimeValueClass(lowerer, node.expression.expression) === "MIMEParams"
);
}
@@ -29,11 +29,13 @@ import { checkFsUrlPaths, isFsPathParam, lowerFallibleFsUrlCall, lowerFsPath } f
import { lowerBufferEncodingCall } from "./text-codecs.js";
import { lowerUtilTypeCall } from "./util-types.js";
import { InternalCompilerError } from "../../../errors.js";
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { type BuiltinModuleFn } from "../surfaces.js";
import { type ParamShape } from "../call-signatures.js";
import { BYTES_U8, F64, type IrExpr, STRING, type SrcLoc, arrayOf } from "../../../ir/ir.js";
import { BYTES_U8, DYN, F64, type IrExpr, STRING, type SrcLoc, arrayOf } from "../../../ir/ir.js";
import { typeKey } from "../../type-mapper.js";
import { lowerProcessLoadEnvFile } from "./environment.js";
import { lowerFsWatchCall, lowerFsReaddirTypesCall, lowerFsStatOptionsCall } from "./filesystem.js";
import { lowerZlibModuleCall } from "./compression.js";
@@ -386,7 +388,65 @@ export function lowerBuiltinModuleCall(
const lowered = lowerFsReadPathCall(lowerer, expr, loc);
if (lowered) return lowered;
}
const required = fn.params.length - (fn.defaults?.length ?? 0);
// util._extend over a typed target: the writes go through its
// checked-dynamic view, and the call answers the target itself, as Node
// does, when the result type is the target's.
if (
bi.module === "util" &&
bi.member === "_extend" &&
expr.arguments.nodes.length >= 1 &&
!expr.arguments.nodes.some(ts.isSpreadElement)
) {
const target = lowerer.lowerExpr(expr.arguments.nodes[0]!);
const resultType = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (
target.type.kind !== "dyn" &&
resultType !== null &&
typeKey(resultType) === typeKey(target.type)
) {
const held = lowerer.declareHiddenLocal("%extendTarget", target.type);
const ref = varRef(held.id, target.type, loc);
const source = expr.arguments.nodes[1]
? lowerer.lowerExprExpecting(expr.arguments.nodes[1], DYN)
: dynUndefinedExpr(loc);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: held.id, init: target, loc },
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "util.extend",
args: [lowerer.coerceToExpected(ref, DYN), source],
type: DYN,
loc,
},
loc,
},
],
result: ref,
type: target.type,
loc,
};
}
}
// Trailing checked-dynamic parameters whose value adapter defaults to
// undefined may be omitted: the call passes undefined, as Node sees it.
let undefinedTail = 0;
if (!fn.defaults && fn.valueParams) {
for (let i = fn.valueParams.length - 1; i >= 0; i--) {
const param = fn.valueParams[i]!;
if (
param.mode !== "optional" ||
param.defaultValue !== undefined ||
fn.params[i]?.kind !== "dyn"
)
break;
undefinedTail++;
}
}
const required = fn.params.length - (fn.defaults?.length ?? undefinedTail);
const hasSpread = expr.arguments.nodes.some(ts.isSpreadElement);
if (
bi.module === "url" &&
@@ -466,12 +526,9 @@ export function lowerBuiltinModuleCall(
: {
type,
mode: "omittable",
callDefault: {
kind: "strLit",
value: fn.defaults![i - required]!,
type: STRING,
loc,
},
callDefault: fn.defaults
? { kind: "strLit", value: fn.defaults[i - required]!, type: STRING, loc }
: dynUndefinedExpr(loc),
},
);
args = lowerer.completeArgs(expr.arguments.nodes, shapes, loc, expr);
@@ -482,7 +539,11 @@ export function lowerBuiltinModuleCall(
: lowerer.lowerExprExpecting(a, fn.params[i]),
);
for (let i = args.length; i < fn.params.length; i++) {
const dflt = fn.defaults![i - required]!;
if (!fn.defaults) {
args.push(dynUndefinedExpr(loc));
continue;
}
const dflt = fn.defaults[i - required]!;
args.push({ kind: "strLit", value: dflt, type: STRING, loc });
}
}
@@ -1,7 +1,8 @@
import { keyedHelperName } from "../stable-names.js";
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { varRef } from "../../../ir/build.js";
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
import { isJsSourceFile } from "../../program.js";
import {
BOOL,
DYN,
@@ -92,12 +93,22 @@ function callbackShape(
return { fnT, resultT, args, callbackT, callbackParamT: callbackParamT!, inner, error };
}
/** Whether `promisify(fn)`'s one argument is typed `any`: the function is
* only known at run time, so the promisified function is built there. */
/** Whether `promisify(fn)`'s one argument is only known at run time, so
* the promisified function is built there: a value typed `any`, a function
* with untyped parameters (the declarations type that call `any`), or any
* argument in JavaScript, whose inferred types do not describe the
* function Node receives (it validates the argument and reads its own
* properties). */
export function untypedPromisifyArgument(lowerer: Lowerer, call: ts.CallExpression): boolean {
if (call.questionDotToken || call.arguments.nodes.length !== 1) return false;
const argNode = call.arguments.nodes[0]!;
return !ts.isSpreadElement(argNode) && (lowerer.typeOf(argNode).flags & ts.TypeFlags.Any) !== 0;
if (call.questionDotToken || call.arguments.nodes.length > 1) return false;
const argNode = call.arguments.nodes[0];
if (argNode !== undefined && ts.isSpreadElement(argNode)) return false;
if (isJsSourceFile(ts.getSourceFileOfNode(call))) return true;
if (argNode === undefined) return false;
return (
(lowerer.typeOf(argNode).flags & ts.TypeFlags.Any) !== 0 ||
(lowerer.typeOf(call).flags & ts.TypeFlags.Any) !== 0
);
}
/** The callback shape of a `promisify(fn)` call, or null. */
@@ -144,10 +155,11 @@ export function lowerCallbackPromisify(
if (shape === null) {
// An untyped (`any`) function: Node's promisify at run time.
if (!untypedPromisifyArgument(lowerer, call)) return null;
const argNode = call.arguments.nodes[0];
return {
kind: "libCall",
fn: "util.promisifyDyn",
args: [lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)],
args: [argNode ? lowerer.lowerExprExpecting(argNode, DYN) : dynUndefinedExpr(loc)],
type: DYN,
loc,
};
@@ -57,6 +57,95 @@ function emptyTupleSpread(lowerer: Lowerer, node: ts.Expression): boolean {
return restTupleElementTypes(lowerer.typeOf(operand), lowerer.typeCtx)?.length === 0;
}
/** A call's arguments as one checked-dynamic array, built in source order
* into a fresh hidden local: each argument converts to dyn, and a spread
* contributes the values its iteration yields (holes as undefined).
* `lowerSpread` lowers a spread's operand; the default lowers it here. */
export function dynArgumentPack(
lowerer: Lowerer,
argNodes: readonly ts.Expression[],
loc: SrcLoc,
options: {
/** Pre-lowered values pushed before the arguments. */
leading?: readonly IrExpr[] | undefined;
blame?: ts.Node | undefined;
lowerSpread?: (arg: ts.SpreadElement) => IrExpr;
} = {},
): { stmts: IrStmt[]; pack: () => IrExpr } {
const blame = options.blame ?? argNodes[0]!;
const lowerSpread =
options.lowerSpread ?? ((arg: ts.SpreadElement) => lowerer.lowerExpr(arg.expression));
const pack = lowerer.declareHiddenLocal("%spreadArgs", DYN);
const ref = (): IrExpr => ({ kind: "varRef", localId: pack.id, type: DYN, loc });
const stmts: IrStmt[] = [
{
kind: "varDecl",
localId: pack.id,
init: { kind: "dynArrLit", elems: [], type: DYN, loc },
loc,
},
];
const push = (value: IrExpr, fn: "dyn.packPush" | "dyn.packPushSpreadIter") => {
stmts.push({
kind: "exprStmt",
expr: { kind: "libCall", fn, args: [ref(), value], type: VOID, loc: value.loc },
loc: value.loc,
});
};
for (const value of options.leading ?? [])
push(lowerer.coerceInto(blame, value, DYN), "dyn.packPush");
const spreads = argNodes.filter(ts.isSpreadElement).length;
argNodes.forEach((arg, index) => {
if (!ts.isSpreadElement(arg)) {
push(lowerer.lowerExprExpecting(arg, DYN), "dyn.packPush");
return;
}
let value = lowerSpread(arg);
if (value.type.kind === "set") {
value = {
kind: "setIntrinsic",
method: "toArray",
receiver: value,
args: [],
type: arrayOf(value.type.elem),
loc: value.loc,
};
} else if (value.type.kind === "object") {
value = lowerer.classIteratorDrainCall(value, locOf(arg)) ?? value;
}
if (value.type.kind === "array") {
// Argument iteration materializes holes as present undefined
// before conversion to the dense checked-dynamic argument pack.
value = {
kind: "arrayLit",
elems: [value],
spreads: [0],
type: value.type,
loc: value.loc,
};
}
value = checkedIterableSpread(lowerer, value, ts.getTextOfNode(arg.expression), loc);
if (spreads === 1 && index === argNodes.length - 1) {
stmts.push({
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "dyn.packPushSpread",
args: [
ref(),
value,
{ kind: "strLit", value: ts.getTextOfNode(arg.expression), type: STRING, loc },
],
type: VOID,
loc,
},
loc,
});
} else push(value, "dyn.packPushSpreadIter");
});
return { stmts, pack: ref };
}
export function completeArgs(
lowerer: Lowerer,
argNodes: readonly ts.Expression[],
@@ -97,72 +186,10 @@ export function completeArgs(
// Runtime-length spreads determine the complete argument list before
// any parameter default runs. Build it once in source order, then
// extract the fixed native ABI slots; missing elements are undefined.
const pack = lowerer.declareHiddenLocal("%spreadArgs", DYN);
const ref = (): IrExpr => ({ kind: "varRef", localId: pack.id, type: DYN, loc });
const stmts: IrStmt[] = [
{
kind: "varDecl",
localId: pack.id,
init: { kind: "dynArrLit", elems: [], type: DYN, loc },
loc,
},
];
const push = (value: IrExpr, fn: "dyn.packPush" | "dyn.packPushSpreadIter") => {
stmts.push({
kind: "exprStmt",
expr: { kind: "libCall", fn, args: [ref(), value], type: VOID, loc: value.loc },
loc: value.loc,
});
};
for (const value of leading ?? []) push(lowerer.coerceInto(blame, value, DYN), "dyn.packPush");
const spreads = argNodes.filter(ts.isSpreadElement).length;
argNodes.forEach((arg, index) => {
if (!ts.isSpreadElement(arg)) {
push(lowerer.lowerExprExpecting(arg, DYN), "dyn.packPush");
return;
}
let value = lowerSpread(arg);
if (value.type.kind === "set") {
value = {
kind: "setIntrinsic",
method: "toArray",
receiver: value,
args: [],
type: arrayOf(value.type.elem),
loc: value.loc,
};
} else if (value.type.kind === "object") {
value = lowerer.classIteratorDrainCall(value, locOf(arg)) ?? value;
}
if (value.type.kind === "array") {
// Argument iteration materializes holes as present undefined
// before conversion to the dense checked-dynamic argument pack.
value = {
kind: "arrayLit",
elems: [value],
spreads: [0],
type: value.type,
loc: value.loc,
};
}
value = checkedIterableSpread(lowerer, value, ts.getTextOfNode(arg.expression), loc);
if (spreads === 1 && index === argNodes.length - 1) {
stmts.push({
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "dyn.packPushSpread",
args: [
ref(),
value,
{ kind: "strLit", value: ts.getTextOfNode(arg.expression), type: STRING, loc },
],
type: VOID,
loc,
},
loc,
});
} else push(value, "dyn.packPushSpreadIter");
const { stmts, pack: ref } = dynArgumentPack(lowerer, argNodes, loc, {
leading,
blame,
lowerSpread,
});
const args = shapes.map((shape, index): IrExpr => {
if (shape.mode === "arguments") return ref();
@@ -1,4 +1,6 @@
import type { IrType } from "../../../ir/ir.js";
import type * as ts from "../../ts7/adapter.js";
import type { Lowerer } from "../lowerer.js";
/** Per-compilation conversion state. Recursive plans are scoped to the
* active planning stack; only fully validated copy routes are memoized. */
@@ -14,8 +16,28 @@ export class CoercionState {
// The extracted arm of each checked (non-defaulting) extraction helper.
readonly checkedNarrowArms = new Map<
string,
{ unionId: string; tag: number; target: IrType; message?: string }
{ unionId: string; tag: number; target: IrType; widens?: true; message?: string }
>();
readonly retags = new Map<string, string>();
readonly narrows = new Map<string, string>();
readonly widthNarrows = new Map<string, string>();
/** Unions widenReducedReturn produced: only these read through the
* width-converting extraction (widthNarrowHelper). */
readonly widenedReturns = new Set<string>();
/** widenReducedReturn's results by declaration and inferred return type,
* for the results computed without cutting a widening cycle through
* another function. Each keeps the type-parameter bindings it was mapped
* under and answers only under the same bindings. */
readonly widenedReturnTypes = new Map<ts.Node, Map<ts.Type, WidenedReturn>>();
}
/** A widened inferred return type and the lowering state that mapped it
* (Lowerer.typeParamBindings and its checker-type and implicit-any twins,
* and whether an erased generic body is lowering). */
export interface WidenedReturn {
type: IrType;
bindings: Lowerer["typeParamBindings"];
tsBindings: Lowerer["typeParamTsBindings"];
implicit: Lowerer["implicitParamTypes"];
erasing: boolean;
}
@@ -128,6 +128,25 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
const route = presenceRoute(lowerer, src.shapeId, selected[0]!, selected[1]!);
if (route) return route;
}
// A record whose fields some arm keeps EVERY one of, each lifting
// (`{ id; limit; opts?: A | B }` into `{ id; limit; opts?: A | B | C } |
// { id; opts?: C }`): that arm is the value's own layout, widened; the
// others hold it only by dropping fields. Prefer the unique lossless arm.
if (selected.length > 1 && src.kind === "record") {
const source = lowerer.shapes.get(src.shapeId);
const lossless = selected.filter(({ arm }) => {
const target = arm.kind === "record" ? lowerer.shapes.get(arm.shapeId) : undefined;
return (
source !== undefined &&
target !== undefined &&
!source.tuple &&
!target.tuple &&
target.fields.length === source.fields.length &&
source.fields.every((field) => target.fields.some((kept) => kept.name === field.name))
);
});
if (lossless.length === 1) selected = lossless;
}
if (selected.length !== 1) return null;
return { how: "liftWrap", tag: selected[0]!.tag, arm: selected[0]!.arm };
}
@@ -437,6 +437,47 @@ export function narrowedArmHelper(
return name;
}
/** Interned `%union.narrow.width.<n>(u)`: the checked extraction of a
* record arm from a union that also holds records whose fields strictly
* include the arm's (a function's widened inferred return, see
* widenReducedReturn). Those arms convert like any record width coercion;
* every other arm throws like narrowedArmHelper's. */
export function widthNarrowHelper(
lowerer: Lowerer,
fromId: string,
target: IrType,
loc: SrcLoc,
): string | null {
const from = lowerer.unions.get(fromId);
if (!from || target.kind !== "record") return null;
const tag = lowerer.armTag(fromId, target);
if (tag < 0) return null;
const key = `${fromId}:${tag}`;
const existing = lowerer.coercions.widthNarrows.get(key);
if (existing) return existing;
const name = keyedHelperName(lowerer, "%union.narrow.width", key);
lowerer.coercions.widthNarrows.set(key, name);
const fn = buildUnionNarrow(
name,
from,
target,
loc,
(type) => lowerer.fmt(type),
null,
null,
(armTag, payload) => widthSubtypeConversion(lowerer, from.arms[armTag]!, target, payload, loc),
);
if (!fn) throw new InternalCompilerError("lowerer bug: invalid checked union extraction");
lowerer.coercions.checkedNarrowArms.set(name, {
unionId: fromId,
tag,
target: from.arms[tag]!,
widens: true,
});
lowerer.liftedFns.push(fn);
return name;
}
/** A checked extraction at its use. A plain read tests the stored tag in
* place and projects the payload directly, so member reads can borrow it;
* every other arm throws the helper's TypeError with the same message.
@@ -458,7 +499,8 @@ export function checkedNarrowCall(
value.type.kind !== "union" ||
value.type.unionId !== arm.unionId ||
!typeEquals(type, arm.target) ||
!isSafeToRepeat(value)
!isSafeToRepeat(value) ||
arm.widens === true
)
return call;
const unionId = arm.unionId;
@@ -616,3 +658,31 @@ export function deferredReadHelper(
lowerer.liftedFns.push(fn);
return name;
}
/** Whether `arm` is a record whose fields strictly include those of the
* record `target`: a value of the target type under TypeScript's structural
* typing, which the checker's subtype reduction may have spelled as the
* target (`{ status } | { status, value }` infers `{ status }`). */
export function isWidthSubtypeRecord(lowerer: Lowerer, arm: IrType, target: IrType): boolean {
if (arm.kind !== "record" || target.kind !== "record") return false;
const own = lowerer.shapes.get(arm.shapeId);
const want = lowerer.shapes.get(target.shapeId);
if (!own || !want || own.tuple || want.tuple || own.indexValue || want.indexValue) return false;
if (want.fields.length >= own.fields.length) return false;
const names = new Set(own.fields.map((f) => f.name));
return want.fields.every((f) => names.has(f.name));
}
/** A checked narrow that meets a width-subtype record arm converts it like
* any record width coercion instead of reporting a lying narrowing. */
function widthSubtypeConversion(
lowerer: Lowerer,
arm: IrType,
target: IrType,
payload: IrExpr,
loc: SrcLoc,
): IrExpr | null {
if (!isWidthSubtypeRecord(lowerer, arm, target)) return null;
const lift = lowerer.widthLiftPlan(arm, target);
return lift === null ? null : lowerer.applyWidthLift(lift, payload, target, loc);
}
@@ -44,6 +44,7 @@ import {
} from "../collection-ingestion.js";
import { strCharsCall } from "./array-construction.js";
import { arrayElementRead } from "./array-iteration.js";
import { isMimeParamsIterable } from "../builtins/mime.js";
import { isSearchParamsIterable } from "../builtins/url.js";
import { classGeneratorIterable } from "../class-iterables.js";
import { generatorDrain, lowerCollectionIteratorValue } from "../iterator-adapters.js";
@@ -1271,8 +1272,8 @@ export function lowerMapSeedNew(
);
}
// new Map(sp) / new Map(sp.entries()): the checked-dynamic view's live
// pairs seed the map in order.
if (isSearchParamsIterable(lowerer, argNode)) {
// pairs seed the map in order, and so do a MIMEParams's entries.
if (isSearchParamsIterable(lowerer, argNode) || isMimeParamsIterable(lowerer, argNode)) {
return mapFromSeedValue(lowerer, lowerer.lowerExprExpecting(argNode, DYN), mapT);
}
const input = collectionInput(lowerer, argNode);
@@ -29,6 +29,7 @@ import { lowerTernary } from "./narrowing.js";
import { lowerCollectionSpread } from "../containers/collection-methods.js";
import { strCharsCall } from "../containers/array-construction.js";
import { generatorDrain } from "../iterator-adapters.js";
import { isMimeParamsIterable } from "../builtins/mime.js";
import { isSearchParamsIterable } from "../builtins/url.js";
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
import { arrayValueStore, primitiveElement } from "../array-values.js";
@@ -543,7 +544,9 @@ export function lowerArrayLiteral(
// `[...sp]` and `[...sp.entries()]` over a URLSearchParams: its
// checked-dynamic view iterates the live pair list, and the drained
// [name, value] pairs convert to the literal's tuple elements.
const searchParamsSource = isSearchParamsIterable(lowerer, el.expression);
const searchParamsSource =
isSearchParamsIterable(lowerer, el.expression) ||
isMimeParamsIterable(lowerer, el.expression);
if (src.type.kind === "searchParams") src = lowerer.coerceInto(el.expression, src, DYN);
// A native checked iterable can supply scalar elements even when
// the checker inferred a typed array from its producer. Drain once
@@ -1551,6 +1551,9 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
reference.kind === "map" ||
reference.kind === "set" ||
reference.kind === "union" ||
// A promise of dyn boxes its own promise, so identity holds;
// other promise types box a fresh adapter.
(reference.kind === "promise" && reference.inner.kind === "dyn") ||
DYN_HANDLE_KINDS.has(reference.kind)) &&
lowerer.dynConvertible(reference)
) {
@@ -2388,6 +2391,34 @@ export function lowerStrictEquality(
loc,
};
}
// A promise against a union with a promise arm, or two such unions:
// converting a promise into a union's promise arm of another payload
// (`chains.get(id) === tail` with a `Map<K, Promise<unknown>>`, or
// `a.pending === b.pending` after both were set to one promise) builds a
// new promise, so either side may hold a conversion of the other's
// promise, and a value narrowed out of the union may be one too.
// Promises are objects to ===, so compare both sides as untyped values,
// where a converted promise answers with its origin's identity (the plain
// promise-vs-promise rule below).
const holdsPromise = (type: IrType): boolean =>
type.kind === "promise" ||
(type.kind === "union" &&
(lowerer.unions.get(type.unionId)?.arms ?? []).some((arm) => arm.kind === "promise"));
if (
holdsPromise(left.type) &&
holdsPromise(right.type) &&
lowerer.dynConvertible(left.type) &&
lowerer.dynConvertible(right.type)
) {
return {
kind: "dynScalarEq",
left: lowerer.coerceToExpected(left, DYN),
right: lowerer.coerceToExpected(right, DYN),
...(negated ? { negated: true as const } : {}),
type: BOOL,
loc,
};
}
if ((sameUnion || !bothUnion) && lowerer.eqComparableUnion(ut.unionId)) {
aliasRefusal(ut);
return {
@@ -70,6 +70,7 @@ import { InternalCompilerError } from "../../../errors.js";
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../../ir/build.js";
import { npmStaticPackageOfPath } from "../../npm-static.js";
import { isSafeToRepeat } from "./evaluation-safety.js";
import { withInheritedMethods } from "../record-inherited.js";
import { lowerClassPrototypeComputedAssignment } from "../class-prototypes.js";
import { isClassCallback } from "../class-callbacks.js";
import { isKeyedFieldAccess } from "./keyed-field-targets.js";
@@ -1445,7 +1446,17 @@ export function lowerRecordKeyRead(
loc,
};
if (nanCoded) return read;
return includeUndefined || optionalRead ? read : lowerer.maybeNarrow(read, expr);
const answered = withInheritedMethods(lowerer, read, litKey, isCalledAccess(expr));
return includeUndefined || optionalRead ? answered : lowerer.maybeNarrow(answered, expr);
}
/** The access is the callee of a call (`o[k](…)`, `o[k]?.(…)`, `o[k]!(…)`),
* which passes the object as `this`. */
function isCalledAccess(node: ts.Expression): boolean {
let child: ts.Expression = node;
while (ts.isParenthesizedExpression(child.parent) || ts.isNonNullExpression(child.parent))
child = child.parent;
return ts.isCallExpression(child.parent) && child.parent.expression === child;
}
/** ToPropertyKey for represented record keys. Missing array-derived string
@@ -34,7 +34,11 @@ import {
} from "../../type-mapper.js";
import { isSafeToRepeat } from "./evaluation-safety.js";
import { tryLowerExpression } from "./try-lower-expression.js";
import { fenceSymbolFieldCopy } from "../symbol-fields.js";
import {
classSymbolKeyOf,
fenceSymbolFieldCopy,
symbolFieldDisplayName,
} from "../symbol-fields.js";
import { planUnionRetag } from "../union-retag.js";
import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js";
import { builtinGlobalValue } from "./property-access.js";
@@ -197,7 +201,19 @@ function propNameText(lowerer: Lowerer, name: ts.PropertyName): string {
* (NamedEvaluation): an identifier, string or numeric key, or a computed
* key that folds to one string. Null for other keys. */
function propertyFunctionName(lowerer: Lowerer, name: ts.PropertyName): string | null {
if (ts.isComputedPropertyName(name)) return literalComputedKey(lowerer, name);
if (ts.isComputedPropertyName(name)) {
const literal = literalComputedKey(lowerer, name);
if (literal !== null) return literal;
// A symbol key names the function `[description]`.
let key = name.expression;
while (ts.isParenthesizedExpression(key)) key = key.expression;
const symbol = classSymbolKeyOf(lowerer, key);
if (symbol) return `[${symbolFieldDisplayName(symbol.fieldName).slice("Symbol(".length, -1)}]`;
const wellKnown = ts.isPropertyAccessExpression(key)
? lowerer.stdlibGlobalMember(key, "Symbol")
: null;
return wellKnown ? `[Symbol.${wellKnown}]` : null;
}
if (ts.isNumericLiteral(name)) return String(Number(name.text));
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
return null;
@@ -1,3 +1,5 @@
import { widensReducedRecords } from "../function-returns.js";
import { widthNarrowHelper } from "../coercions/unions.js";
/* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */
import { indexedUnionArms, lowerIndexedUnionMember } from "../indexed-unions.js";
import { keyedHelperName } from "../stable-names.js";
@@ -7,6 +9,7 @@ import * as ts from "../../ts7/adapter.js";
import { lowerHistogramProperty } from "../builtins/performance.js";
import { staticFunctionNameRead } from "../static-function-names.js";
import { typedArgumentsArity } from "../typed-arguments.js";
import { INSPECT_CUSTOM_DESCRIPTION, isInspectCustomAccess } from "../symbol-fields.js";
import { isThisParameter } from "../call-signatures.js";
import { argumentCountSlot, contextualCallCount, usedAsValue } from "../argument-counts.js";
import {
@@ -62,6 +65,7 @@ import {
} from "../../../ir/ir.js";
import { lowerNavigatorMember } from "../builtins/navigator.js";
import { lowerX509PublicKey } from "../builtins/x509.js";
import { fenceMimeStaticRead } from "../builtins/mime.js";
import { NODE_COMPAT_VERSION } from "../builtins/process.js";
import {
inlineNamespaceRequireOf,
@@ -115,6 +119,7 @@ import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../.
import { typedArrayLacksMember } from "../builtins/typed-array-members.js";
import { TEMPORAL_GLOBAL } from "../../builtin-modules.js";
import { tryLowerExpression } from "./try-lower-expression.js";
import { lowerRecordInheritedMember } from "../record-inherited.js";
import { DATE_METHOD_HINT } from "../builtins/dates.js";
import {
cjsClassExprWholeExportOf,
@@ -335,6 +340,16 @@ export function lowerPropertyAccessExpr(
// Cloudflare probe) folds like `navigator.userAgent`.
const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION);
if (navigator) return navigator;
// util.inspect.custom: Symbol.for('nodejs.util.inspect.custom').
if (isInspectCustomAccess(lowerer, expr)) {
return {
kind: "libCall",
fn: "sym.for",
args: [{ kind: "strLit", value: INSPECT_CUSTOM_DESCRIPTION, type: STRING, loc }],
type: SYMBOL_T,
loc,
};
}
{
const member = lowerBufferPrototypeMember(lowerer, expr);
if (member) return member;
@@ -931,6 +946,7 @@ export function lowerPropertyAccessExpr(
// prototype that no lowering above claimed keeps the SC2020 fence
// rather than reading undefined through the field and dynamic reads.
fenceBuiltinConstructorStaticRead(lowerer, expr) ??
fenceMimeStaticRead(lowerer, expr) ??
lowerer.lowerFieldRead(expr);
// A union-typed field read narrows like an identifier when the
// checker has narrowed this use to one arm.
@@ -1364,7 +1380,23 @@ export function lowerPropertyAccessExpr(
// honest one, not a generic recitation about the outer dot. A
// receiver that lowers cleanly means the MEMBER is the gap — name
// the property and the receiver's type instead of "property access".
const recvLowered = lowerer.lowerExpr(expr.expression);
let recvLowered = lowerer.lowerExpr(expr.expression);
// A receiver whose value union holds record types the checker's subtype
// reduction folded into its type (`{ status } | { status, value }` typed
// `{ status }`, a call of a function with an inferred return type): read
// through the checker's type, converting the wider arms.
if (recvLowered.type.kind === "union") {
const declared = lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
if (
declared !== null &&
declared.kind === "record" &&
widensReducedRecords(lowerer, declared, recvLowered.type)
) {
const helper = widthNarrowHelper(lowerer, recvLowered.type.unionId, declared, loc);
if (helper !== null)
recvLowered = lowerer.checkedNarrowCall(helper, recvLowered, declared, loc);
}
}
// A dyn receiver — JSON.parse's `any`, or `unknown` the checker
// narrowed to `object` — reads through the dyn keyed read: member
// or undefined (JS's own-property answer), throwing JS's TypeError
@@ -3188,6 +3220,12 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi
loc: locOf(expr),
};
}
// Object.prototype's methods read as values: an own entry of the
// member's function type, else the built-in.
if (actual?.type.kind === "record") {
const inherited = lowerRecordInheritedMember(lowerer, actual, expr);
if (inherited) return inherited;
}
// A member the checker declares on the receiver, stored in a pure
// index-signature record (an object literal that spread one, see
// dictSpreadLiteralShape): the key is always present, so the keyed
@@ -39,6 +39,7 @@ import { lowerReadlineInstanceof } from "../builtins/readline.js";
import { lowerKeyObjectInstanceof } from "../builtins/key-object.js";
import { lowerCryptoKeyInstanceof } from "../builtins/webcrypto.js";
import { lowerNetAddressInstanceof } from "../builtins/net-address.js";
import { lowerMimeInstanceof } from "../builtins/mime.js";
import { typedArrayHasMember } from "../builtins/typed-array-members.js";
import { fenceEarlyNsMemberRef, nsMemberIdentOf } from "../lower-namespaces.js";
import { maybePromiseInstanceOf } from "../maybe-promise.js";
@@ -246,6 +247,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
if (cryptoKey) return cryptoKey;
const netAddress = lowerNetAddressInstanceof(lowerer, expr);
if (netAddress) return netAddress;
const mime = lowerMimeInstanceof(lowerer, expr);
if (mime) return mime;
const readlineInterface = lowerReadlineInstanceof(lowerer, expr);
if (readlineInterface) return readlineInterface;
}
@@ -11,12 +11,14 @@ import {
VOID,
isUnitType,
typeEquals,
typeKey,
} from "../../ir/ir.js";
import { isJsSourceFile } from "../program.js";
import { PoisonError, dynFallbackType } from "./lowerer.js";
import { functionCanReturnUndefined } from "../function-completion.js";
import { jsBindingAppendsOnly, jsBindingHasOpenWrites } from "./lower-stmts.js";
import type { FnCtx } from "./function-context.js";
import { constituentTypes } from "../ts7/checker.js";
import { returnsGuessedOverloadValue } from "./overload-choice.js";
/** Keep the conservative completion rule shared by return construction
@@ -193,6 +195,8 @@ export function declaredReturnType(
nextT: DYN,
};
let mappedReturn = lowerer.mapTypeOf(retTsType);
if (mappedReturn !== null)
mappedReturn = widenReducedReturn(lowerer, decl, retTsType, mappedReturn);
// A generator declared to return Iterable<T> returns a generator over T.
if (mappedReturn && decl.asteriskToken)
mappedReturn = iterableGeneratorArm(lowerer, mappedReturn);
@@ -556,3 +560,194 @@ export function blockBodyOf(decl: ts.FunctionLikeDeclaration): ts.Block | null {
if (ts.isBlock(body)) return body;
return ts.forEachChild(decl, (c) => (ts.isBlock(c) ? c : undefined)) ?? null;
}
const widening = new Set<ts.Node>();
/** How many times widenReducedReturn answered a declaration it was still
* widening with its unwidened type. A result computed while that happened
* may lack arms, so it is not kept. */
let wideningCuts = 0;
/** An inferred return type is the checker's SUBTYPE-REDUCED union of the
* returned values' types: `return aborted` (`{ status }`) beside `return
* { status, value }` infers just `{ status }`, because the second is a
* subtype of the first. Records have fixed layouts, so that type cannot
* hold the second value: the return would drop `value` or fail the union
* wrap. A function whose return type is inferred therefore returns the
* union of the record types it actually returns. Declared return types keep
* their meaning. */
function widenReducedReturn(
lowerer: Lowerer,
decl: ts.SignatureDeclaration,
retTsType: ts.Type,
mapped: IrType,
): IrType {
if (decl.type !== undefined || hasExplicitJsDocReturn(decl)) return mapped;
const body = (decl as { body?: ts.Node }).body;
if (body === undefined || decl.asteriskToken !== undefined) return mapped;
if (decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return mapped;
// A function expression with a contextual signature returns that
// signature's type; only free-standing inference is widened.
if (
(ts.isFunctionExpression(decl) || ts.isArrowFunction(decl)) &&
lowerer.checker.getContextualType(decl) !== undefined
)
return mapped;
const arms =
mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped];
const recordArms = arms.filter((a) => a.kind === "record");
if (recordArms.length === 0) return mapped;
if (widening.has(decl)) {
wideningCuts++;
return mapped;
}
// Functions that forward to the same callees along several return
// branches read each callee's widened type once. A nested function
// lowered under other type-parameter bindings maps its returns again.
const known = lowerer.coercions.widenedReturnTypes.get(decl)?.get(retTsType);
const erasing = lowerer.erasingDepth > 0;
if (
known !== undefined &&
known.bindings === lowerer.typeParamBindings &&
known.tsBindings === lowerer.typeParamTsBindings &&
known.implicit === lowerer.implicitParamTypes &&
known.erasing === erasing
)
return known.type;
const cutsBefore = wideningCuts;
const widened = widenReturnArms(lowerer, decl, retTsType, mapped, arms, recordArms);
if (wideningCuts === cutsBefore) {
let byType = lowerer.coercions.widenedReturnTypes.get(decl);
if (byType === undefined) lowerer.coercions.widenedReturnTypes.set(decl, (byType = new Map()));
byType.set(retTsType, {
type: widened,
bindings: lowerer.typeParamBindings,
tsBindings: lowerer.typeParamTsBindings,
implicit: lowerer.implicitParamTypes,
erasing,
});
}
return widened;
}
/** widenReducedReturn's scan of the returned values, for a declaration not
* already being widened. */
function widenReturnArms(
lowerer: Lowerer,
decl: ts.SignatureDeclaration,
retTsType: ts.Type,
mapped: IrType,
arms: readonly IrType[],
recordArms: readonly IrType[],
): IrType {
const body = (decl as { body?: ts.Node }).body!;
const checkerArms = new Set<ts.Type>(
retTsType.flags & ts.TypeFlags.Union ? constituentTypes(retTsType) : [retTsType],
);
const fieldsOf = (t: IrType): readonly string[] | null => {
if (t.kind !== "record") return null;
const shape = lowerer.shapes.get(t.shapeId);
return shape === undefined || shape.tuple || shape.indexValue !== undefined
? null
: shape.fields.map((f) => f.name);
};
const narrower = (part: IrType): boolean => {
const own = fieldsOf(part);
if (own === null || arms.some((a) => typeKey(a) === typeKey(part))) return false;
return recordArms.some((arm) => {
const fields = fieldsOf(arm);
return fields !== null && fields.length < own.length && fields.every((f) => own.includes(f));
});
};
const extra = new Map<string, IrType>();
const returned = (expr: ts.Expression): void => {
// A call of another inferred function returns ITS widened type.
const callee = ts.isCallExpression(expr)
? lowerer.checker.getResolvedSignature(expr)?.declaration
: undefined;
if (
callee !== undefined &&
ts.isFunctionLike(callee) &&
(callee as { body?: ts.Node }).body !== undefined &&
callee.type === undefined
) {
// A call of the function itself returns its own type, whose arms
// add nothing. Reading it would cut the widening and keep this
// result from being reused.
if (callee === decl) return;
let t: IrType | null = null;
widening.add(decl);
try {
t = declaredReturnType(lowerer, callee, expr);
} catch {
t = null;
} finally {
widening.delete(decl);
}
if (t === null) return;
const parts = t.kind === "union" ? (lowerer.unions.get(t.unionId)?.arms ?? []) : [t];
for (const part of parts) if (narrower(part)) extra.set(typeKey(part), part);
return;
}
// Only a value type the checker's reduction dropped from the inferred
// union is a candidate; a constituent of the inferred type is already
// represented by it.
const valueType = lowerer.typeOf(expr);
const valueArms =
valueType.flags & ts.TypeFlags.Union ? constituentTypes(valueType) : [valueType];
for (const part of valueArms) {
if (checkerArms.has(part) || !(part.flags & ts.TypeFlags.Object)) continue;
if (!lowerer.checker.isTypeAssignableTo(part, retTsType)) continue;
const t = lowerer.mapTypeOf(part);
if (t !== null && narrower(t)) extra.set(typeKey(t), t);
}
};
const scan = (node: ts.Node): void => {
if (node !== decl && ts.isFunctionLike(node)) return;
if (ts.isReturnStatement(node) && node.expression !== undefined) returned(node.expression);
ts.forEachChild(node, scan);
};
if (ts.isArrowFunction(decl) && !ts.isBlock(body)) returned(body as ts.Expression);
else scan(body);
if (extra.size === 0) return mapped;
const joined = new Map<string, IrType>();
for (const a of [...arms, ...extra.values()]) joined.set(typeKey(a), a);
const sorted = [...joined.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
const unionId = lowerer.unions.intern(sorted);
lowerer.coercions.widenedReturns.add(unionId);
return { kind: "union", unionId };
}
/** Whether `wide` is a widened inferred return that holds `narrow`'s arms plus records that are width
* subtypes of `narrow`'s record arms: the shape a subtype-reduced inferred
* type takes next to the values it was inferred from. */
export function widensReducedRecords(lowerer: Lowerer, narrow: IrType, wide: IrType): boolean {
if (wide.kind !== "union" || typeKey(narrow) === typeKey(wide)) return false;
if (!lowerer.coercions.widenedReturns.has(wide.unionId)) return false;
const narrowArms =
narrow.kind === "union" ? (lowerer.unions.get(narrow.unionId)?.arms ?? []) : [narrow];
const wideArms = lowerer.unions.get(wide.unionId)?.arms ?? [];
const keys = new Set(wideArms.map(typeKey));
if (!narrowArms.every((a) => keys.has(typeKey(a)))) return false;
const fieldsOf = (t: IrType): readonly string[] | null => {
if (t.kind !== "record") return null;
const shape = lowerer.shapes.get(t.shapeId);
return shape === undefined || shape.tuple || shape.indexValue !== undefined
? null
: shape.fields.map((f) => f.name);
};
const narrowKeys = new Set(narrowArms.map(typeKey));
const extras = wideArms.filter((a) => !narrowKeys.has(typeKey(a)));
return (
extras.length > 0 &&
extras.every((extra) => {
const own = fieldsOf(extra);
return (
own !== null &&
narrowArms.some((arm) => {
const f = fieldsOf(arm);
return f !== null && f.length < own.length && f.every((x) => own.includes(x));
})
);
})
);
}
@@ -1040,6 +1040,14 @@ function unifySignatureBindings(
dAliasArgs.length &&
iAliasArgs.length === dAliasArgs.length
) {
if (tsBindings && declared !== inst) {
let entries = lowerer.boundAliasInstances.get(declared);
if (!entries) lowerer.boundAliasInstances.set(declared, (entries = []));
// A body forwarding its own alias (`inner(g)` returning `Catalog<G,
// "x">` inside an instance) resolves through the enclosing one.
if (!entries.some((entry) => entry.tsBindings === tsBindings))
entries.push({ tsBindings, instance: lowerer.boundAliasTsResolver(inst) ?? inst });
}
dAliasArgs.forEach((da, i) => {
const ia = iAliasArgs[i];
if (ia) unify(da, ia, depth + 1);
@@ -282,9 +282,32 @@ import {
} from "./generic-functions.js";
import { codeGenerationCalleeOf, lowerCodeGenerationCall } from "./code-generation.js";
import { lowerRecordInheritedCall } from "./record-inherited.js";
const ZLIB_STREAM_METHODS: ReadonlySet<string> = new Set(["flush", "params", "close", "reset"]);
/** The node:util member a `(0, member)` callee names, or null. */
function indirectUtilCalleeOf(
lowerer: Lowerer,
callee: ts.Expression,
): { module: string; member: string } | null {
let e = callee;
while (ts.isParenthesizedExpression(e)) e = e.expression;
if (
!ts.isBinaryExpression(e) ||
e.operatorToken.kind !== ts.SyntaxKind.CommaToken ||
!ts.isNumericLiteral(e.left)
)
return null;
const target = e.right;
const bi = ts.isIdentifier(target)
? lowerer.builtinImportOf(target)
: ts.isPropertyAccessExpression(target) && !target.questionDotToken
? lowerer.builtinMemberOf(target)
: null;
return bi?.module === "util" ? bi : null;
}
/** `Object.defineProperty(target, ...)`, `Object.defineProperties`,
* `Reflect.defineProperty`, and `target.__defineGetter__/__defineSetter__`
* over a target that may hold an Error can install an accessor over an
@@ -384,6 +407,14 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const nodeApi = nodeApiCalleeOf(lowerer.program, expr.expression);
if (nodeApi !== null) return lowerNodeApiCall(lowerer, expr, nodeApi);
}
// `(0, util_1.inspect)(value)`: TypeScript's CommonJS output calls a
// named import through a comma expression, which leaves the call's
// receiver undefined. node:util's functions do not read their receiver,
// so the call lowers as the direct one.
{
const util = indirectUtilCalleeOf(lowerer, expr.expression);
if (util) return lowerBuiltinCall(lowerer, expr, util, loc);
}
// The deprecated Buffer(...) call (containers/bytes.ts). The checker
// types it `any`, so the bytes cross as a checked-dynamic value, which
// keeps the Buffer brand for inspection.
@@ -1651,6 +1682,9 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// Union receivers whose every arm has a text — the ngrok
// `(chunk: Buffer | string) => chunk.toString()` idiom.
lowerUnionToStringCall(lowerer, expr, expr.expression) ??
// Object.prototype's methods called by name on records: an own
// callable entry, else the built-in with the record as receiver.
lowerRecordInheritedCall(lowerer, expr, expr.expression) ??
// Object.prototype.toString's default answer on records and
// override-free program classes — "[object Object]", folded.
lowerDefaultToStringCall(lowerer, expr, expr.expression) ??
@@ -163,6 +163,7 @@ import {
} from "./lower-event-emitter.js";
import { declSymbolOf } from "./lower-modules.js";
import {
INSPECT_CUSTOM_MEMBER,
classSymbolKeyOf,
classSymbolKeyOfSymbol,
symbolFieldDisplayName,
@@ -4454,6 +4455,9 @@ export function collectClassShapeInner(
...(methods.size > 0 ? { methods: [...methods.keys()] } : {}),
...((): { prototypeNames?: string[] } => {
const names = classPrototypeNames(decl);
// A [util.inspect.custom] method, by its reserved slot name: the
// checked-dynamic inspect walk refuses instances that have one.
if (methods.has(INSPECT_CUSTOM_MEMBER)) names.push(INSPECT_CUSTOM_MEMBER);
return names.length > 0 ? { prototypeNames: names } : {};
})(),
...(abstractClass ? { abstract: true as const } : {}),
@@ -14,7 +14,15 @@ import { boolLit, dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/buil
import { locOf } from "../program.js";
import { type ConsoleMethodMember } from "./builtins/console.js";
import { jsFuncNameOf, type Lowerer } from "./lowerer.js";
import { lowerConsoleInspectArg, lowerFormatCall, lowerInspectValue } from "./lower-inspect.js";
import {
heldFormatArgs,
lowerConsoleInspectArg,
lowerFormatCall,
lowerInspectValue,
lowerRuntimeFormat,
runtimeFormatString,
formatStringConverts,
} from "./lower-inspect.js";
import { streamSidesOf } from "./lower-stream.js";
/** node:console Console instances: a native handle (the checked-dynamic
@@ -358,9 +366,32 @@ export function lowerConsoleOutput(
) {
return consoleWrite(stdout, [lowerFormatCall(lowerer, expr, loc, false)], loc);
}
const args = expr.arguments.nodes.map((a) =>
lowerConsoleArg(lowerer, a, lowerer.lowerExpr(a), surface, loc),
);
const nodes = expr.arguments.nodes;
// Spread arguments and format strings that may hold a substitution at
// run time go through the runtime formatter. A string first argument
// without a '%' keeps the space-joined write below.
if (nodes.some(ts.isSpreadElement))
return consoleWrite(stdout, [lowerRuntimeFormat(lowerer, nodes, loc)], loc);
const runtime =
nodes.length > 1 && !isStringLiteralLike(first) ? runtimeFormatString(lowerer, first!) : null;
if (runtime === "other" && !formatStringConverts(lowerer, first!))
return consoleWrite(stdout, [lowerRuntimeFormat(lowerer, nodes, loc)], loc);
if (runtime !== null) {
const held = heldFormatArgs(lowerer, nodes, loc);
if (!held.joinable) return seq(held.stmts, consoleWrite(stdout, [held.format()], loc), loc);
const joined = held.args.map((arg) =>
lowerConsoleArg(lowerer, arg.node, arg.ref(), surface, loc),
);
const write: IrStmt = {
kind: "if",
cond: held.specifies,
then: [{ kind: "exprStmt", expr: consoleWrite(stdout, [held.format()], loc), loc }],
else_: [{ kind: "exprStmt", expr: consoleWrite(stdout, joined, loc), loc }],
loc,
};
return seq([...held.stmts, write], noop(loc), loc);
}
const args = nodes.map((a) => lowerConsoleArg(lowerer, a, lowerer.lowerExpr(a), surface, loc));
return consoleWrite(stdout, args, loc);
}
@@ -167,6 +167,23 @@ function declareFunctionArguments(
}
}
/** The type parameters a lambda without its own takes from a generic
* contextual signature, unbound in the current scope. */
function contextualTypeParameterSymbols(
lowerer: Lowerer,
node: ts.ArrowFunction | ts.FunctionExpression,
): ts.Symbol[] {
const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(node));
if (signatures.length !== 1) return [];
const symbols: ts.Symbol[] = [];
for (const parameter of signatures[0]!.getTypeParameters()) {
const symbol = parameter.getSymbol();
if (!symbol || lowerer.typeParamBindings?.has(symbol)) return [];
symbols.push(symbol);
}
return symbols;
}
/** Lifts an arrow function / function expression / nested declaration /
* object-literal shorthand method to a module-level function and yields
* the `closure` expression creating it. */
@@ -202,6 +219,31 @@ export function lowerLambda(
lowerer.typeParamBindings = previous;
}
}
// A lambda contextually typed by a GENERIC signature (`send: (m) => …`
// for `send<T>(m: Msg<T>): string`) takes that signature's type
// parameters as its own; it lowers once, erased, like a generic lambda.
if (
(ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
!node.typeParameters?.nodes.length &&
!lowerer.erasingLambdas.has(node)
) {
const contextual = contextualTypeParameterSymbols(lowerer, node);
if (contextual.length > 0) {
const previous = lowerer.typeParamBindings;
const bindings = new Map(previous ?? []);
for (const symbol of contextual) bindings.set(symbol, DYN);
lowerer.typeParamBindings = bindings;
lowerer.erasingLambdas.add(node);
lowerer.erasingDepth++;
try {
return lowerLambda(lowerer, node, checkedReceiver);
} finally {
lowerer.erasingDepth--;
lowerer.erasingLambdas.delete(node);
lowerer.typeParamBindings = previous;
}
}
}
const prepared =
ts.isArrowFunction(node) || ts.isFunctionExpression(node)
? lowerer.preparedClassFactories.get(node)
File diff suppressed because it is too large Load Diff
@@ -98,6 +98,7 @@ import {
createRequireProgramModuleDecl,
} from "./builtins/module-bindings.js";
import { isPromisifyCall, registerBuiltinCallableAlias } from "./builtins/callable-aliases.js";
import { customInspectRegistrations } from "./lower-inspect.js";
import { registerBuiltinClassAlias } from "./builtins/class-values.js";
import { stripTypeCasts } from "./builtins/arguments.js";
import { isWebCryptoMemberType } from "./builtins/webcrypto.js";
@@ -1979,18 +1980,21 @@ export function collectGlobals(
// symbol registers HERE, before any use site can lower, and the
// statement lowering skips the declaration by the same check. A
// bad promisify target reports here (recorded once; the statement
// lowering poisons too, like every collectGlobals failure).
// lowering poisons too, like every collectGlobals failure). Targets
// that promisify to a function value (typed callback-last functions
// and untyped functions) keep an ordinary global.
if (
ts.isIdentifier(decl.name) &&
decl.initializer &&
isPromisifyCall(lowerer, decl.initializer)
) {
let registered = true;
try {
lowerer.registerPromisifiedBuiltinDecl(decl.name, decl.initializer);
registered = lowerer.registerPromisifiedBuiltinDecl(decl.name, decl.initializer);
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
}
continue;
if (registered) continue;
}
// `const inspect = require("util").inspect` at file scope: a
// named import in const clothing (builtinImportOf resolves uses)
@@ -3555,6 +3559,9 @@ export function buildMain(lowerer: Lowerer): IrFunction {
loc,
});
}
// Classes with custom inspect methods register their compiled calls for
// the runtime's inspect walk before any module runs, in every thread.
body.unshift(...customInspectRegistrations(lowerer, loc));
// Node's startup refusal (a resolution the graph carries that Node
// rejects — preflight's Node-order resolution walk — or a module-LINK
// SyntaxError from either named-import checker): the graph is refused
@@ -5422,6 +5422,12 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration)
const scope = decl.parent?.parent?.parent;
if (symbol === undefined || (!ts.isBlock(scope) && !ts.isSourceFile(scope))) return null;
// A shorthand property's name (`{ width }`) resolves to the property
// symbol, but it reads the binding: the checker's shorthand-value symbol.
const refersToBinding = (node: ts.Identifier): boolean =>
(ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node
? lowerer.checker.getShorthandAssignmentValueSymbol(node.parent)
: lowerer.checker.getSymbolAtLocation(node)) === symbol;
const isPlainAssignmentTarget = (node: ts.Identifier): boolean => {
let target: ts.Expression = node;
while (ts.isParenthesizedExpression(target.parent) && target.parent.expression === target) {
@@ -5437,11 +5443,7 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration)
const captures = (fn: ts.Node): boolean => {
let captured = false;
ts.walkPreorder(fn, (node) => {
if (
node !== fn &&
ts.isIdentifier(node) &&
lowerer.checker.getSymbolAtLocation(node) === symbol
) {
if (node !== fn && ts.isIdentifier(node) && refersToBinding(node)) {
captured = true;
return "stop";
}
@@ -5493,7 +5495,7 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration)
if (
ts.isIdentifier(node) &&
node !== decl.name &&
lowerer.checker.getSymbolAtLocation(node) === symbol &&
refersToBinding(node) &&
!isPlainAssignmentTarget(node)
) {
reads.push(node);
@@ -69,6 +69,7 @@ import { isDiffieHellmanType, lowerDiffieHellmanCall } from "./builtins/dh.js";
import { fenceWebCryptoMember, lowerWebCryptoCall } from "./builtins/webcrypto.js";
import { valueStreamOf } from "./builtins/hash-stream.js";
import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js";
import { lowerMimeCall, lowerMimeNew } from "./builtins/mime.js";
import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js";
import { iteratorSlotAdapter } from "./iterator-adapters.js";
import { iterableSlotValue } from "./iterable-values.js";
@@ -116,6 +117,7 @@ import {
checkedNarrowCall,
checkedNarrowOperand,
deferredReadHelper,
widthNarrowHelper,
} from "./coercions/unions.js";
import {
lowerUtilTypeValue,
@@ -176,7 +178,7 @@ import {
import { DeferredModuleInitializers } from "./deferred-module-initializers.js";
import { finalizeClassMethodValues } from "./class-method-values.js";
import { finalizePrivateBrands } from "./private-brands.js";
import { type ClassSymbolKey, tableKeyFieldType } from "./symbol-fields.js";
import { type ClassSymbolKey, INSPECT_CUSTOM_MEMBER, tableKeyFieldType } from "./symbol-fields.js";
import type { HttpClientFnBinding } from "./lower-server.js";
export type { WidthLift } from "./width-lift.js";
export { newFnCtx, type FnCtx } from "./function-context.js";
@@ -461,7 +463,7 @@ import { bindingNeverReassigned } from "./binding-analysis.js";
import type { InstantiationPath } from "./instantiation-path.js";
import { bodyReadsArguments } from "../arguments-usage.js";
import { completeArgs, wrappedUndefined, undefinedArgFor } from "./call-arguments.js";
import { bodyReturnType, declaredReturnType } from "./function-returns.js";
import { bodyReturnType, declaredReturnType, widensReducedRecords } from "./function-returns.js";
import { lowerCall, lowerPromiseMethodCall, lowerFilterNarrowCall } from "./lower-calls.js";
import { lowerFfiCall, validateFfiImports } from "./native-ffi.js";
import {
@@ -2234,6 +2236,19 @@ export class Lowerer {
* the body needs — indexed accesses (`T[K]` needs K's literal key) and
* keyed record reads (`o[k]` where k's type is a literal-bound K). */
typeParamTsBindings: Map<ts.Symbol, ts.Type> | null = null;
/** Generic alias references of call-keyed instances' declared signatures
* (`Catalog<G, P>`), each with the same alias as every call's resolved
* signature instantiates it (`Catalog<{ g1: … }, "acme">`), together with
* that instance's ts-level bindings map (typeParamTsBindings), which every
* scope that lowers the instance installs. The checker keeps the alias
* symbolic inside the generic body and offers no way to instantiate it
* with the bound arguments, but alias instantiations are interned: the
* body's `x as Catalog<G, P>` is the very type the signature declares, so
* the pair recorded during unification is that re-instantiation. */
readonly boundAliasInstances = new Map<
ts.Type,
{ tsBindings: Map<ts.Symbol, ts.Type>; instance: ts.Type }[]
>();
/** Non-null while an IMPLICIT-ANY instance body lowers (npm-static JS;
* see generic-functions.ts): bound param symbol →
* the call site's checker type, consulted by typeOf for identifier
@@ -3554,6 +3569,7 @@ export class Lowerer {
this.internTableHelpers,
this.coercions.retags,
this.coercions.narrows,
this.coercions.widthNarrows,
])
for (const name of table.values()) interned.add(name);
for (const fn of [...this.liftedFns, ...this.implicitFns])
@@ -3811,6 +3827,7 @@ export class Lowerer {
classNamer: (decl) => this.classNamer(decl),
resolveTypeParam: this.typeParamResolver,
resolveTypeParamTs: this.typeParamTsResolver,
resolveBoundAliasTs: this.boundAliasTsResolver,
isErasing: () => this.erasingDepth > 0,
genericClassInstance: (decl, ref, mapArgument) =>
this.genericClassInstanceType(decl, ref, mapArgument),
@@ -5235,11 +5252,11 @@ export class Lowerer {
const sourceFiles = parts.map((p) => p.sf);
const fnDecls = parts.flatMap((p) => p.fnDecls);
// This fixed-point scan intentionally sees deferred bodies before
// reachability is known. Batch their binding queries through the
// facade's panic fence, then keep lookup free of lowering side effects:
// resolveValueSymbol also flushes deferred diagnostics and applies
// merged-namespace fences, whose authority remains ordinary lowering.
this.checker.prefetchSymbolRoots(sourceFiles, true);
// reachability is known. Its binding queries were batched through the
// facade's panic fence before collection (prefetchProgramSymbols); keep
// lookup free of lowering side effects: resolveValueSymbol also flushes
// deferred diagnostics and applies merged-namespace fences, whose
// authority remains ordinary lowering.
// The facts each fixed-point site reads, by the symbols, nodes and
// element keys they belong to (created with the site list below).
let siteDependencies: SiteDependencies | null = null;
@@ -8041,6 +8058,7 @@ export class Lowerer {
if (this.remainder) {
for (const fp of parts) this.checker.prefetchSourceFile(fp.sf);
}
this.prefetchProgramSymbols(parts);
this.collectProgram(parts);
this.speculativeTypeQueries(() => {
extendAssertedRecordMembers(
@@ -8501,6 +8519,7 @@ export class Lowerer {
this.onEdge = (name) => {
collectionEdges.add(name);
};
this.prefetchProgramSymbols(parts);
this.collectProgram(parts);
this.speculativeTypeQueries(() => {
extendAssertedRecordMembers(
@@ -8687,6 +8706,19 @@ export class Lowerer {
this.constructedClasses.add(name.slice(1, -".constructor".length));
for (const [owner, methods] of this.virtualEdges)
for (const method of methods) this.noteVirtualEdge(owner, method);
// An instance's [util.inspect.custom] method runs from the runtime
// inspect walk (customInspectRegistrations), which no IR edge shows.
for (
let c = this.classes.get(name.slice(1, -".constructor".length)) ?? null;
c;
c = c.base
) {
const custom = `%${c.def.name}.${INSPECT_CUSTOM_MEMBER}`;
if (units.has(custom)) {
this.onEdge?.(custom);
break;
}
}
}
};
// Class EXPRESSIONS collect while init bodies lower (below): their
@@ -9840,6 +9872,19 @@ export class Lowerer {
return (sym && this.typeParamTsBindings.get(sym)) ?? null;
};
/** The concrete instantiation of a generic alias reference (`Catalog<G,
* P>`) the current call-keyed instance's signature declares
* (boundAliasInstances), or null when its signature never instantiated
* that reference. Inert outside generic function instantiation. */
readonly boundAliasTsResolver = (t: ts.Type): ts.Type | null => {
if (!this.typeParamTsBindings || t.getAliasTypeArguments().length === 0) return null;
const tsBindings = this.typeParamTsBindings;
return (
this.boundAliasInstances.get(t)?.find((entry) => entry.tsBindings === tsBindings)?.instance ??
null
);
};
irTypeOf(node: ts.Node): IrType {
const t = this.typeOf(node);
// JS inference can leave mutable fields, accessor parameters and
@@ -10563,7 +10608,11 @@ export class Lowerer {
expected.kind !== "void" &&
this.armTag(expr.type.unionId, expected) >= 0
) {
const helper = this.narrowedArmHelper(expr.type.unionId, expected, expr.loc);
// A function's widened inferred return (widenReducedReturn) also
// holds records with more fields; those convert instead of throwing.
const helper = widensReducedRecords(this, expected, expr.type)
? widthNarrowHelper(this, expr.type.unionId, expected, expr.loc)
: this.narrowedArmHelper(expr.type.unionId, expected, expr.loc);
if (helper) {
return this.checkedNarrowCall(helper, expr, expected, expr.loc);
}
@@ -10687,6 +10736,31 @@ export class Lowerer {
}
}
}
// A promise or function value against a union whose ONLY arm of
// that kind differs from it (`Promise<T>` into `Promise<unknown> |
// undefined`, `() => Promise<string | null>` into an optional
// `() => Promise<string | null | undefined>` member): convert into
// that arm with the arm-level conversion (the checked payload
// widening, the function adapter), then wrap. Several arms of the
// kind stay ambiguous and fall through to the union fence.
if (expr.type.kind === "promise" || expr.type.kind === "func") {
const arms = this.unions.get(expected.unionId)?.arms ?? [];
const candidates = arms.filter((arm) => arm.kind === expr.type.kind);
const arm = candidates.length === 1 ? candidates[0]! : null;
if (arm) {
const converted = this.coerceToExpected(expr, arm);
if (converted !== expr && typeEquals(converted.type, arm)) {
return {
kind: "unionWrap",
unionId: expected.unionId,
tag: arms.indexOf(arm),
value: converted,
type: expected,
loc: expr.loc,
};
}
}
}
// A width-coercible value against a union: coerce into the SINGLE
// width-liftable arm, then wrap like any arm value (widthLiftPlan's
// liftWrap — several candidate arms are ambiguous and decline).
@@ -12210,6 +12284,17 @@ export class Lowerer {
return lowerClassExpressionInfo(this, expr);
}
/** Batch-prefetches the symbols of every runtime identifier of the
* program's files. Collection's early-binding scan and the runtime-
* optional analysis look them up across every body, reached or not;
* one batch answers both, instead of one request per identifier. */
private prefetchProgramSymbols(parts: FileParts[]): void {
this.checker.prefetchSymbolRoots(
parts.map((fp) => fp.sf),
true,
);
}
/** Depth of the whole-program analyses that query types across every
* file before reachability is known (speculativeTypeQueries). */
private speculativeTypeQueryDepth = 0;
@@ -13807,6 +13892,7 @@ export class Lowerer {
lowerEcdhCall(this, expr) ??
lowerDiffieHellmanCall(this, expr) ??
lowerNetAddressCall(this, expr) ??
lowerMimeCall(this, expr) ??
lowerFetchElementMethodCall(this, expr) ??
lowerDynamicImportCall(this, expr) ??
lowerCall(this, expr)
@@ -14044,6 +14130,7 @@ export class Lowerer {
lowerResponseNew(this, expr) ??
lowerBlobNew(this, expr) ??
lowerNetAddressNew(this, expr) ??
lowerMimeNew(this, expr) ??
lowerStaticReadableStreamNew(this, expr) ??
lowerStaticTransformStreamNew(this, expr) ??
lowerConsoleNew(this, expr);
@@ -14488,6 +14575,10 @@ export class Lowerer {
const assertion = assertFunctionValue(bi, loc);
if (assertion) return assertion;
if (bi.module === "util" && bi.member === "types") return utilTypesModuleValue(this, loc);
// util.inspect: the function custom inspect methods receive, one
// identity per thread.
if (bi.module === "util" && bi.member === "inspect")
return { kind: "libCall", fn: "insp.inspectFunction", args: [], type: DYN, loc };
if (bi.module === "util/types" && builtinModuleFnOf(this, bi.module, bi.member))
return lowerUtilTypeValue(this, bi.member, loc);
if (bi.module === "util" && bi.member === "getSystemErrorMap")
@@ -0,0 +1,911 @@
/* Object.prototype's methods on typed records.
*
* A record is an ordinary object, so `r.toString()`, `r.hasOwnProperty(k)`
* and the other Object.prototype methods answer from its prototype unless
* the record holds an own entry under that name. Method calls with a
* constant name call an own entry when the record's type lets one be
* callable and otherwise run the built-in natively with the record as its
* receiver. Keyed reads of records whose values are functions or checked
* values answer the built-in as a native function value on the miss path,
* so truthiness, typeof and calls behave as in Node; hits are untouched.
* Records of other value types keep reading undefined for these names
* (a documented limitation). No dynamic-tier unit is involved: every
* built-in is a small lifted function over the record. */
import * as ts from "../ts7/adapter.js";
import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js";
import {
BOOL,
DYN,
STRING,
UNDEFINED_T,
isUnitType,
typeEquals,
typeKey,
type IrExpr,
type IrLocal,
type IrStmt,
type IrType,
type SrcLoc,
} from "../../ir/ir.js";
import type { Lowerer } from "./lowerer.js";
import { locOf } from "../program.js";
import { keyedHelperName } from "./stable-names.js";
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
import { nullProtoRecordShape, recordNullProtoTest } from "./null-prototype.js";
import { recordHasOwnHelper } from "./lower-calls.js";
import { OTHER_ARM_FAILURE_MESSAGE } from "./coercions/unions.js";
/** Object.prototype's methods a record answers from its prototype. */
export const RECORD_INHERITED_METHODS: ReadonlySet<string> = new Set([
"toString",
"toLocaleString",
"valueOf",
"hasOwnProperty",
"isPrototypeOf",
"propertyIsEnumerable",
]);
type FuncType = IrType & { kind: "func" };
/** Declares a hidden local in the function being built: the lowerer's
* current function, or a lifted helper's own local list. */
type DeclareLocal = (name: string, type: IrType) => string;
function liftedLocals(): { declare: DeclareLocal; locals: IrLocal[] } {
const locals: IrLocal[] = [];
return {
locals,
declare: (name, type) => {
const id = `${name}.${locals.length}`;
locals.push({ id, name, type, mutable: false });
return id;
},
};
}
/** The form a read of `type` gives a built-in method value: the union's one
* function arm (beside an undefined arm), a checked value, or null when the
* type cannot hold a function. */
function methodValueForm(lowerer: Lowerer, type: IrType): FuncType | "dyn" | null {
if (type.kind === "dyn") return "dyn";
if (type.kind !== "union") return null;
const arms = lowerer.unions.get(type.unionId)?.arms ?? [];
if (!arms.some((arm) => arm.kind === "undefinedT")) return null;
const funcs = arms.filter((arm): arm is FuncType => arm.kind === "func");
if (funcs.length !== 1 || funcs[0]!.restAbi !== undefined) return null;
return funcs[0]!;
}
/** A value of `type` may be a function that a call can enter. */
function callableType(lowerer: Lowerer, type: IrType): boolean {
if (type.kind === "func" || type.kind === "dyn") return true;
if (type.kind !== "union") return false;
return (lowerer.unions.get(type.unionId)?.arms ?? []).some(
(arm) => arm.kind === "func" || arm.kind === "dyn",
);
}
/** A keyed record read (`r[k]`, `r["toString"]`) whose miss answers the
* inherited Object.prototype method for those names. Hits keep the
* ordinary read; only the miss path tests the key. `bound`: the read is
* the callee of a call (`r[k](…)`, `r[k]?.(…)`), whose `this` is the
* record; any other read yields the method as a plain value, whose calls
* see an undefined `this` as in Node. */
export function withInheritedMethods(
lowerer: Lowerer,
read: IrExpr & { kind: "recordKeyGet" },
litKey: string | null,
bound: boolean,
): IrExpr {
if (litKey !== null && !RECORD_INHERITED_METHODS.has(litKey)) return read;
if (read.missNaN === true) return read;
const form = methodValueForm(lowerer, read.type);
const shape = lowerer.shapes.get(read.shapeId);
if (form === null || !shape || shape.tuple) return read;
const loc = read.loc;
const stmts: IrStmt[] = [];
const repeatable = (value: IrExpr): boolean => isSafeToRepeat(value) || value.kind === "strLit";
const stable = (value: IrExpr, name: string, keep: boolean): IrExpr => {
if (keep) return value;
const local = lowerer.declareHiddenLocal(name, value.type);
stmts.push({ kind: "varDecl", localId: local.id, init: value, loc });
return varRef(local.id, value.type, loc);
};
// The receiver is evaluated before the key: when computing the key runs
// code (it may reassign the receiver's variable), the receiver is held
// first, even when reading it again would be safe on its own.
const keyRepeatable = repeatable(read.key);
const obj = stable(read.obj, "%inheritedRecv", repeatable(read.obj) && keyRepeatable);
const key = stable(read.key, "%inheritedKey", keyRepeatable);
const helper = inheritedReadHelper(lowerer, read.shapeId, read.type, form, bound, loc);
const fallback: IrExpr = { kind: "call", callee: helper, args: [obj, key], type: read.type, loc };
if (
form !== "dyn" &&
read.type.kind === "union" &&
!(lowerer.unions.get(read.type.unionId)?.arms ?? []).some((arm) => arm.kind === "nullT")
) {
// `r[k] ?? inherited(r, k)`: the hit is the read's own value, untouched.
const coalesced: IrExpr = {
kind: "nullish",
left: { ...read, obj, key },
right: fallback,
type: read.type,
loc,
};
return stmts.length === 0
? coalesced
: { kind: "seqExpr", stmts, result: coalesced, type: read.type, loc };
}
const value = lowerer.declareHiddenLocal("%inheritedRead", read.type);
stmts.push({ kind: "varDecl", localId: value.id, init: { ...read, obj, key }, loc });
const found = varRef(value.id, read.type, loc);
let missing: IrExpr;
if (form === "dyn") {
missing = { kind: "dynTest", test: "undefined", value: found, type: BOOL, loc } as IrExpr;
} else {
const unionId = (read.type as IrType & { kind: "union" }).unionId;
const tag = (lowerer.unions.get(unionId)?.arms ?? []).findIndex(
(arm) => arm.kind === "undefinedT",
);
missing = { kind: "unionIsTag", unionId, tag, negated: false, value: found, type: BOOL, loc };
}
return {
kind: "seqExpr",
stmts,
result: {
kind: "ternary",
cond: missing,
then: fallback,
else_: found,
type: read.type,
loc,
},
type: read.type,
loc,
};
}
/** `%rec.inherited.<n>(o, k)`: the inherited method `k` names as a value
* of `readType` (bound to `o`), or undefined. */
function inheritedReadHelper(
lowerer: Lowerer,
shapeId: string,
readType: IrType,
form: FuncType | "dyn",
bound: boolean,
loc: SrcLoc,
): string {
const key = `rec.inherited:${bound ? "bound" : "value"}:${shapeId}:${typeKey(readType)}`;
const existing = lowerer.arrHofHelpers.get(key);
if (existing) return existing;
const name = keyedHelperName(lowerer, "%rec.inherited", key);
lowerer.arrHofHelpers.set(key, name);
const recT: IrType = { kind: "record", shapeId };
const o = varRef("o.0", recT, loc);
const k = varRef("k.0", STRING, loc);
const fnType: FuncType =
form === "dyn" ? ({ kind: "func", params: [DYN], ret: DYN } as FuncType) : form;
const undefinedValue: IrExpr =
form === "dyn"
? dynUndefinedExpr(loc)
: lowerer.coerceToExpected(
{ kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
readType,
);
const named = (method: string): IrExpr => ({
kind: "strEq",
negated: false,
left: k,
right: { kind: "strLit", value: method, type: STRING, loc },
type: BOOL,
loc,
});
const methods = [...RECORD_INHERITED_METHODS];
let anyName: IrExpr = named(methods[0]!);
for (const method of methods.slice(1))
anyName = { kind: "logical", op: "||", left: anyName, right: named(method), type: BOOL, loc };
const body: IrStmt[] = [
// Most misses name no inherited method: one comparison chain, nothing else.
{
kind: "if",
cond: { kind: "unary", op: "!", operand: anyName, type: BOOL, loc },
then: [{ kind: "return", value: { ...undefinedValue }, loc }],
else_: null,
loc,
},
// An own key holding undefined shadows the prototype like any own key.
{
kind: "if",
cond: {
kind: "call",
callee: recordHasOwnHelper(lowerer, shapeId, loc),
args: [o, k],
type: BOOL,
loc,
},
then: [{ kind: "return", value: { ...undefinedValue }, loc }],
else_: null,
loc,
},
];
if (nullProtoRecordShape(lowerer, recT))
body.push({
kind: "if",
cond: recordNullProtoTest(o, shapeId, loc),
then: [{ kind: "return", value: { ...undefinedValue }, loc }],
else_: null,
loc,
});
for (const method of methods) {
const value: IrExpr = bound
? {
kind: "call",
callee: boundMethodFactory(lowerer, shapeId, method, fnType, loc),
args: [o],
type: fnType,
loc,
}
: {
kind: "closure",
fnName: unboundMethodImpl(lowerer, method, fnType, loc),
captures: [],
type: fnType,
loc,
};
body.push({
kind: "if",
cond: named(method),
then: [
{
kind: "return",
value: lowerer.coerceToExpected(value, form === "dyn" ? DYN : readType),
loc,
},
],
else_: null,
loc,
});
}
body.push({ kind: "return", value: { ...undefinedValue }, loc });
lowerer.liftedFns.push({
name,
params: [
{ localId: "o.0", name: "o", type: recT },
{ localId: "k.0", name: "k", type: STRING },
],
returnType: readType,
locals: [
{ id: "o.0", name: "o", type: recT, mutable: false },
{ id: "k.0", name: "k", type: STRING, mutable: false },
],
body,
loc,
});
return name;
}
/** `%rec.bind.<n>(o)`: the built-in `method` bound to `o` as a closure. */
function boundMethodFactory(
lowerer: Lowerer,
shapeId: string,
method: string,
fnType: FuncType,
loc: SrcLoc,
): string {
const key = `rec.bind:${shapeId}:${method}:${typeKey(fnType)}`;
const existing = lowerer.arrHofHelpers.get(key);
if (existing) return existing;
const name = keyedHelperName(lowerer, "%rec.bind", key);
lowerer.arrHofHelpers.set(key, name);
const recT: IrType = { kind: "record", shapeId };
lowerer.liftedFns.push({
name,
params: [{ localId: "o.0", name: "o", type: recT }],
returnType: fnType,
locals: [{ id: "o.0", name: "o", type: recT, mutable: false, boxed: true }],
body: [
{
kind: "return",
value: {
kind: "closure",
fnName: inheritedMethodImpl(lowerer, shapeId, method, fnType, loc),
captures: ["o.0"],
type: fnType,
loc,
},
loc,
},
],
loc,
});
return name;
}
/** The lifted body of one built-in bound to its record, with the read's
* function signature: the built-in's answer converted to the signature's
* return type, or the checked-conversion TypeError where it cannot be. */
function inheritedMethodImpl(
lowerer: Lowerer,
shapeId: string,
method: string,
fnType: FuncType,
loc: SrcLoc,
): string {
const key = `rec.inheritedImpl:${shapeId}:${method}:${typeKey(fnType)}`;
const existing = lowerer.arrHofHelpers.get(key);
if (existing) return existing;
const name = keyedHelperName(lowerer, `%rec.${method}`, key);
lowerer.arrHofHelpers.set(key, name);
const recT: IrType = { kind: "record", shapeId };
const o = varRef("o.0", recT, loc);
const params = fnType.params.map((type, i) => ({ localId: `p${i}.0`, name: `p${i}`, type }));
const args = params.map((p) => varRef(p.localId, p.type, loc));
const body: IrStmt[] = [];
const hidden = liftedLocals();
const answer = builtinAnswer(lowerer, hidden.declare, o, shapeId, method, args, fnType.ret, loc);
body.push(...returnAs(lowerer, answer, fnType.ret, loc));
lowerer.liftedFns.push({
name,
params,
returnType: fnType.ret,
captures: [{ localId: "o.0", name: "o", type: recT }],
locals: [
{ id: "o.0", name: "o", type: recT, mutable: false, boxed: true },
...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
...hidden.locals,
],
body,
loc,
});
return name;
}
/** One built-in as a plain function value (no receiver): its answer for an
* undefined `this`, converted like inheritedMethodImpl's. */
function unboundMethodImpl(
lowerer: Lowerer,
method: string,
fnType: FuncType,
loc: SrcLoc,
): string {
const key = `rec.unboundImpl:${method}:${typeKey(fnType)}`;
const existing = lowerer.arrHofHelpers.get(key);
if (existing) return existing;
const name = keyedHelperName(lowerer, `%obj.${method}`, key);
lowerer.arrHofHelpers.set(key, name);
const params = fnType.params.map((type, i) => ({ localId: `p${i}.0`, name: `p${i}`, type }));
const arg: IrExpr | undefined = params[0] && varRef(params[0].localId, params[0].type, loc);
const coerce = "Cannot convert undefined or null to object";
let answer: IrExpr;
switch (method) {
case "toString":
answer = { kind: "strLit", value: "[object Undefined]", type: STRING, loc };
break;
case "toLocaleString":
answer = nodeThrowExpr(
1,
"",
"Object.prototype.toLocaleString called on null or undefined",
STRING,
loc,
);
break;
case "isPrototypeOf": {
// A primitive argument answers false before `this` converts.
const no: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc };
const kind = arg?.type.kind;
if (
arg === undefined ||
isUnitType(arg.type) ||
kind === "string" ||
kind === "f64" ||
kind === "bool" ||
kind === "bigint"
)
answer = no;
else if (kind === "dyn")
answer = {
kind: "ternary",
cond: {
kind: "logical",
op: "||",
left: { kind: "dynTest", test: "object", value: arg, type: BOOL, loc },
right: { kind: "dynTest", test: "function", value: arg, type: BOOL, loc },
type: BOOL,
loc,
} as IrExpr,
then: nodeThrowExpr(1, "", coerce, BOOL, loc),
else_: no,
type: BOOL,
loc,
};
else answer = nodeThrowExpr(1, "", coerce, BOOL, loc);
break;
}
default:
answer = nodeThrowExpr(1, "", coerce, method === "valueOf" ? DYN : BOOL, loc);
}
const body: IrStmt[] =
answer.kind === "libCall" && answer.fn === "error.nodeThrow"
? [
{
kind: "return",
value: nodeThrowExpr(
1,
"",
(answer.args[2] as IrExpr & { kind: "strLit" }).value,
fnType.ret,
loc,
),
loc,
},
]
: returnAs(lowerer, answer, fnType.ret, loc);
lowerer.liftedFns.push({
name,
params,
returnType: fnType.ret,
locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
body,
loc,
});
return name;
}
/** The built-in's answer with `o` as its receiver. `want` is the type the
* caller needs (valueOf answers the record itself). */
function builtinAnswer(
lowerer: Lowerer,
declare: DeclareLocal,
o: IrExpr,
shapeId: string,
method: string,
args: readonly IrExpr[],
want: IrType,
loc: SrcLoc,
): IrExpr {
switch (method) {
case "toString":
return { kind: "strLit", value: "[object Object]", type: STRING, loc };
case "toLocaleString":
// Object.prototype.toLocaleString calls this.toString().
return callOwnOrBuiltin(
lowerer,
declare,
o,
shapeId,
"toString",
[],
STRING,
"toString",
loc,
);
case "valueOf":
return want.kind === "dyn" ? lowerer.coerceToExpected(o, DYN) : o;
case "isPrototypeOf":
// No object is created with a typed record as its prototype.
return { kind: "boolLit", value: false, type: BOOL, loc };
default: {
// hasOwnProperty and propertyIsEnumerable: ToPropertyKey keeps a
// symbol a symbol, and a record's own keys are strings, so a symbol
// key is no entry of it.
const no: IrExpr = { kind: "boolLit", value: false, type: BOOL, loc };
const arg = args[0];
if (arg?.type.kind === "symbol") return sequence(arg, no, loc);
const has = (key: IrExpr): IrExpr => ({
kind: "call",
callee: recordHasOwnHelper(lowerer, shapeId, loc),
args: [o, key],
type: BOOL,
loc,
});
if (arg === undefined || !mayBeSymbol(arg.type)) return has(propertyKeyOf(lowerer, arg, loc));
const id = declare("%protoKey", DYN);
const held = varRef(id, DYN, loc);
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: id, init: lowerer.coerceToExpected(arg, DYN), loc }],
result: {
kind: "ternary",
cond: { kind: "dynTest", test: "symbol", value: held, type: BOOL, loc } as IrExpr,
then: no,
else_: has(propertyKeyOf(lowerer, held, loc)),
type: BOOL,
loc,
},
type: BOOL,
loc,
};
}
}
}
/** Whether a value of `type` may be a symbol at run time. */
function mayBeSymbol(type: IrType): boolean {
return !(
isUnitType(type) ||
type.kind === "string" ||
type.kind === "f64" ||
type.kind === "bool"
);
}
/** ToPropertyKey of a built-in's key argument (absent: "undefined"), for
* keys that are not symbols. */
function propertyKeyOf(lowerer: Lowerer, arg: IrExpr | undefined, loc: SrcLoc): IrExpr {
if (arg === undefined || isUnitType(arg.type))
return {
kind: "strLit",
value: arg?.type.kind === "nullT" ? "null" : "undefined",
type: STRING,
loc,
};
if (arg.type.kind === "string") return arg;
if (arg.type.kind === "f64" || arg.type.kind === "bool")
return { kind: "toString", operand: arg, type: STRING, loc };
return {
kind: "libCall",
fn: "dyn.toStringCoerce",
args: [lowerer.coerceToExpected(arg, DYN)],
type: STRING,
loc,
};
}
/** `value` returned as `ret`: as is, wrapped into a union arm or a checked
* value, discarded for void, or the checked-conversion TypeError. */
function returnAs(lowerer: Lowerer, value: IrExpr, ret: IrType, loc: SrcLoc): IrStmt[] {
const converted = convertTo(lowerer, value, ret);
if (converted !== null) return [{ kind: "return", value: converted, loc }];
if (ret.kind === "void")
return [
{ kind: "exprStmt", expr: value, loc },
{ kind: "return", value: null, loc },
];
return [
{ kind: "exprStmt", expr: value, loc },
{ kind: "return", value: nodeThrowExpr(1, "", OTHER_ARM_FAILURE_MESSAGE, ret, loc), loc },
];
}
function convertTo(lowerer: Lowerer, value: IrExpr, want: IrType): IrExpr | null {
if (typeEquals(value.type, want)) return value;
if (want.kind === "dyn" && lowerer.dynConvertible(value.type))
return lowerer.coerceToExpected(value, DYN);
if (want.kind === "union" && lowerer.armTag(want.unionId, value.type) >= 0)
return lowerer.coerceToExpected(value, want);
return null;
}
/** `o.method(...args)` on a record: an own callable entry when the record
* holds one, else the built-in. `calleeText` names the callee for Node's
* "is not a function". */
function callOwnOrBuiltin(
lowerer: Lowerer,
declare: DeclareLocal,
o: IrExpr,
shapeId: string,
method: string,
args: readonly IrExpr[],
want: IrType,
calleeText: string,
loc: SrcLoc,
resolved?: { has: IrExpr; own: IrExpr },
): IrExpr {
const shape = lowerer.shapes.get(shapeId)!;
const field = shape.fields.find((f) => f.name === method);
const ownType = field ? field.type : shape.indexValue;
let builtin = builtinAnswer(lowerer, declare, o, shapeId, method, args, want, loc);
const recT: IrType = { kind: "record", shapeId };
if (nullProtoRecordShape(lowerer, recT))
builtin = {
kind: "ternary",
cond: recordNullProtoTest(o, shapeId, loc),
then: nodeThrowExpr(1, "", `${calleeText} is not a function`, builtin.type, loc),
else_: builtin,
type: builtin.type,
loc,
};
const builtinAsWant = convertTo(lowerer, builtin, want) ?? builtin;
if (ownType === undefined || !callableType(lowerer, ownType)) return builtinAsWant;
const own = resolved?.own ?? ownEntryRead(lowerer, o, shapeId, method, loc);
const called = callOwn(lowerer, declare, own, args, builtinAsWant.type, calleeText, loc);
return {
kind: "ternary",
cond: resolved?.has ?? ownEntryTest(lowerer, o, shapeId, method, loc),
then: called,
else_: builtinAsWant,
type: builtinAsWant.type,
loc,
};
}
/** Whether the record holds an own entry named `method`. */
function ownEntryTest(
lowerer: Lowerer,
o: IrExpr,
shapeId: string,
method: string,
loc: SrcLoc,
): IrExpr {
return {
kind: "call",
callee: recordHasOwnHelper(lowerer, shapeId, loc),
args: [o, { kind: "strLit", value: method, type: STRING, loc }],
type: BOOL,
loc,
};
}
/** The record's own entry named `method`: its field, or its overflow entry
* (undefined when absent). */
function ownEntryRead(
lowerer: Lowerer,
o: IrExpr,
shapeId: string,
method: string,
loc: SrcLoc,
): IrExpr {
const shape = lowerer.shapes.get(shapeId)!;
const field = shape.fields.find((f) => f.name === method);
return field
? { kind: "recordGet", obj: o, shapeId, field: method, type: field.type, loc }
: {
kind: "recordKeyGet",
obj: o,
shapeId,
key: { kind: "strLit", value: method, type: STRING, loc },
overflowOnly: true,
type: lowerer.withUndefinedArmOf(shape.indexValue!) ?? shape.indexValue!,
loc,
};
}
/** Call the own entry `own` (present, of any stored type) with `args`. A
* non-function throws Node's "is not a function"; a function whose
* parameters or result cannot carry the call throws the checked-conversion
* TypeError (the program's types claimed otherwise). */
function callOwn(
lowerer: Lowerer,
declare: DeclareLocal,
own: IrExpr,
args: readonly IrExpr[],
want: IrType,
calleeText: string,
loc: SrcLoc,
): IrExpr {
const notFunction = nodeThrowExpr(1, "", `${calleeText} is not a function`, want, loc);
const lie = nodeThrowExpr(1, "", OTHER_ARM_FAILURE_MESSAGE, want, loc);
const localId = declare("%ownMethod", own.type);
const value = varRef(localId, own.type, loc);
const enter = (fn: IrExpr): IrExpr => {
if (fn.type.kind === "dyn") {
const result: IrExpr = {
kind: "dynCall",
callee: fn,
calleeName: calleeText,
args: args.map((a) => lowerer.coerceToExpected(a, DYN)),
type: DYN,
loc,
} as IrExpr;
return convertTo(lowerer, result, want) ?? sequence(result, lie, loc);
}
const sig = fn.type as FuncType;
if (sig.restAbi !== undefined) return lie;
const passed: IrExpr[] = [];
for (let i = 0; i < sig.params.length; i++) {
const param = sig.params[i]!;
const arg = args[i];
const given =
arg === undefined
? convertTo(
lowerer,
{ kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
param,
)
: convertTo(lowerer, arg, param);
if (given === null) return lie;
passed.push(given);
}
const result: IrExpr = { kind: "callValue", callee: fn, args: passed, type: sig.ret, loc };
if (sig.ret.kind === "void") return sequence(result, lie, loc);
return convertTo(lowerer, result, want) ?? sequence(result, lie, loc);
};
let call: IrExpr;
if (own.type.kind === "func" || own.type.kind === "dyn") call = enter(value);
else if (own.type.kind === "union") {
const unionId = own.type.unionId;
const arms = lowerer.unions.get(unionId)?.arms ?? [];
call = notFunction;
for (let tag = arms.length - 1; tag >= 0; tag--) {
const arm = arms[tag]!;
if (arm.kind !== "func" && arm.kind !== "dyn") continue;
call = {
kind: "ternary",
cond: { kind: "unionIsTag", unionId, tag, negated: false, value, type: BOOL, loc },
then: enter({ kind: "unionNarrow", unionId, tag, value, type: arm, loc }),
else_: call,
type: want,
loc,
};
}
} else call = notFunction;
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId, init: own, loc }],
result: call,
type: want,
loc,
};
}
function sequence(first: IrExpr, result: IrExpr, loc: SrcLoc): IrExpr {
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: first, loc }],
result,
type: result.type,
loc,
};
}
/** `r.toString()`, `r.hasOwnProperty(k)` and the other Object.prototype
* methods called by name on a typed record (the checker resolved the
* member to Object.prototype's). Null for other calls. */
export function lowerRecordInheritedCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
const method = access.name.text;
if (
call.questionDotToken ||
access.questionDotToken ||
!RECORD_INHERITED_METHODS.has(method) ||
call.arguments.nodes.some(ts.isSpreadElement) ||
!lowerer.isStdlibMember(access)
)
return null;
const recvT = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
if (recvT?.kind !== "record") return null;
const shape = lowerer.shapes.get(recvT.shapeId);
if (!shape || shape.tuple) return null;
const want =
method === "valueOf"
? (lowerer.mapTypeOf(lowerer.typeOf(call)) ?? recvT)
: method === "toString" || method === "toLocaleString"
? STRING
: BOOL;
// Without a possible own function, toString and the own-key tests keep
// their existing lowerings (with the special record views they know).
if (
(method === "hasOwnProperty" || method === "propertyIsEnumerable" || method === "toString") &&
!shape.fields.some((f) => f.name === method && callableType(lowerer, f.type)) &&
!(shape.indexValue && callableType(lowerer, shape.indexValue))
)
return null;
const loc = locOf(call);
const stmts: IrStmt[] = [];
const receiver = lowerer.lowerExpr(access.expression);
if (receiver.type.kind !== "record") return null;
const recvLocal = lowerer.declareHiddenLocal("%protoRecv", receiver.type);
stmts.push({ kind: "varDecl", localId: recvLocal.id, init: receiver, loc });
const o = varRef(recvLocal.id, receiver.type, loc);
// `o.m(...)` reads the member before its arguments run: an argument that
// adds, deletes or replaces the own method does not change the callee.
const field = shape.fields.find((f) => f.name === method);
const ownType = field ? field.type : shape.indexValue;
let resolved: { has: IrExpr; own: IrExpr } | undefined;
if (ownType !== undefined && callableType(lowerer, ownType)) {
const shapeId = receiver.type.shapeId;
const hasLocal = lowerer.declareHiddenLocal("%protoHas", BOOL);
stmts.push({
kind: "varDecl",
localId: hasLocal.id,
init: ownEntryTest(lowerer, o, shapeId, method, loc),
loc,
});
const read = ownEntryRead(lowerer, o, shapeId, method, loc);
const ownLocal = lowerer.declareHiddenLocal("%protoOwn", read.type);
stmts.push({ kind: "varDecl", localId: ownLocal.id, init: read, loc });
resolved = { has: varRef(hasLocal.id, BOOL, loc), own: varRef(ownLocal.id, read.type, loc) };
}
const args: IrExpr[] = [];
for (const node of call.arguments.nodes) {
const arg = lowerer.lowerExpr(node);
const local = lowerer.declareHiddenLocal("%protoArg", arg.type);
stmts.push({ kind: "varDecl", localId: local.id, init: arg, loc });
args.push(varRef(local.id, arg.type, loc));
}
const result = callOwnOrBuiltin(
lowerer,
(name, type) => lowerer.declareHiddenLocal(name, type).id,
o,
receiver.type.shapeId,
method,
args,
want,
ts.getTextOfNode(access),
loc,
resolved,
);
return { kind: "seqExpr", stmts, result, type: result.type, loc };
}
/** `r.toString` and the other Object.prototype methods read by name (not
* called) on an index-signature record: an own entry of the member's
* function type when the record holds one, else the built-in as a plain
* function value. An own entry of another type than the member's (a
* function of another signature) throws the checked-conversion TypeError:
* the program's types claimed the member. Null when the checker's member
* type is not a function. */
export function lowerRecordInheritedMember(
lowerer: Lowerer,
receiver: IrExpr,
expr: ts.PropertyAccessExpression,
): IrExpr | null {
const method = expr.name.text;
if (!RECORD_INHERITED_METHODS.has(method) || !lowerer.isStdlibMember(expr)) return null;
if (receiver.type.kind !== "record") return null;
const want = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (want?.kind !== "func" || want.restAbi !== undefined) return null;
const shapeId = receiver.type.shapeId;
const shape = lowerer.shapes.get(shapeId);
if (!shape || shape.tuple) return null;
const loc = locOf(expr);
const builtin: IrExpr = {
kind: "closure",
fnName: unboundMethodImpl(lowerer, method, want, loc),
captures: [],
type: want,
loc,
};
const recvLocal = lowerer.declareHiddenLocal("%protoRecv", receiver.type);
const o = varRef(recvLocal.id, receiver.type, loc);
const stmts: IrStmt[] = [{ kind: "varDecl", localId: recvLocal.id, init: receiver, loc }];
let result: IrExpr = builtin;
if (shape.indexValue && typeEquals(shape.indexValue, want)) {
const ownType = lowerer.withUndefinedArmOf(want);
if (ownType?.kind === "union") {
const read: IrExpr = {
kind: "recordKeyGet",
obj: o,
shapeId,
key: { kind: "strLit", value: method, type: STRING, loc },
overflowOnly: true,
type: ownType,
loc,
};
const tag = lowerer.armTag(ownType.unionId, want);
const ownLocal = lowerer.declareHiddenLocal("%ownMethod", ownType);
const own = varRef(ownLocal.id, ownType, loc);
stmts.push({ kind: "varDecl", localId: ownLocal.id, init: read, loc });
result = {
kind: "ternary",
cond: {
kind: "unionIsTag",
unionId: ownType.unionId,
tag,
negated: false,
value: own,
type: BOOL,
loc,
},
then: { kind: "unionNarrow", unionId: ownType.unionId, tag, value: own, type: want, loc },
else_: builtin,
type: want,
loc,
};
}
} else if (shape.indexValue && callableType(lowerer, shape.indexValue)) {
result = {
kind: "ternary",
cond: ownEntryTest(lowerer, o, shapeId, method, loc),
then: nodeThrowExpr(1, "", OTHER_ARM_FAILURE_MESSAGE, want, loc),
else_: builtin,
type: want,
loc,
};
}
return { kind: "seqExpr", stmts, result, type: want, loc };
}
@@ -1740,7 +1740,8 @@ function buildBuiltinModuleFns(): BuiltinModuleFnTable {
// lowerVarDecl / collectGlobals BEFORE any call lowering runs — and
// inspect/format/formatWithOptions plus parseArgs through the util spoke
// (lower-inspect.ts: per-type synthesized traversal helpers,
// compile-time format strings, checked-dynamic config/result for
// compile-time format strings and the runtime formatter for the others,
// checked-dynamic config/result for
// parseArgs), which both dispatch paths try before the generic table.
// parseArgs is tabled as well so the generated surface manifest records
// the dedicated static member; the spoke owns its optional arity and
@@ -1767,6 +1768,67 @@ function buildBuiltinModuleFns(): BuiltinModuleFnTable {
],
},
parseArgs: { fn: "util.parseArgs", params: [DYN], result: DYN },
aborted: {
fn: "util.aborted",
params: [DYN, DYN],
result: DYN,
valueParams: [
{ mode: "optional", type: DYN, defaultValue: undefined },
{ mode: "optional", type: DYN, defaultValue: undefined },
],
},
diff: {
fn: "util.diff",
params: [DYN, DYN],
result: DYN,
valueParams: [
{ mode: "optional", type: DYN, defaultValue: undefined },
{ mode: "optional", type: DYN, defaultValue: undefined },
],
},
debounce: {
fn: "util.debounce",
params: [DYN, DYN, DYN],
result: DYN,
valueParams: [
{ mode: "optional", type: DYN, defaultValue: undefined },
{ mode: "optional", type: DYN, defaultValue: undefined },
{ mode: "optional", type: DYN, defaultValue: undefined },
],
},
throttle: {
fn: "util.throttle",
params: [DYN, DYN, DYN, DYN],
result: DYN,
valueParams: [
{ mode: "optional", type: DYN, defaultValue: undefined },
{ mode: "optional", type: DYN, defaultValue: undefined },
{ mode: "optional", type: DYN, defaultValue: undefined },
{ mode: "optional", type: DYN, defaultValue: undefined },
],
},
convertProcessSignalToExitCode: {
fn: "util.signalExitCode",
params: [DYN],
result: F64,
valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }],
},
// The deprecated helpers report DEP0044 and DEP0060 on their first call.
isArray: {
fn: "util.isArrayDeprecated",
params: [DYN],
result: BOOL,
valueParams: [{ mode: "optional", type: DYN, defaultValue: undefined }],
},
_extend: {
fn: "util.extend",
params: [DYN, DYN],
result: DYN,
valueParams: [
{ mode: "optional", type: DYN, defaultValue: undefined },
{ mode: "optional", type: DYN, defaultValue: undefined },
],
},
parseEnv: { fn: "util.parseEnv", params: [DYN], result: DYN },
getSystemErrorName: {
fn: "util.getSystemErrorName",
@@ -76,7 +76,36 @@ export function fenceSymbolFieldCopy(
}
}
/** The registry name util.inspect.custom denotes. */
export const INSPECT_CUSTOM_DESCRIPTION = "nodejs.util.inspect.custom";
/** The method slot of a class's `[util.inspect.custom]` member. */
export const INSPECT_CUSTOM_MEMBER = `${FIELD_PREFIX}Symbol(${INSPECT_CUSTOM_DESCRIPTION})`;
/** `inspect.custom` over node:util's inspect: a named import, `util.inspect`
* or the CommonJS `util_1.inspect`. */
export function isInspectCustomAccess(lowerer: Lowerer, expr: ts.Expression): boolean {
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
if (!ts.isPropertyAccessExpression(expr) || expr.questionDotToken) return false;
if (!ts.isIdentifier(expr.name) || expr.name.text !== "custom") return false;
let target = expr.expression;
while (ts.isParenthesizedExpression(target)) target = target.expression;
const bi = ts.isIdentifier(target)
? lowerer.builtinImportOf(target)
: ts.isPropertyAccessExpression(target) && !target.questionDotToken
? lowerer.builtinMemberOf(target)
: null;
return bi?.module === "util" && bi.member === "inspect";
}
export function classSymbolKeyOf(lowerer: Lowerer, key: ts.Expression): ClassSymbolKey | null {
// util.inspect.custom is the registered Symbol.for(INSPECT_CUSTOM_DESCRIPTION),
// one identity with the program's own Symbol.for calls of that name.
if (isInspectCustomAccess(lowerer, key)) {
let e = key;
while (ts.isParenthesizedExpression(e)) e = e.expression;
const sym = lowerer.checker.getSymbolAtLocation((e as ts.PropertyAccessExpression).name);
return sym ? symbolKeyOf(lowerer, sym, true, INSPECT_CUSTOM_DESCRIPTION) : null;
}
const wellKnown = wellKnownSymbolKeyOf(lowerer, key);
if (wellKnown !== undefined) return wellKnown;
if (
@@ -32,6 +32,9 @@ export function buildUnionNarrow(
formatType: (type: IrType) => string,
undefinedDefault: IrExpr | null = null,
failureMessage: string | null = null,
/** Converts another arm's payload into the target (a record arm that is
* a width subtype of a record target), or null to throw. */
convert: ((tag: number, payload: IrExpr) => IrExpr | null) | null = null,
): IrFunction | null {
if (isUnitType(target) || target.kind === "void") return null;
const targetTag = from.arms.findIndex((arm) => typeEquals(arm, target));
@@ -47,6 +50,7 @@ export function buildUnionNarrow(
const inputType: IrType = { kind: "union", unionId: from.id };
const input: IrExpr = { kind: "varRef", localId: "u.0", type: inputType, loc };
const body: IrStmt[] = [];
let converted: IrExpr | null = null;
for (let tag = 0; tag < from.arms.length; tag++) {
const arm = from.arms[tag]!;
let branch: IrStmt[];
@@ -60,6 +64,19 @@ export function buildUnionNarrow(
];
} else if (arm.kind === "undefinedT" && undefinedDefault !== null) {
branch = [{ kind: "return", value: undefinedDefault, loc }];
} else if (
convert !== null &&
!isUnitType(arm) &&
(converted = convert(tag, {
kind: "unionNarrow",
unionId: from.id,
tag,
value: input,
type: arm,
loc,
})) !== null
) {
branch = [{ kind: "return", value: converted, loc }];
} else {
branch = [
{
+2 -1
View File
@@ -23,7 +23,7 @@ import {
trackedRealpath,
} from "./input-tracker.js";
import { npmBuiltinAliasOf } from "./builtin-modules.js";
import { packageSourceText } from "./type-patches/layer.js";
import { packageSourceText, typePatchSuppliesTypes } from "./type-patches/layer.js";
let activePackages: ReadonlySet<string> = new Set();
let declarationOverloads: ReadonlyMap<string, NpmStaticDeclarationOverloads> = new Map();
@@ -374,6 +374,7 @@ function packageIsUntyped(path: string): boolean {
/* no package.json — keep probing */
}
if (untyped && trackedExists(`${pkgDir}/index.d.ts`)) untyped = false;
if (untyped && typePatchSuppliesTypes(pkgDir)) untyped = false;
if (untyped) {
// the @types twin, hoisted anywhere up the realm chain
const mangled = mangledTypesName(dirName);
+18 -3
View File
@@ -665,6 +665,14 @@ function createRequireProgramRoots7(program: ts.Program): string[] {
return [...roots].sort();
}
/** Whether the tsc gate can keep a file's diagnostics when the program
* loads: it drops all of an opted-in package's files and of the package
* shims (keptErrors in preflight also drops unselected package JavaScript,
* which is not known yet here, so those files are still checked). */
function tscGateFileKept(fileName: string): boolean {
return npmStaticPackageOfPath(fileName) === null && !isPackageShimFile(fileName);
}
/** Classify the explicitly selected installed package's own sources. */
export function entryPackageFilePredicate(entryPath: string): (file: string) => boolean {
const entryFile = tsgoPath(entryPath);
@@ -934,7 +942,7 @@ function loadProgram7(
const original = program;
// The project world must read the original text too, so it is built
// before the rewritten files are served, and only when it is needed.
const diagnostics = ts.getPreEmitDiagnostics(original);
const diagnostics = ts.getPreEmitDiagnostics(original, tscGateFileKept);
const errors = diagnostics.some((d) => d.category === ts.DiagnosticCategory.Error);
tscWorld = {
program: original,
@@ -982,7 +990,7 @@ function loadProgram7(
} else {
tscWorld = {
program: original,
diagnostics: ts.getPreEmitDiagnostics(original),
diagnostics: ts.getPreEmitDiagnostics(original, tscGateFileKept),
projectWorld: null,
declWorldFallback: true,
};
@@ -5438,7 +5446,14 @@ function preflight7(load: LoadResult): {
const sf = p.getSourceFile(d.fileName);
return sf !== undefined && isCheckedJsSourceFile(sf, projectCheckJs);
};
const errorsOf = (p: ts.Program): ts.Diagnostic[] => keptErrors(p, ts.getPreEmitDiagnostics(p));
// keptErrors drops every diagnostic of these files: the gate does not ask
// the checker for them.
const diagnosticsKept = (fileName: string): boolean =>
npmStaticPackageOfPath(fileName) === null &&
!unselectedPackageJsFile(fileName) &&
!isPackageShimFile(fileName);
const errorsOf = (p: ts.Program): ts.Diagnostic[] =>
keptErrors(p, ts.getPreEmitDiagnostics(p, diagnosticsKept));
const keptErrors = (p: ts.Program, all: readonly ts.Diagnostic[]): ts.Diagnostic[] => {
// First pass: every comment-side 2300's (file, name) — the partners
// the second pass forgives.
+3 -1
View File
@@ -37,9 +37,11 @@ import {
trackedRealpath,
} from "./input-tracker.js";
import { isRelativeSpecifier, packageNameOfSpecifier } from "./workspace-registry.js";
import { typePatchFileExists } from "./type-patches/layer.js";
function isFile(path: string): boolean {
return trackedFileExists(path);
// A declaration entry a project type patch supplies (type-patches/layer.ts).
return trackedFileExists(path) || typePatchFileExists(path);
}
function isDirectory(path: string): boolean {
@@ -4,6 +4,7 @@ import { join } from "node:path";
import { afterAll, afterEach, expect, test } from "vitest";
import type { SourceFile } from "./ast-types.js";
import {
reusedKeptLibDiagnostics,
reusedLibDiagnostics,
setLibDiagnosticsStore,
type LibDiagnosticsStore,
@@ -118,6 +119,45 @@ test("a stored answer stands in for checking the supplied files, merged by file
expect(full?.semantic).toEqual(reused?.semantic);
});
test("with kept files, a stored answer needs checking only the kept other files", () => {
const store = memoryStore();
setLibDiagnosticsStore(store, "checker");
const pkg = join(dir, "node_modules", "pkg", "index.d.ts");
const semantic = new Map([
[lib, [diagnostic(lib, 1)]],
[fallback, []],
[main, [diagnostic(main, 5)]],
[pkg, [diagnostic(pkg, 3)]],
]);
const withPackage = files({
[main]: "export const n: number = 'x';\n",
[pkg]: "export declare const p: number;\n",
});
const all = [lib, fallback, main, pkg];
const keep = new Set([lib, fallback, main]);
// Without a stored answer nothing is checked or stored.
const cold = sessionProgram({ semantic });
expect(reusedKeptLibDiagnostics(cold, all, supplied, withPackage, keep)).toBeNull();
expect(cold.asked).toEqual([]);
expect(store.entries.size).toBe(0);
reusedLibDiagnostics(sessionProgram({ semantic }), all, supplied, withPackage);
expect(store.entries.size).toBe(1);
const warm = sessionProgram({ semantic });
expect(reusedKeptLibDiagnostics(warm, all, supplied, withPackage, keep)).toEqual({
semantic: [diagnostic(lib, 1), diagnostic(main, 5)],
global: [],
});
expect(warm.asked).toEqual([`checked ${main}`]);
// A left-out file that can declare something global still prevents reuse.
const augmenting = files({
[main]: "export const n: number = 'x';\n",
[pkg]: "export {};\ndeclare global {\n var NaN: number;\n}\n",
});
const blocked = sessionProgram({ semantic });
expect(reusedKeptLibDiagnostics(blocked, all, supplied, augmenting, keep)).toBeNull();
expect(blocked.asked).toEqual([]);
});
test("files merge in code point order, which differs from UTF-16 order", () => {
const store = memoryStore();
setLibDiagnosticsStore(store, "checker");
@@ -64,6 +64,31 @@ export function reusedLibDiagnostics(
fileNames: readonly string[],
supplied: (fileName: string) => boolean,
sourceFile: (fileName: string) => SourceFile | undefined,
): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null {
return reuse(program, fileNames, supplied, sourceFile, () => true, false);
}
/** reusedLibDiagnostics for a caller that keeps only the diagnostics of the
* `kept` files: a stored answer then needs checking only the kept other
* files, and without a stored answer the result is null (nothing is checked
* or stored). */
export function reusedKeptLibDiagnostics(
program: Ts7SessionProgram,
fileNames: readonly string[],
supplied: (fileName: string) => boolean,
sourceFile: (fileName: string) => SourceFile | undefined,
kept: ReadonlySet<string>,
): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null {
return reuse(program, fileNames, supplied, sourceFile, (name) => kept.has(name), true);
}
function reuse(
program: Ts7SessionProgram,
fileNames: readonly string[],
supplied: (fileName: string) => boolean,
sourceFile: (fileName: string) => SourceFile | undefined,
keep: (fileName: string) => boolean,
storedOnly: boolean,
): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null {
if (configured === null || process.env["SCRIPTC_LIB_DIAGNOSTICS"] === "0") return null;
const { store, checker } = configured;
@@ -104,12 +129,16 @@ export function reusedLibDiagnostics(
if (globals !== null) writeEntry(store, key, globals, Object.fromEntries(stored.files));
}
if (stored !== null && (scripts.length === 0 || (globals !== null && !collides(globals)))) {
const checked = program.checkedFileDiagnosticData(own);
const checkedNames = storedOnly ? own.filter(keep) : own;
const checked = program.checkedFileDiagnosticData(checkedNames);
if (checked === null) return null;
const byFile = new Map<string, Ts7DiagnosticData[]>(stored.files);
own.forEach((name, index) => byFile.set(name, checked.files[index] ?? []));
const byFile = new Map<string, Ts7DiagnosticData[]>(
storedOnly ? [...stored.files].filter(([name]) => keep(name)) : stored.files,
);
checkedNames.forEach((name, index) => byFile.set(name, checked.files[index] ?? []));
return { semantic: inFileOrder(byFile), global: checked.global };
}
if (storedOnly) return null;
// The full check, as before. Its answers for the supplied files are stored
// when it reports no global diagnostics and the program's scripts declare
// none of the supplied files' globals (so nothing of the program reached
@@ -339,7 +368,7 @@ function readEntry(
/** Each file's diagnostics (already in the checker's order) in the order the
* checker sorts files: by file name, compared by code point. */
function inFileOrder(byFile: ReadonlyMap<string, Ts7DiagnosticData[]>): Ts7DiagnosticData[] {
export function inFileOrder(byFile: ReadonlyMap<string, Ts7DiagnosticData[]>): Ts7DiagnosticData[] {
const names = [...byFile.keys()].sort(compareCodePoints);
const out: Ts7DiagnosticData[] = [];
for (const name of names) for (const d of byFile.get(name)!) out.push(d);
@@ -3,7 +3,12 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, expect, test, vi } from "vitest";
import { Ts7Api } from "./rpc-api.js";
import { Ts7Host, type Ts7ApiOptions, type Ts7CompilerOptions } from "./program-host.js";
import {
getPreEmitDiagnostics,
Ts7Host,
type Ts7ApiOptions,
type Ts7CompilerOptions,
} from "./program-host.js";
import { ModuleDetectionKind, ModuleKind, ModuleResolutionKind, ScriptTarget } from "./enums.js";
import { tsgoPath } from "./session-path.js";
@@ -70,6 +75,54 @@ test("implementation discovery leaves declaration trees lazy without skipping th
expect(() => program.getSourceFileNames()).toThrow("disposed");
});
test("pre-emit diagnostics for kept files check only those, unless a global diagnostic appears", () => {
const { directory, entry } = fixture();
const other = join(directory, "other.ts");
const later = join(directory, "later.ts");
writeFileSync(
entry,
'import "./other.js";\nimport "./later.js";\nexport const n: number = "x";\n',
);
writeFileSync(other, "export const s: string = 1;\n");
writeFileSync(later, "export const b: boolean = 0;\n");
const { host } = makeHost(directory);
const program = host.createProgram([entry], options);
try {
const full = getPreEmitDiagnostics(program);
const keep = (name: string): boolean => name !== tsgoPath(other);
const checked = vi.spyOn(program.project.program, "checkedFileDiagnosticData");
const kept = getPreEmitDiagnostics(program, keep);
// The full answer without the left-out file's diagnostics, in the same order.
expect(full.some((diagnostic) => diagnostic.fileName === tsgoPath(other))).toBe(true);
expect(kept.map((diagnostic) => [diagnostic.fileName, diagnostic.code])).toEqual(
full
.filter((diagnostic) => diagnostic.fileName === undefined || keep(diagnostic.fileName))
.map((diagnostic) => [diagnostic.fileName, diagnostic.code]),
);
expect(kept).toHaveLength(2);
expect(checked).toHaveBeenCalledTimes(1);
expect(checked.mock.calls[0]![0]).not.toContain(tsgoPath(other));
// Keeping every file is the full check.
expect(getPreEmitDiagnostics(program, () => true)).toEqual(full);
expect(checked).toHaveBeenCalledTimes(1);
// A global diagnostic from the partial check, or a server without it: the full check.
for (const answer of [
{
files: [],
global: [{ fileName: undefined, pos: 0, end: 0, code: 2318, category: 1, text: "global" }],
},
null,
]) {
checked.mockReturnValueOnce(
answer as ReturnType<typeof program.project.program.checkedFileDiagnosticData>,
);
expect(getPreEmitDiagnostics(program, keep)).toEqual(full);
}
} finally {
program.dispose();
}
});
test("the factory receives one connection with the host's filesystem and timing", () => {
const { directory, entry } = fixture();
const factory = vi.fn((connection: Ts7ApiOptions) => new Ts7Api(connection));
@@ -46,9 +46,9 @@ import {
trackedRealpath,
} from "../input-tracker.js";
import { packageShimDeclarationText } from "../package-shims.js";
import { typePatchedText } from "../type-patches/layer.js";
import { typePatchedText, typePatchFileExists } from "../type-patches/layer.js";
import { ambientDtsPath, fallbackDtsPath, overridesDtsPath } from "../dts-paths.js";
import { reusedLibDiagnostics } from "./lib-diagnostics.js";
import { inFileOrder, reusedKeptLibDiagnostics, reusedLibDiagnostics } from "./lib-diagnostics.js";
import { ts7Diagnostic } from "./diagnostic.js";
import type { Ts7DiagnosticData } from "./session-schema.generated.js";
@@ -190,7 +190,7 @@ export class Ts7Host {
fileExists: (fileName) => {
if (virtualFiles.has(tsgoPath(fileName))) return true;
if (shadow !== null && shadow.hideFile(fileName)) return false;
return trackedFileExists(fileName);
return trackedFileExists(fileName) || typePatchFileExists(fileName);
},
directoryExists: (path) => trackedDirectoryExists(path),
realpath: (path) =>
@@ -423,17 +423,31 @@ export class Ts7Program {
/** The semantic and global diagnostics with the stored diagnostics of the
* declaration files scriptc supplies (lib-diagnostics.ts), or null. */
reusedLibDiagnostics(): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null {
return this.libDiagnostics(null);
}
/** reusedLibDiagnostics for the `kept` files only: null without a stored
* answer. */
reusedKeptLibDiagnostics(
kept: ReadonlySet<string>,
): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null {
return this.libDiagnostics(kept);
}
private libDiagnostics(
kept: ReadonlySet<string> | null,
): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null {
this.snapshot.ensureActive();
const program = this.project.program;
const declarations = new Set([ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]);
return reusedLibDiagnostics(
program,
this.getSourceFileNames(),
(name) =>
declarations.has(tsgoPath(name)) ||
program.getSourceFileMetadataByPath(name)?.isDefaultLibrary === true,
(name) => this.getSourceFile(name),
);
const names = this.getSourceFileNames();
const supplied = (name: string): boolean =>
declarations.has(tsgoPath(name)) ||
program.getSourceFileMetadataByPath(name)?.isDefaultLibrary === true;
const sourceFile = (name: string): SourceFile | undefined => this.getSourceFile(name);
return kept === null
? reusedLibDiagnostics(program, names, supplied, sourceFile)
: reusedKeptLibDiagnostics(program, names, supplied, sourceFile, kept);
}
isSourceFileDefaultLibrary(file: SourceFile): boolean {
@@ -464,10 +478,35 @@ export class Ts7Program {
/** 5.9.3's getPreEmitDiagnostics, composed from 7.0.2's split surface (the
* package dropped the aggregate). Order mirrors 5.9.3: config, options/
* program, global, syntactic, semantic. */
export function getPreEmitDiagnostics(program: Ts7Program): readonly Diagnostic[] {
* program, global, syntactic, semantic.
*
* With `keep`, a caller that discards every diagnostic of the other files
* asks for semantic diagnostics of the kept files only: the server checks
* only those (getDiagnosticsOfCheckedFiles), or only those not answered by
* stored diagnostics of the supplied declaration files, and their
* diagnostics come in the full check's order. A global diagnostic can come
* from checking any file, so when that partial check reports one, the full
* check answers. */
export function getPreEmitDiagnostics(
program: Ts7Program,
keep?: (fileName: string) => boolean,
): readonly Diagnostic[] {
const config = program.getConfigFileParsingDiagnostics();
const options = program.getProgramDiagnostics();
if (keep !== undefined) {
const kept = program.getSourceFileNames().filter((name) => keep(name));
if (kept.length < program.getSourceFileNames().length) {
const partial =
program.reusedKeptLibDiagnostics(new Set(kept)) ?? keptFileDiagnostics(program, kept);
if (partial !== null && partial.global.length === 0)
return [
...config,
...options,
...program.getSyntacticDiagnostics(),
...partial.semantic.map(ts7Diagnostic),
];
}
}
const reused = program.reusedLibDiagnostics();
if (reused !== null)
return [
@@ -486,6 +525,19 @@ export function getPreEmitDiagnostics(program: Ts7Program): readonly Diagnostic[
];
}
/** The semantic diagnostics of the kept files, in the full check's order,
* and the global diagnostics of checking them; null when the server cannot
* check only some files. */
function keptFileDiagnostics(
program: Ts7Program,
kept: readonly string[],
): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null {
const checked = program.project.program.checkedFileDiagnosticData(kept);
if (checked === null) return null;
const byFile = new Map(kept.map((name, index) => [name, checked.files[index] ?? []]));
return { semantic: inFileOrder(byFile), global: checked.global };
}
/** ts.findConfigFile: nearest configName walking up from searchPath. */
export function findConfigFile(
searchPath: string,
+106 -8
View File
@@ -10,6 +10,7 @@ import {
isDynTypedRefType,
BOOL,
bytesOf,
canBoxFuncIntoDyn,
canConvertToDyn,
canDynCheckTo,
CHILD_T,
@@ -717,6 +718,12 @@ export class UnionRegistry {
return id !== undefined && this.pendingRec.has(id);
}
/** True when `unionId` is a recursive placeholder whose arms are not
* known yet (its outer frame is still mapping, or failed). */
isPendingPlaceholder(unionId: string): boolean {
return this.pendingRec.has(unionId);
}
/** Completes a recursive placeholder with its canonical arm list and
* registers the structural key (first writer wins, like shapes). */
finalizeRecursive(t: ts.Type, arms: IrType[], discriminant?: IrUnionDiscriminant): string {
@@ -1255,6 +1262,10 @@ export interface TypeMapperCtx {
* (erasedGenericMemberType): the signature's own type parameters
* resolve to `unknown` instead of refusing the mapping. */
erasingSignature?: ts.Signature;
/** Set with erasingSignature: the signature's own type parameters, and
* whether the erasure consulted an enclosing binding. An erasure that
* resolved only its own parameters answers the same in every context. */
erasedMember?: { own: ReadonlySet<ts.Type>; outerConsulted: boolean };
/** True while an erased generic body lowers (its type parameters are
* bound to `unknown`). */
isErasing?: () => boolean;
@@ -1266,6 +1277,11 @@ export interface TypeMapperCtx {
* only where literal identity matters — indexed accesses (`T[K]`) inside
* generic bodies whose K is bound to a literal key. */
resolveTypeParamTs?: TypeParamTsResolver;
/** A generic alias reference inside a generic body (`Catalog<G, P>`)
* re-instantiated with the instance's bound type arguments: the concrete
* checker type its signature instantiates, or null. Consulted only when
* the symbolic form does not map. */
resolveBoundAliasTs?: TypeParamTsResolver;
/** GENERIC program classes (monomorphization by flow): the mapping of a
* concrete instantiation reference (`Box<number>`) — the Lowerer
* registers/reuses the instantiation (`Box%0`) and answers its object
@@ -1830,11 +1846,22 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
if (!topLevel || !ts.isUnreadableConstituentsPanic(e)) throw e;
mapped = null;
}
// A generic alias the body spells over its own type parameters (`x as
// Catalog<G, P>`) stays symbolic in the checker; when that form does not
// map, the instance's concrete instantiation of the same alias does.
if (mapped === null && ctx.resolveBoundAliasTs) {
const concrete = ctx.resolveBoundAliasTs(type);
if (concrete !== null && concrete !== type) {
mapped = mapType(concrete, ctx);
if (mapped !== null) contextResolutions++;
}
}
if (
mapped !== null &&
memoKey !== null &&
topLevel &&
contextResolutions === sensitivityAtEntry &&
ctx.erasingSignature === undefined &&
(ctx.canMemoizeType?.() ?? true)
) {
ctx.typeMemo?.set(memoKey, mapped);
@@ -1991,7 +2018,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
!ERASURE_KEPT_CONTAINERS.has(ts.symbolNameOf(type.getSymbol()) ?? "") &&
mentionsErasedTypeParam(type, ctx, 0)
) {
contextResolutions++;
if (!ctx.erasedMember || ctx.erasedMember.outerConsulted) contextResolutions++;
return DYN;
}
// Readonly<C> over a program class (Object.freeze's result type) is the
@@ -2001,7 +2028,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
if (resolveTypeParam && type.flags & ts.TypeFlags.TypeParameter) {
const bound = resolveTypeParam(type);
if (bound) {
contextResolutions++;
if (!ctx.erasedMember?.own.has(type)) contextResolutions++;
return bound;
}
}
@@ -3009,6 +3036,30 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// spawn consumes a presence-sensitive option bag, including optional
// stdio and env values. Keep it checked-dynamic for native normalization.
if (isStdlibInterface("SpawnOptions")) return DYN;
// util.inspect as a value (a custom inspect method's third parameter is
// `typeof inspect`): the runtime's inspect function, called through the
// checked-dynamic boundary.
if (
psym !== undefined &&
ts.symbolName(psym) === "inspect" &&
psym.declarations.some(
(d) =>
ts.isFunctionDeclaration(d) &&
ctx.isStdlibFile(ts.getSourceFileOfNode(d)) &&
isDeclaredInAmbientModule(d, "util"),
)
)
return DYN;
// util.inspect's options bag, which inspect and formatWithOptions read
// at run time (its sorted option can be a comparator function), and the
// options custom inspect functions receive.
if (
(isStdlibInterface("InspectOptions") || isStdlibInterface("InspectOptionsStylized")) &&
psym!.declarations.some(
(d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientModule(d, "util"),
)
)
return DYN;
// dns.LookupAllOptions (`all: true`, the overload whose callback takes
// an address list): a lookup option bag dns.lookup reads at run time.
// It rides checked-dynamic storage, so the lookup function type that
@@ -3072,11 +3123,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
if (!elem) return null;
return genResultRecord(elem, VOID, ctx.shapes, ctx.unions);
}
// node:net's SocketAddress and BlockList: native handles (scr_net.c)
// whose getters and methods dispatch through the handle.
// node:net's SocketAddress and BlockList (scr_net.c) and node:util's
// MIMEType and MIMEParams (scr_mime.c): native handles whose getters and
// methods dispatch through the handle.
if (
((psym !== undefined && ts.symbolName(psym) === "SocketAddress") ||
(psym !== undefined && ts.symbolName(psym) === "BlockList")) &&
(psym !== undefined && ts.symbolName(psym) === "BlockList") ||
(psym !== undefined && ts.symbolName(psym) === "MIMEType") ||
(psym !== undefined && ts.symbolName(psym) === "MIMEParams")) &&
psym.declarations.some(
(d) =>
(ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) &&
@@ -3085,6 +3139,18 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
) {
return DYN;
}
// The functions node:util's debounce and throttle return
// (scr_util_timing.c): native functions with their own members and
// getters.
if (
psym !== undefined &&
(ts.symbolName(psym) === "DebouncedFunction" || ts.symbolName(psym) === "ThrottledFunction") &&
psym.declarations.some(
(d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)),
)
) {
return DYN;
}
// WebCrypto: the crypto global and its subtle object are dynamic
// objects, CryptoKey values are native handles (scr_webcrypto.c), and
// the algorithm dictionaries the methods take and keys report are plain
@@ -5315,8 +5381,19 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null
if (pt) pt = jsOpenObjectType(decl, pt, ctx.shapes, ctx.unions);
// Belt and braces for non-strict type worlds: an optional param's
// ABI slot is always the undefined-armed union (strictNullChecks
// already spells it that way; arm it here if the world didn't).
if (optional && pt && pt.kind !== "void" && pt.kind !== "dyn") {
// already spells it that way; arm it here if the world didn't). A
// back-reference to a union still being mapped (`options?: { then?:
// (ctx: Sub) => void }` where Sub extends the interface declaring the
// method) is a recursive placeholder with no arms yet; when the checker
// spelled the parameter `X | undefined`, the placeholder receives that
// undefined arm when its frame finishes.
const pendingOptional =
pt?.kind === "union" &&
ctx.unions.isPendingPlaceholder(pt.unionId) &&
ts
.constituentTypes(checker.getTypeOfSymbol(p))
.some((part) => (part.flags & ts.TypeFlags.Undefined) !== 0);
if (optional && pt && pt.kind !== "void" && pt.kind !== "dyn" && !pendingOptional) {
const armed = withUndefinedArm(pt, ctx.unions);
if (!armed) return null;
pt = armed;
@@ -7335,10 +7412,21 @@ export function erasedGenericMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType
const sig = sigs[0]!;
const own = new Set<ts.Type>(sig.getTypeParameters());
const outer = ctx.resolveTypeParam;
// Resolving only the member's own parameters does not make the erasure
// context-sensitive, so a recursive record holding a generic method
// (`interface Sub extends Base { send<T>(m: Msg<T>): void }` reached
// from Base) still closes its knot. Consulting an enclosing binding does.
const erasedMember = { own, outerConsulted: false };
const erased = mapType(t, {
...ctx,
erasingSignature: sig,
resolveTypeParam: (param) => (own.has(param) ? DYN : (outer?.(param) ?? null)),
erasedMember,
resolveTypeParam: (param) => {
if (own.has(param)) return DYN;
if (outer === undefined) return null;
erasedMember.outerConsulted = true;
return outer(param);
},
});
return erased?.kind === "func" ? erased : undefined;
}
@@ -8367,6 +8455,16 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean {
// `string | URL | Request`, fetch's own input type under @types/node.
case "url":
return true;
// A function the checked-dynamic tree can box (each parameter checks
// in, the result converts out) is held by reference there, and
// `typeof x === "function"` narrows it back out: `Progress | AbortSignal`
// beside fetch's handles.
case "func":
return canBoxFuncIntoDyn(
arm,
(id) => ctx.shapes.get(id),
(id) => ctx.unions.get(id),
);
case "record":
case "array":
// `T | Promise<T>` with T bound to `unknown`: the checked-dynamic value
@@ -27,7 +27,7 @@ import { dirname, relative } from "node:path";
import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js";
import { typePatchDiag } from "../../diagnostics/diagnostic.js";
import { readProjectConfig, type TypePatchEntry } from "../../project-config.js";
import { trackedReadFile } from "../input-tracker.js";
import { trackedFileExists, trackedReadFile } from "../input-tracker.js";
import type { SourceFile } from "../ts7/syntax.js";
import { applyPatchEdits, patchFile, type PatchMatch, type PatchRequest } from "./locate.js";
import { parseSelector, parseTypeText, type Selector, type TypePatchOp } from "./selector.js";
@@ -49,6 +49,11 @@ interface CopyInfo {
root: string;
/** Requests whose entry's range the version satisfies. */
requests: Request[];
/** Replaced declaration files: package-relative path → local file. */
files: Map<string, string>;
/** Declaration entries a `types` patch supplies: package-relative path
* → local file (existing entries are replaced, missing ones appear). */
entries: Map<string, string>;
}
interface MatchRecord extends PatchMatch {
@@ -63,9 +68,11 @@ interface State {
projectDir: string;
manifestText: string;
parse: TypePatchParse;
byPackage: Map<string, Request[]>;
byPackage: Map<string, { entries: TypePatchEntry[]; requests: Request[] }>;
copies: Map<string, CopyInfo | null>;
files: Map<string, string | null>;
/** `files` targets that do not exist in a copy. */
missingFiles: { entry: TypePatchEntry; root: string; rel: string }[];
/** Package copies whose version no patch range covers. */
unmatchedCopies: Map<string, { pkg: string; version: string | null; entries: TypePatchEntry[] }>;
/** Requests applicable to a copy this load read files of. */
@@ -86,22 +93,23 @@ export function configureTypePatches(entryPath: string, parse: TypePatchParse):
state = null;
return;
}
const byPackage = new Map<string, Request[]>();
const byPackage = new Map<string, { entries: TypePatchEntry[]; requests: Request[] }>();
let id = 0;
for (const entry of config.typePatches) {
const pkg = byPackage.get(entry.pkg) ?? { entries: [], requests: [] };
byPackage.set(entry.pkg, pkg);
pkg.entries.push(entry);
for (const decl of entry.declarations) {
const selector = parseSelector(decl.selector);
const op = parseTypeText(decl.type);
if ("error" in selector || "error" in op) continue; // reported as SC8004
const list = byPackage.get(entry.pkg) ?? [];
list.push({
pkg.requests.push({
id: id++,
selector: selector as Selector,
op: op as TypePatchOp,
entry,
offset: decl.offset,
});
byPackage.set(entry.pkg, list);
}
}
state = {
@@ -112,6 +120,7 @@ export function configureTypePatches(entryPath: string, parse: TypePatchParse):
byPackage,
copies: new Map(),
files: new Map(),
missingFiles: [],
unmatchedCopies: new Map(),
touched: new Set(),
matches: [],
@@ -164,7 +173,7 @@ function twinRuntimeVersion(s: State, nodeModules: string, pkg: string): string
/** The installed package copy a file belongs to, with the requests that
* apply to it; null for files outside node_modules or of unpatched
* packages. */
function copyOf(s: State, path: string): { copy: CopyInfo; rel: string } | null {
function copyOf(s: State, path: string, touch = true): { copy: CopyInfo; rel: string } | null {
const marker = "/node_modules/";
const at = path.lastIndexOf(marker);
if (at < 0) return null;
@@ -178,33 +187,122 @@ function copyOf(s: State, path: string): { copy: CopyInfo; rel: string } | null
let copy = s.copies.get(root);
if (copy === undefined) {
copy = null;
const pkg = name.startsWith("@types/") ? unmangleTypesName(name) : name;
const requests = s.byPackage.get(pkg);
if (requests !== undefined) {
const twin = name.startsWith("@types/");
const pkg = twin ? unmangleTypesName(name) : name;
const patches = s.byPackage.get(pkg);
if (patches !== undefined) {
// A twin is matched by the runtime package's version, never its own.
const version = name.startsWith("@types/")
const version = twin
? twinRuntimeVersion(s, path.slice(0, at + marker.length), pkg)
: versionAt(root);
const applicable = requests.filter(
(r) => version !== null && satisfies(version, r.entry.range),
const applicable = patches.entries.filter(
(e) => version !== null && satisfies(version, e.range),
);
// Later entries win on the same selector.
const bySelector = new Map<string, Request>();
for (const r of applicable) bySelector.set(r.selector.text, r);
const chosen = [...bySelector.values()];
if (chosen.length === 0) {
const entries = [...new Set(requests.map((r) => r.entry))];
s.unmatchedCopies.set(root, { pkg, version, entries });
if (applicable.length === 0) {
s.unmatchedCopies.set(root, { pkg, version, entries: patches.entries });
} else {
// Later entries win on the same selector, file or entry.
const bySelector = new Map<string, Request>();
for (const r of patches.requests)
if (applicable.includes(r.entry)) bySelector.set(r.selector.text, r);
const files = new Map<string, string>();
let types: string | null = null;
const prefix = `${name}/`;
for (const e of applicable) {
for (const [target, local] of e.files) {
// `@types/<name>/<path>` names the twin's file; plain paths the
// package's own.
const forTwin = target.startsWith("@types/");
if (forTwin !== twin) continue;
const rel = forTwin
? target.startsWith(prefix)
? target.slice(prefix.length)
: null
: target;
if (rel === null) continue;
if (!trackedFileExists(`${root}/${rel}`)) {
s.missingFiles.push({ entry: e, root, rel });
continue;
}
files.set(rel, local);
}
if (e.types !== null && !twin) types = e.types;
}
copy = {
pkg,
version,
root,
requests: [...bySelector.values()],
files,
entries: types === null ? new Map() : declarationEntries(root, types),
};
}
copy = chosen.length === 0 ? null : { pkg, version, root, requests: chosen };
}
s.copies.set(root, copy);
}
if (copy === null) return null;
for (const r of copy.requests) s.touched.add(r);
if (touch) for (const r of copy.requests) s.touched.add(r);
return { copy, rel };
}
/** The declaration entry paths of a package a `types` patch stands for:
* its `types`/`typings` file and every declaration TypeScript probes
* beside a JavaScript entry (`main`, the `exports` targets of "."). */
function declarationEntries(root: string, local: string): Map<string, string> {
const out = new Map<string, string>();
let pkg: Record<string, unknown> = {};
try {
pkg = JSON.parse(trackedReadFile(`${root}/package.json`) ?? "{}") as Record<string, unknown>;
} catch {
pkg = {};
}
const add = (target: unknown): void => {
if (typeof target !== "string") return;
const rel = target.replace(/^\.\//, "");
if (/\.d\.[cm]?ts$/.test(rel)) out.set(rel, local);
else if (/\.[cm]?js$/.test(rel)) out.set(rel.replace(/\.([cm]?)js$/, ".d.$1ts"), local);
// TypeScript probes x.tsx and then x.d.ts for x.jsx.
else if (rel.endsWith(".jsx")) out.set(`${rel.slice(0, -".jsx".length)}.d.ts`, local);
else if (!/\.\w+$/.test(rel)) out.set(`${rel}.d.ts`, local);
};
add(pkg["types"]);
add(pkg["typings"]);
add(typeof pkg["main"] === "string" ? pkg["main"] : "index.js");
const exportsField = pkg["exports"];
const dot =
exportsField !== null && typeof exportsField === "object" && "." in exportsField
? (exportsField as Record<string, unknown>)["."]
: exportsField;
const visit = (v: unknown): void => {
if (typeof v === "string") add(v);
else if (v !== null && typeof v === "object") for (const x of Object.values(v)) visit(x);
};
// Subpath exports ("./x") are not the entry.
if (dot !== null && typeof dot === "object" && Object.keys(dot).some((k) => k.startsWith(".")))
return out;
visit(dot);
return out;
}
/** True for a declaration entry a `types` patch supplies that does not
* exist on disk (a package without declarations gains one). */
export function typePatchFileExists(fileName: string): boolean {
const s = state;
if (s === null) return false;
const path = fileName.split("\\").join("/");
if (!/\.d\.[cm]?ts$/.test(path)) return false;
const located = copyOf(s, path, false);
return located !== null && located.copy.entries.has(located.rel);
}
/** True when a `types` patch gives the package at `pkgDir` declarations. */
export function typePatchSuppliesTypes(pkgDir: string): boolean {
const s = state;
if (s === null) return false;
const located = copyOf(s, `${pkgDir.split("\\").join("/")}/package.json`, false);
return located !== null && located.copy.entries.size > 0;
}
/** Names a request's root can appear under in a file's text. */
function rootWords(sel: Selector): string[] {
if (sel.root === "export=") return ["exports", "export"];
@@ -224,7 +322,12 @@ export function typePatchedText(fileName: string): string | undefined {
let result: string | null = null;
const located = copyOf(s, path);
if (located !== null) {
const text = trackedReadFile(path);
// A replaced file or a supplied entry reads as the local file; the
// declaration patches then apply to it like to the package's own.
const local = located.copy.files.get(located.rel) ?? located.copy.entries.get(located.rel);
const replaced = local === undefined ? null : trackedReadFile(local);
const text = replaced ?? trackedReadFile(path);
if (replaced !== null) result = replaced;
const requests = located.copy.requests.filter(
(r) =>
(r.selector.file === null || r.selector.file === located.rel) &&
@@ -333,6 +436,16 @@ export function typePatchDiagnostics(): {
);
}
}
for (const missing of s.missingFiles) {
out.push(
typePatchDiag(
"SC8031",
`type patch '${missing.entry.key}': ${missing.rel} is not a file of ${missing.entry.pkg} at ${relativeToManifest(s, missing.root)}`,
"name a declaration file the installed package ships; the package may have moved it",
loc(s, missing.entry.offset),
),
);
}
for (const r of s.touched) {
const mine = s.matches.filter((m) => m.id === r.id);
// A selector that matched somewhere (the .d.ts) is not a problem where
+5
View File
@@ -655,6 +655,8 @@ async function compileExecutableNative(
...(features.declAdopt ? { declAdopt: true } : {}),
...(features.dynForAwait ? { dynForAwait: true } : {}),
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
...(features.mime ? { mime: true } : {}),
...(features.utilTiming ? { utilTiming: true } : {}),
...(features.dynBuffer ? { dynBuffer: true } : {}),
...(features.builtinValues ? { builtinValues: true } : {}),
events: features.events,
@@ -897,6 +899,7 @@ async function prepareExecutableInput(
// the pending flag, since their link may not add an unwinder.
unwindAbi: buildPlatform === "linux" && outputKind !== "obj",
objectAudit: opts.sanitize === true,
sanitizeAttributes: opts.sanitize === true,
lowRegion: lowRegionTarget(buildPlatform, opts.sanitize === true),
...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}),
...inlineRcOption(opts.optimization, opts.sanitize === true),
@@ -969,6 +972,7 @@ async function prepareExecutableInput(
runtimeAbiMarker: useRuntimePack,
unwindAbi: buildPlatform === "linux",
objectAudit: opts.sanitize === true,
sanitizeAttributes: opts.sanitize === true,
lowRegion: lowRegionTarget(buildPlatform, opts.sanitize === true),
...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}),
...inlineRcOption(opts.optimization, opts.sanitize === true),
@@ -2412,6 +2416,7 @@ async function compileLibraryTracked(
pointerBits: buildPlatform === "wasi" ? 32 : 64,
wasi: buildPlatform === "wasi",
objectAudit: opts.sanitize === true,
sanitizeAttributes: opts.sanitize === true,
...inlineRcOption(profile.optimization, opts.sanitize === true),
});
timing("llvm-emit", {
@@ -187,6 +187,16 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"num.toStringRadix",
"num.toStringRadixF64",
"insp.jsonDyn",
// The runtime formatter calls toString, valueOf and toJSON, and refuses
// renderings it has no lowering for.
"insp.format",
"insp.formatList",
"insp.formatWithOptions",
"insp.inspect",
// The options object reads the caller's options (getters included), and
// the checked-dynamic walk calls custom inspect functions.
"insp.customOptions",
"insp.customResult",
// A checked-dynamic value the inspector does not render (a promise, a
// URL, a RegExp) throws the 'not supported yet' Error.
"insp.dyn",
@@ -601,6 +611,13 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"util.inherits",
"util.inheritsPrototype",
"util.deprecate",
"util.extend",
"util.signalExitCode",
"util.diff",
"util.debounce",
"util.throttle",
"mime.typeNew",
"mime.typeParse",
"util.callbackify",
"util.promisifyDyn",
"util.debuglog",
+54 -1
View File
@@ -1165,6 +1165,17 @@ function buildLibFnSignatures() {
"util.inherits": { argTypes: [DYN, DYN], result: VOID },
"util.inheritsPrototype": { argTypes: [DYN, DYN, DYN], result: VOID },
"util.deprecate": { argTypes: [DYN, DYN, DYN, DYN], result: DYN },
"util.isArrayDeprecated": { argTypes: [DYN], result: BOOL },
"util.extend": { argTypes: [DYN, DYN], result: DYN },
"util.signalExitCode": { argTypes: [DYN], result: F64 },
"util.aborted": { argTypes: [DYN, DYN], result: DYN },
"util.diff": { argTypes: [DYN, DYN], result: DYN },
"util.debounce": { argTypes: [DYN, DYN, DYN], result: DYN },
"util.throttle": { argTypes: [DYN, DYN, DYN, DYN], result: DYN },
"mime.typeNew": { argTypes: [DYN, DYN], result: DYN },
"mime.typeParse": { argTypes: [DYN], result: DYN },
"mime.paramsNew": { argTypes: [], result: DYN },
"mime.is": { argTypes: [DYN, F64], result: BOOL },
"util.callbackify": { argTypes: [DYN], result: DYN },
/** util.promisify over an untyped function value: a native dyn function
* whose calls answer a dyn promise the appended callback settles. */
@@ -4609,12 +4620,54 @@ function buildLibFnSignatures() {
"insp.errorCause": { argTypes: [null, F64, F64], result: STRING },
"insp.dyn": { argTypes: [DYN, F64, F64], result: STRING },
"insp.dynS": { argTypes: [DYN, F64], result: STRING },
/** util.format over a dyn array of its arguments, and
* util.formatWithOptions(options, pack): the runtime formatter for format
* strings known at run time and spread arguments. */
"insp.format": { argTypes: [DYN], result: STRING },
/** insp.format over arguments that are all strings, numbers, booleans,
* null or undefined, without the checked-dynamic walk. */
"insp.formatPrimitives": { argTypes: [arrayOf(DYN)], result: STRING },
"insp.formatList": { argTypes: [arrayOf(DYN)], result: STRING },
"insp.formatWithOptions": { argTypes: [DYN, DYN], result: STRING },
/** util.inspect(value, options, depth, colors) with the call's argument
* count: the runtime reads the options and renders the checked-dynamic
* view. */
"insp.inspect": { argTypes: [DYN, DYN, DYN, DYN, F64], result: STRING },
/** A [util.inspect.custom] method's call: whether ctx.customInspect is
* set, its depth argument (the remaining depth at recursion r of depth
* d), its options object and util.inspect, then its result rendered: a
* string re-indented at the current level, or a checked-dynamic value
* (customIsSelf tests for the instance itself). customInspect sets the
* flag for the renders that follow and answers the previous value, and
* depthKind does the same for how an unbounded depth was given (0
* Infinity, 1 null, 2 undefined). */
"insp.customEnabled": { argTypes: [], result: BOOL },
/** Registers a class's compiled custom inspect function under its
* instances' typed-ref type key, for the checked-dynamic walk. */
"insp.registerCustom": { argTypes: [STRING, DYN], result: VOID },
"insp.customInspect": { argTypes: [BOOL], result: BOOL },
"insp.depthKind": { argTypes: [F64], result: F64 },
"insp.customDepth": { argTypes: [F64, F64], result: DYN },
"insp.customOptions": { argTypes: [F64], result: DYN },
"insp.inspectFunction": { argTypes: [], result: DYN },
"insp.customText": { argTypes: [STRING], result: STRING },
"insp.customIsSelf": { argTypes: [DYN, DYN], result: BOOL },
"insp.customResult": { argTypes: [DYN, F64, F64], result: STRING },
/** Around the method's call: the inspect and format calls it makes run
* in a fresh inspect context with the default options. begin answers the
* token end takes; end also discards what a call that threw left. */
"insp.customCallBegin": { argTypes: [], result: F64 },
"insp.customCallEnd": { argTypes: [F64], result: VOID },
/** Whether a runtime format string has a '%' before its last character. */
"insp.formatSpecifies": { argTypes: [STRING], result: BOOL },
"insp.formatSpecifiesDyn": { argTypes: [DYN], result: BOOL },
"insp.dynFormatS": { argTypes: [DYN, F64], result: STRING },
/** util.format's %o over a checked-dynamic value: inspect with
* showHidden. Kinds whose hidden entries have no rendering throw the
* 'not supported yet' Error. */
"insp.dynHidden": { argTypes: [DYN, F64], result: STRING },
"insp.begin": { argTypes: [F64], result: VOID },
/** util.inspect's breakLength, compact (0 for false) and sorted for the
/** util.inspect's breakLength, compact (-1 for false) and sorted for the
* renders that follow; the call site restores 80, 3 and false afterwards. */
"insp.layout": { argTypes: [F64, F64, BOOL], result: VOID },
"insp.entry": { argTypes: [STRING, BOOL], result: VOID },
@@ -33,6 +33,22 @@ const REBINDING_KINDS: readonly IrNodeKind[] = [
"closure",
];
/** The rebindings one function's body makes: locals it writes after
* declaring them, and the closures it creates over captured bindings. */
interface FunctionRebindings {
ids: Set<string>;
sites: { owner: string; fnName: string; captures: string[] }[];
}
/** Records `node`'s rebinding into `into` (owner `fnName`). */
function noteRebinding(fnName: string, node: IrExpr | IrStmt, into: FunctionRebindings): void {
if (node.kind === "assign" || node.kind === "forOf") into.ids.add(node.localId);
else if (node.kind === "tryCatch" && node.catchLocalId !== null) into.ids.add(node.catchLocalId);
else if (node.kind === "assignExpr" || node.kind === "incDec") into.ids.add(node.localId);
else if (node.kind === "closure" && node.captures.length)
into.sites.push({ owner: fnName, fnName: node.fnName, captures: node.captures });
}
function rebindings(
mod: { readonly functions: readonly IrFunction[] },
nodes?: IrNodeIndex,
@@ -41,21 +57,31 @@ function rebindings(
/** Bindings of each function that a closure it creates writes. */
closureWritten: Map<string, Set<string>>;
} {
const scan = nodes?.scan(REBINDING_KINDS);
const each = mod.functions.map((fn) => {
const own: FunctionRebindings = { ids: new Set(), sites: [] };
eachNodeOfKinds(fn, scan, (node) => noteRebinding(fn.name, node, own));
return own;
});
return closeRebindings(mod, each);
}
/** The module's rebindings from each function's own (`each`, in module
* order):
* a binding a closure writes through its environment is written by the
* function that created the closure, transitively. */
function closeRebindings(
mod: { readonly functions: readonly IrFunction[] },
each: readonly FunctionRebindings[],
): { written: Map<string, Set<string>>; closureWritten: Map<string, Set<string>> } {
const written = new Map<string, Set<string>>();
const closureWritten = new Map<string, Set<string>>();
const sites: { owner: string; fnName: string; captures: string[] }[] = [];
const scan = nodes?.scan(REBINDING_KINDS);
for (const fn of mod.functions) {
const ids = new Set<string>();
eachNodeOfKinds(fn, scan, (node) => {
if (node.kind === "assign" || node.kind === "forOf") ids.add(node.localId);
else if (node.kind === "tryCatch" && node.catchLocalId !== null) ids.add(node.catchLocalId);
else if (node.kind === "assignExpr" || node.kind === "incDec") ids.add(node.localId);
else if (node.kind === "closure" && node.captures.length)
sites.push({ owner: fn.name, fnName: node.fnName, captures: node.captures });
});
written.set(fn.name, ids);
}
mod.functions.forEach((fn, index) => {
const own = each[index]!;
written.set(fn.name, own.ids);
for (const site of own.sites) sites.push(site);
});
const functions = new Map(mod.functions.map((fn) => [fn.name, fn]));
for (let changed = true; changed;) {
changed = false;
@@ -130,9 +156,10 @@ export function specializeCallbackCalls(
symbolName: (name: string) => string = (name) => name,
): IrModule {
const functions = new Map(mod.functions.map((fn) => [fn.name, fn]));
const { written, closureWritten } = rebindings(mod);
const copies = new Map<string, IrFunction>();
const candidates = new Map<string, Candidate>();
// Helpers that may take callbacks: their callback parameters are final
// once the walk below finds which of them the body rebinds.
const helpers = new Map<string, IrFunction>();
for (const fn of mod.functions) {
if (
!(fn.ownsPrototype || fn.name.startsWith("%arr.")) ||
@@ -142,19 +169,13 @@ export function specializeCallbackCalls(
fn.captures !== undefined ||
fn.classCaptures?.length ||
fn.fastRead !== undefined ||
fn.speculativeDispatch
fn.speculativeDispatch ||
!fn.params.some((param) => param.type.kind === "func")
)
continue;
const nodes = countNodes(fn);
if (nodes > MAX_FUNCTION_NODES) continue;
const rebound = written.get(fn.name)!;
const slots: number[] = [];
fn.params.forEach((param, index) => {
if (param.type.kind === "func" && !rebound.has(param.localId)) slots.push(index);
});
if (slots.length) candidates.set(fn.name, { fn, slots, nodes });
helpers.set(fn.name, fn);
}
if (!candidates.size) return mod;
if (!helpers.size) return mod;
// Function values, exports, entry points and methods: an original
// reachable this way stays even when every direct call moves to a copy.
@@ -162,16 +183,17 @@ export function specializeCallbackCalls(
for (const cls of mod.classes ?? [])
for (const method of ["constructor", ...(cls.methods ?? [])])
referenced.add(`%${cls.name}.${method}`);
// Single definitions of function-typed locals and globals.
const definitions = new Map<string, Map<string, IrExpr>>();
const globalIds = new Set(mod.globals?.map((global) => global.id));
const globalWrites = new Map<string, IrExpr[]>();
// The sum of countNodes over every function, counted by the walk below.
let totalNodes = 0;
for (const fn of mod.functions) {
// One walk of every body records what the steps below read: its node
// count (countNodes), its rebindings, the values written into each of its
// bindings, and its direct calls of helpers, in walk order.
const each = mod.functions.map((fn) => {
let nodes = 0;
const own: FunctionRebindings = { ids: new Set(), sites: [] };
const values = new Map<string, IrExpr[]>();
const counts = new Map<string, number>();
const calls: (IrExpr & { kind: "call" })[] = [];
const record = (id: string, value: IrExpr | null, weight: number): void => {
counts.set(id, (counts.get(id) ?? 0) + weight);
if (value) {
@@ -183,6 +205,7 @@ export function specializeCallbackCalls(
everyStmtList(fn.body, {
stmt: (stmt) => {
nodes++;
noteRebinding(fn.name, stmt, own);
if (stmt.kind === "varDecl") record(stmt.localId, stmt.init, stmt.init ? 1 : 0);
if (stmt.kind === "assign") {
if (globalIds.has(stmt.localId)) {
@@ -199,15 +222,42 @@ export function specializeCallbackCalls(
},
expr: (expr) => {
nodes++;
noteRebinding(fn.name, expr, own);
if (expr.kind === "assignExpr" || expr.kind === "incDec") {
record(expr.localId, null, 2);
if (globalIds.has(expr.localId)) globalWrites.set(expr.localId, []);
}
if (expr.kind === "call" && helpers.has(expr.callee)) calls.push(expr);
const name = exprFunctionReference(expr);
if (name !== undefined) referenced.add(name);
return true;
},
});
return { fn, nodes: Math.min(nodes, MAX_FUNCTION_NODES + 1), own, values, counts, calls };
});
const { written, closureWritten } = closeRebindings(
mod,
each.map((entry) => entry.own),
);
const nodesOf = new Map(each.map((entry) => [entry.fn.name, entry.nodes]));
const candidates = new Map<string, Candidate>();
for (const fn of helpers.values()) {
const nodes = nodesOf.get(fn.name)!;
if (nodes > MAX_FUNCTION_NODES) continue;
const rebound = written.get(fn.name)!;
const slots: number[] = [];
fn.params.forEach((param, index) => {
if (param.type.kind === "func" && !rebound.has(param.localId)) slots.push(index);
});
if (slots.length) candidates.set(fn.name, { fn, slots, nodes });
}
if (!candidates.size) return mod;
// Single definitions of function-typed locals and globals.
const definitions = new Map<string, Map<string, IrExpr>>();
// The sum of countNodes over every function.
let totalNodes = 0;
for (const { fn, nodes, values, counts } of each) {
const single = new Map<string, IrExpr>();
const locals = new Set(fn.locals.map((local) => local.id));
const shared = closureWritten.get(fn.name);
@@ -221,7 +271,7 @@ export function specializeCallbackCalls(
)
single.set(id, list[0]!);
definitions.set(fn.name, single);
totalNodes += Math.min(nodes, MAX_FUNCTION_NODES + 1);
totalNodes += nodes;
}
const globalDefinitions = new Map<string, IrExpr>();
for (const global of mod.globals ?? [])
@@ -288,23 +338,18 @@ export function specializeCallbackCalls(
// unknown callbacks or function-value entries.
const keysOf = new Map<string, Set<string>>();
const unknown = new Set<string>();
for (const fn of mod.functions)
everyStmtList(fn.body, {
stmt: () => true,
expr: (expr) => {
if (expr.kind !== "call") return true;
const candidate = candidates.get(expr.callee);
if (!candidate) return true;
const key = callKey(fn, new Map(), expr, candidate);
if (key === "") unknown.add(expr.callee);
else if (key !== null) {
const keys = keysOf.get(expr.callee) ?? new Set<string>();
keys.add(key);
keysOf.set(expr.callee, keys);
}
return true;
},
});
for (const { fn, calls } of each)
for (const call of calls) {
const candidate = candidates.get(call.callee);
if (!candidate) continue;
const key = callKey(fn, new Map(), call, candidate);
if (key === "") unknown.add(call.callee);
else if (key !== null) {
const keys = keysOf.get(call.callee) ?? new Set<string>();
keys.add(key);
keysOf.set(call.callee, keys);
}
}
const specialized = new Set<string>();
for (const [name, keys] of keysOf)
if (keys.size > 1 || unknown.has(name) || referenced.has(name)) specialized.add(name);
@@ -419,12 +464,9 @@ export function specializeCallbackCalls(
return owner.body.map(stmt);
}
const functionsOut = mod.functions.map((fn) => {
const functionsOut = each.map(({ fn, calls }) => {
// Most bodies call no specialized original; keep their nodes.
const untouched = everyStmtList(fn.body, {
stmt: () => true,
expr: (expr) => expr.kind !== "call" || !specialized.has(expr.callee),
});
const untouched = calls.every((call) => !specialized.has(call.callee));
return untouched ? fn : { ...fn, body: rewriteBody(fn, new Map(), new Set(), false) };
});
for (let i = 0; i < pending.length; i++) {
@@ -437,19 +479,29 @@ export function specializeCallbackCalls(
// Drop originals no remaining code reaches: every direct call moved to a
// copy and nothing names them as a value.
const callers = new Map<string, Set<string>>();
for (const fn of functionsOut)
const call = (callee: string, caller: string): void => {
if (!specialized.has(callee)) return;
const set = callers.get(callee) ?? new Set<string>();
set.add(caller);
callers.set(callee, set);
};
functionsOut.forEach((fn, index) => {
// A body the rewrite kept has the calls the walk recorded, and its
// closures are already in `referenced`.
const kept = each[index];
if (kept !== undefined && kept.fn === fn) {
for (const site of kept.calls) call(site.callee, fn.name);
return;
}
everyStmtList(fn.body, {
stmt: () => true,
expr: (expr) => {
if (expr.kind === "call" && specialized.has(expr.callee)) {
const set = callers.get(expr.callee) ?? new Set<string>();
set.add(fn.name);
callers.set(expr.callee, set);
}
if (expr.kind === "call") call(expr.callee, fn.name);
if (expr.kind === "closure" && specialized.has(expr.fnName)) referenced.add(expr.fnName);
return true;
},
});
});
const live = new Set<string>();
const work: string[] = [];
for (const fn of functionsOut)
+28 -4
View File
@@ -86,7 +86,12 @@ export function copyStableCaptures(mod: IrModule): IrModule {
// A closure body whose every use became a direct call is only reachable
// through its copy now.
const referenced = new Set(moduleFunctionReferences(mod));
for (const fn of functions)
functions.forEach((fn, index) => {
// A body the rewrite kept names what the flow recorded of it.
if (fn === mod.functions[index]) {
for (const name of flow.references(fn)) referenced.add(name);
return;
}
everyStmtList(fn.body, {
stmt: () => true,
expr: (e) => {
@@ -95,6 +100,7 @@ export function copyStableCaptures(mod: IrModule): IrModule {
return true;
},
});
});
const clones = flow.directClones();
return {
...mod,
@@ -251,6 +257,9 @@ interface FunctionFacts {
captureIndex: Map<string, number>;
classCaptures: Set<string>;
params: Set<string>;
/** Functions the body names: direct call callees and function values
* (exprFunctionReference). */
references: Set<string>;
}
interface CreationSite {
@@ -273,17 +282,26 @@ export class CaptureFlow {
/** Environment-free bodies called directly, and their copies' names. */
private readonly clones = new Map<string, string>();
constructor(functions: readonly IrFunction[]) {
/** With `scan` false, each body's facts are collected when a question
* first needs them, and only unchanged() may be asked: the other
* questions read creation sites, which come from scanning every body. */
constructor(functions: readonly IrFunction[], scan = true) {
for (const fn of functions) this.functions.set(fn.name, fn);
this.receivers = new AmbientReceiverReaders(this.functions);
// Creation sites are collected while scanning each body.
for (const fn of functions) this.factsOf(fn);
if (scan) for (const fn of functions) this.factsOf(fn);
}
directClones(): ReadonlyMap<string, string> {
return this.clones;
}
/** The functions the body of `fn`, one of the functions this flow was
* made from, names (FunctionFacts.references). */
references(fn: IrFunction): ReadonlySet<string> {
return this.factsOf(fn).references;
}
/** A lifted body that captures nothing and can be entered like a declared
* function: synchronous, never naming its own closure, and unable to
* observe the ambient receiver a closure call would bracket. */
@@ -459,6 +477,7 @@ export class CaptureFlow {
captureIndex: new Map(),
classCaptures: new Set((fn.classCaptures ?? []).map((c) => c.localId)),
params: new Set(fn.params.map((p) => p.localId)),
references: new Set(),
};
(fn.captures ?? []).forEach((c, index) => {
if (!facts.captureIndex.has(c.localId)) facts.captureIndex.set(c.localId, index);
@@ -511,6 +530,8 @@ export class CaptureFlow {
facts.writes.set(id, (facts.writes.get(id) ?? 0) + 1);
};
const expr = (e: IrExpr): boolean => {
const referenced = e.kind === "call" ? e.callee : exprFunctionReference(e);
if (referenced !== undefined) facts.references.add(referenced);
switch (e.kind) {
case "varRef":
facts.reads.set(e.localId, (facts.reads.get(e.localId) ?? 0) + 1);
@@ -707,8 +728,11 @@ export class CaptureFlow {
const splits = new Map<string, string>();
const entry: IrStmt[] = [];
const locals: IrLocal[] = [];
const used = new Set(fn.locals.map((l) => l.id));
// The local ids a copy must avoid, collected for the first copy: most
// functions make none.
let used: Set<string> | null = null;
const fresh = (local: IrLocal): IrLocal => {
used ??= new Set(fn.locals.map((l) => l.id));
let k = 0;
while (used.has(`${local.name}.${k}`)) k++;
const id = `${local.name}.${k}`;
+8 -2
View File
@@ -300,6 +300,8 @@ function rewriteFunction(fn: IrFunction): IrFunction {
// or as copies of another such local (a switch's discriminant temp).
const reads = new Map<string, OptionalCharRead>();
const copyOf = new Map<string, string>();
// Whether a candidate string is declared from a charAt.
let charAtDeclared = false;
const collect = (value: unknown): void => {
if (Array.isArray(value)) for (const item of value) collect(item);
else if (value !== null && typeof value === "object") {
@@ -307,6 +309,7 @@ function rewriteFunction(fn: IrFunction): IrFunction {
if (node.kind === "varDecl" && candidates.has(node.localId as string) && node.init) {
const id = node.localId as string;
const init = node.init as IrExpr;
if (locals.get(id)?.type.kind === "string" && isCharAt(init)) charAtDeclared = true;
const read = optionalCharRead(init);
if (read) reads.set(id, read);
else if (init.kind === "varRef" && reads.has(init.localId)) {
@@ -317,7 +320,8 @@ function rewriteFunction(fn: IrFunction): IrFunction {
for (const key in node) if (key !== "loc") collect(node[key]);
}
};
collect(fn.body);
// Both walks only record candidates: a function without one holds none.
if (candidates.size > 0) collect(fn.body);
const localTags = (operand: IrExpr): OptionalCharRead | null =>
operand.kind === "varRef" ? (reads.get(operand.localId) ?? null) : null;
@@ -344,7 +348,9 @@ function rewriteFunction(fn: IrFunction): IrFunction {
for (const key in node) if (key !== "loc") count(node[key]);
}
};
count(fn.body);
// Only candidates declared from a charAt or an optional read can be held
// (understoodMention), and count() only decides which are.
if (reads.size > 0 || charAtDeclared) count(fn.body);
// A copy is held only with its source, and the source's mention in the
// copy's initializer is understood only while the copy is held.
const held = new Set<string>();
@@ -128,15 +128,24 @@ export function inlineExpressionFunctions(mod: IrModule): IrModule {
const functions = mod.functions.map((fn) => {
const locals = new Map(fn.locals.map((local) => [local.id, local]));
const inLoops = callsInLoops(fn.body);
if (inLoops.size === 0) return fn;
// Each call the rewrite inlines: of an expression function, in a loop,
// with arguments the copy may repeat.
const inlinable = (e: IrExpr & { kind: "call" }): boolean => {
if (e.callee === fn.name) return false;
const target = expressions.get(e.callee);
return (
target !== undefined &&
target.fn.params.length === e.args.length &&
e.args.every((arg) => duplicableArgument(arg, locals))
);
};
if (![...inLoops].some((e) => e.kind === "call" && inlinable(e))) return fn;
let inlined = false;
const body = transformStmtList(fn.body, {
stmt: (s) => s,
expr: (e) => {
if (e.kind !== "call" || e.callee === fn.name || !inLoops.has(e)) return e;
const target = expressions.get(e.callee);
if (target === undefined || target.fn.params.length !== e.args.length) return e;
if (!e.args.every((arg) => duplicableArgument(arg, locals))) return e;
if (e.kind !== "call" || !inLoops.has(e) || !inlinable(e)) return e;
const target = expressions.get(e.callee)!;
const args = new Map(target.fn.params.map((param, i) => [param.localId, e.args[i]!]));
inlined = true;
return copy(target.value, (ref) => {
+13
View File
@@ -575,6 +575,19 @@ const DYN_READ_ONLY = new Set<string>([
"buffer.poolFromDynRange",
"insp.dyn",
"insp.dynS",
// util.format and util.inspect over checked values: the inspect walk and
// the format conversions read them; insp.registerCustom keeps a class's
// custom inspect thunk in the runtime's registry.
"insp.customIsSelf",
"insp.customResult",
"insp.format",
"insp.formatList",
"insp.formatPrimitives",
"insp.formatSpecifiesDyn",
"insp.formatWithOptions",
"insp.inspect",
"insp.registerCustom",
"insp.dynFormatS",
"insp.dynHidden",
// The dynamic-tier operators run their dyn twins (scr_js_value.c); the
// conversions move a value between dyn and jsval, and js.toF64 is
+22
View File
@@ -618,6 +618,28 @@ function writesLocal(body: IrStmt[], localId: string): boolean {
});
}
/** The counter of a `for` loop that its condition reads and its update
* steps by a constant, when nothing else in the loop writes it: each test
* of the condition then sees a different value. The local must not be
* boxed (a closure could write it). */
export function steppedLoopCounter(
stmt: IrStmt & { kind: "for" },
locals: ReadonlyMap<string, IrLocal>,
): { ref: IrExpr & { kind: "varRef" }; step: number } | null {
const cond = stmt.cond;
if (cond?.kind !== "bin") return null;
for (const side of [cond.left, cond.right]) {
if (side.kind !== "varRef") continue;
const local = locals.get(side.localId);
if (local === undefined || local.boxed) continue;
const step = constantStep(stmt.update, side.localId);
if (step === null || writesLocal(stmt.body, side.localId)) continue;
if (writesLocal([{ kind: "exprStmt", expr: cond, loc: cond.loc }], side.localId)) continue;
return { ref: side, step };
}
return null;
}
function isUnitIncrement(update: IrStmt | null, localId: string): boolean {
// The frontend normally lowers `i++` to `i = i + 1` before backend
// emission. Accept the incDec form too so this analysis remains valid for
@@ -184,7 +184,8 @@ export function versionLoopBoundParameters(
symbolName: (name: string) => string = (name) => name,
): IrModule {
const candidates: { fn: IrFunction; params: Set<string> }[] = [];
const sites = callSites(mod);
// Call sites are read only for functions with loop-bound parameters.
let sites: ReturnType<typeof callSites> | null = null;
const functions = new Map(mod.functions.map((fn) => [fn.name, fn]));
const members = new Set((mod.classes ?? []).flatMap((cls) => cls.methods ?? []));
for (const fn of mod.functions) {
@@ -207,7 +208,11 @@ export function versionLoopBoundParameters(
const local = locals.get(id);
if (!local || local.boxed || local.tdz) params.delete(id);
}
if (params.size === 0 || size(fn) > MAX_FUNCTION_NODES || keepsSingleBody(fn, sites, functions))
if (
params.size === 0 ||
size(fn) > MAX_FUNCTION_NODES ||
keepsSingleBody(fn, (sites ??= callSites(mod)), functions)
)
continue;
candidates.push({ fn, params });
}
+32 -5
View File
@@ -93,12 +93,39 @@ test("a row-sum nest runs two rows per pass behind its bounds and overlap test",
// from the array the term reads.
expect(JSON.stringify(go.init)).toContain('"fn":"bytes.disjoint"');
expect(branch.else_).toBeNull();
const pairs = branch.then[0] as IrStmt & { kind: "for" };
// The four bounds, masked to the range the test proved, then the pairs.
expect(branch.then.slice(0, 4)).toMatchObject(
Array(4).fill({ kind: "varDecl", init: { op: "&", right: { value: 2 ** 25 - 1 } } }),
);
const pairs = branch.then[4] as IrStmt & { kind: "for" };
expect(pairs.body.filter((s) => s.kind === "bytesSet")).toHaveLength(2);
const inner = pairs.body[2] as IrStmt & { kind: "for" };
// One read of u[j] for both rows, then the two sums.
expect(inner.body).toHaveLength(3);
const [, , inner, last] = pairs.body as [
IrStmt,
IrStmt,
IrStmt & { kind: "for" },
IrStmt & { kind: "if" },
];
const column = inner.init as IrStmt & { kind: "varDecl" };
// Two columns per step: for each, one read of u[j] for both rows, then
// the two sums; the second column reads `column + 1`.
expect(inner.body).toHaveLength(6);
expect(inner.body[0]).toMatchObject({ kind: "varDecl", init: { kind: "bytesIntrinsic" } });
expect(inner.body[3]).toMatchObject({
kind: "varDecl",
init: {
kind: "bytesIntrinsic",
args: [{ op: "+", left: { localId: column.localId }, right: { value: 1 } }],
},
});
expect(inner.cond).toMatchObject({ op: "<", right: { op: "-", right: { value: 1 } } });
expect(inner.update).toMatchObject({ value: { op: "+", right: { value: 2 } } });
// The last column of an odd count, `to - 1`.
expect(last.then).toHaveLength(3);
expect(last.then[0]).toMatchObject({
kind: "varDecl",
init: { args: [{ op: "-", right: { value: 1 } }] },
});
expect(last.else_).toBeNull();
// The second row's term reads `row + 1`.
const row = pairs.init?.kind === "varDecl" ? pairs.init.localId : "";
expect((inner.body[2] as IrStmt & { kind: "assign" }).value).toMatchObject({
@@ -130,7 +157,7 @@ test("a row-sum nest runs two rows per pass behind its bounds and overlap test",
expect(top.body).toHaveLength(4);
expect(
result.functions[0]!.locals.map((l) => l.id).filter((id) => id.startsWith("%pair.")),
).toHaveLength(7);
).toHaveLength(13);
});
test("nests whose term calls, writes or reads the sum keep their loops", () => {
+163 -60
View File
@@ -7,11 +7,24 @@ import {
type IrModule,
type IrStmt,
} from "./ir.js";
import { everyExpr, everyExprChild, mapExprChildren, transformStmtList } from "./traverse.js";
import {
everyExpr,
everyExprChild,
everyStmtList,
mapExprChildren,
transformStmtList,
} from "./traverse.js";
import { PRODUCT_COUNTER_BOUND } from "./integer-loops.js";
/** Inner-loop expressions above this many nodes keep the loop as written. */
const MAX_TERM_NODES = 64;
/** The largest bound a paired nest runs with. Counters below
* PRODUCT_COUNTER_BOUND keep products of their sums exact, so the nest's
* counted loops take integer counters without a run-time guard (and its
* copy of the body). */
const SMALL_BOUND = PRODUCT_COUNTER_BOUND - 1;
/** The number operators a term may use: they run no other code and cannot
* throw. */
const PURE_BINARY = new Set<string>([
@@ -196,7 +209,7 @@ function copy(
}
const bin = (
op: "+" | "-" | "&" | "|",
op: "+" | "-" | "/" | "&" | "|",
left: IrExpr,
right: IrExpr,
loc: IrExpr["loc"],
@@ -210,7 +223,8 @@ const bin = (
});
/** Runs the row sums of `match` two rows per pass when the test holds and
* as written otherwise. */
* as written otherwise. Each pass takes two columns per step, then the
* last column when their count is odd. */
function pairRows(
s: IrStmt & { kind: "for" },
match: RowSum,
@@ -225,42 +239,93 @@ function pairRows(
sum = fresh("sum"),
next = fresh("next"),
column = fresh("column");
const atColumn = (r: IrExpr & { kind: "varRef" }): IrExpr =>
r.localId === match.column ? { ...r, localId: column } : { ...r };
// Element reads that do not depend on the row are read once per column
// and shared by both rows.
const shared = new Map<IrExpr, string>();
const elements: IrStmt[] = [];
const sharedReads: IrExpr[] = [];
const collect = (e: IrExpr): boolean => {
if (e.kind === "bytesIntrinsic" && e.method === "get" && !readsLocal(e, match.row)) {
const id = fresh("element");
shared.set(e, id);
elements.push({ kind: "varDecl", localId: id, init: copy(e, atColumn), loc });
sharedReads.push(e);
return true;
}
return everyExprChild(e, collect, () => true);
};
collect(match.term);
// The second row's term reads `row + 1`, so its counter sums stay visible
// to the counted-loop analysis.
const termAt = (rowExpr: () => IrExpr): IrExpr =>
copy(
match.term,
(r) => (r.localId === match.row ? rowExpr() : atColumn(r)),
(node) => {
const id = shared.get(node);
return id === undefined ? null : { kind: "varRef", localId: id, type: node.type, loc };
/** Both rows' additions at the column `columnAt()`: its shared element
* reads, then each row's sum. The second row's term reads `row + 1`, and
* the second column's `column + 1`, so their counter sums stay visible to
* the counted-loop analysis. */
const step = (columnAt: () => IrExpr): IrStmt[] => {
const at = (r: IrExpr & { kind: "varRef" }): IrExpr =>
r.localId === match.column ? columnAt() : { ...r };
const shared = new Map<IrExpr, string>();
const elements: IrStmt[] = sharedReads.map((read) => {
const id = fresh("element");
shared.set(read, id);
return { kind: "varDecl", localId: id, init: copy(read, at), loc };
});
const termAt = (rowExpr: () => IrExpr): IrExpr =>
copy(
match.term,
(r) => (r.localId === match.row ? rowExpr() : at(r)),
(node) => {
const id = shared.get(node);
return id === undefined ? null : { kind: "varRef", localId: id, type: node.type, loc };
},
);
return [
...elements,
{
kind: "assign",
localId: sum,
value: bin(
"+",
ref(sum),
termAt(() => ref(row)),
loc,
),
loc,
},
);
{
kind: "assign",
localId: next,
value: bin(
"+",
ref(next),
termAt(() => bin("+", ref(row), num(1), loc)),
loc,
),
loc,
},
];
};
const innerCond = match.inner.cond as IrExpr & { kind: "bin" };
const columnStart = (match.inner.init as IrStmt & { kind: "varDecl" }).init!;
// The bounds, as the paired nest reads them (see `small` below).
const low = fresh("low"),
high = fresh("high"),
first = fresh("first"),
last = fresh("last");
const bounds: IrStmt[] = (
[
[low, match.start],
[high, match.end],
[first, columnStart],
[last, innerCond.right],
] as const
).map(([id, bound]) => ({
kind: "varDecl",
localId: id,
init: bin("&", again(bound), num(SMALL_BOUND), loc),
loc,
}));
const pairs: IrStmt = {
kind: "for",
init: { kind: "varDecl", localId: row, init: again(match.start), loc },
init: { kind: "varDecl", localId: row, init: ref(low), loc },
cond: {
kind: "bin",
op: "<",
left: ref(row),
right: bin("-", again(match.end), num(1), loc),
right: bin("-", ref(high), num(1), loc),
type: BOOL,
loc,
},
@@ -268,41 +333,40 @@ function pairRows(
body: [
{ kind: "varDecl", localId: sum, init: { ...match.init }, loc },
{ kind: "varDecl", localId: next, init: { ...match.init }, loc },
// Both column bounds are int32, so `column < to - 1` is
// `column + 1 < to`. Each row adds its columns in order.
{
kind: "for",
init: {
kind: "varDecl",
localId: column,
init: again((match.inner.init as IrStmt & { kind: "varDecl" }).init!),
init: { kind: "varDecl", localId: column, init: ref(first), loc },
cond: {
...innerCond,
left: ref(column),
right: bin("-", ref(last), num(1), loc),
},
update: { kind: "assign", localId: column, value: bin("+", ref(column), num(2), loc), loc },
body: [...step(() => ref(column)), ...step(() => bin("+", ref(column), num(1), loc))],
loc,
},
// An odd column count leaves the last column, `to - 1`.
{
kind: "if",
cond: {
kind: "logical",
op: "&&",
left: { ...innerCond, left: ref(first), right: ref(last) },
right: {
kind: "bin",
op: "===",
left: bin("&", bin("-", ref(last), ref(first), loc), num(1), loc),
right: num(1),
type: BOOL,
loc,
},
type: BOOL,
loc,
},
cond: { ...innerCond, left: ref(column), right: again(innerCond.right) },
update: { kind: "assign", localId: column, value: bin("+", ref(column), num(1), loc), loc },
body: [
...elements,
{
kind: "assign",
localId: sum,
value: bin(
"+",
ref(sum),
termAt(() => ref(row)),
loc,
),
loc,
},
{
kind: "assign",
localId: next,
value: bin(
"+",
ref(next),
termAt(() => bin("+", ref(row), num(1), loc)),
loc,
),
loc,
},
],
then: step(() => bin("-", ref(last), num(1), loc)),
else_: null,
loc,
},
{ kind: "bytesSet", arr: { ...match.out }, index: ref(row), value: ref(sum), loc },
@@ -316,9 +380,13 @@ function pairRows(
],
loc,
};
// Both bounds are int32, so `row < end - 1` is `row + 1 < end`, and the
// nest has at least two rows. The overlap tests run only then, so they never
// read an array the program assigns only when there are rows. Rows are
// Every bound is an integer in [0, SMALL_BOUND] and not -0, so `row <
// end - 1` is `row + 1 < end` (and the same for the columns), and the
// masks in `bounds` leave the bounds unchanged while proving both
// counters' ranges.
// The nest has at least two rows. The column bounds' tests and the
// overlap tests run only then, so they never read an array the program
// assigns only when there are rows. Rows are
// independent when the written array shares no byte with an array the
// term reads; an array that is not there answers false.
const int32 = (e: IrExpr): IrExpr => ({
@@ -329,9 +397,24 @@ function pairRows(
type: BOOL,
loc,
});
const compare = (op: "<=" | ">=", e: IrExpr, value: number): IrExpr => ({
kind: "bin",
op,
left: again(e),
right: num(value),
type: BOOL,
loc,
});
// `1 / e > 0` holds for +0 and every positive bound, but not for -0:
// masking would turn a -0 start into +0, which a term can tell apart.
const small = (e: IrExpr): IrExpr[] => [
int32(e),
{ kind: "bin", op: ">", left: bin("/", num(1), again(e), loc), right: num(0), type: BOOL, loc },
compare("<=", e, SMALL_BOUND),
];
const tests: IrExpr[] = [
int32(match.start),
int32(match.end),
...small(match.start),
...small(match.end),
{
kind: "bin",
op: "<",
@@ -340,6 +423,8 @@ function pairRows(
type: BOOL,
loc,
},
...small(columnStart),
...small(innerCond.right),
...match.reads.map((read): IrExpr => ({
kind: "libCall",
fn: "bytes.disjoint",
@@ -394,7 +479,7 @@ function pairRows(
},
loc,
},
{ kind: "if", cond: { ...paired }, then: [pairs], else_: null, loc },
{ kind: "if", cond: { ...paired }, then: [...bounds, pairs], else_: null, loc },
rest,
],
loc,
@@ -413,12 +498,30 @@ function pairRows(
* store into shares no byte with any array the term reads, so no row reads
* another row's result. An odd row count runs its last row as written.
* Otherwise the nest runs as written. Two independent sums take turns in
* the inner loop, so neither waits on the other's addition. */
* the inner loop, so neither waits on the other's addition.
*
* The inner loop takes two columns per step, each row adding the first
* column's term and then the second's, and the last column on its own
* when the count is odd, so the additions keep their order. Both column
* bounds must also be int32 (so `column < to - 1` is `column + 1 < to`).
* Neighboring rows and columns share counter sums (`row + 1 + column` and
* `row + (column + 1)`), and the step's divisions have no dependence
* between them. */
export function pairRowSums(mod: IrModule): IrModule {
let changed = false;
const functions = mod.functions.map((fn): IrFunction => {
if (fn.async || fn.generator) return fn;
const locals = new Map(fn.locals.map((local) => [local.id, local]));
// Most bodies hold no row sum: find one before copying the body.
let found = false;
everyStmtList(fn.body, {
expr: () => !found,
stmt: (s) => {
if (s.kind === "for" && matchRowSum(s, locals) !== null) found = true;
return !found;
},
});
if (!found) return fn;
const added: IrLocal[] = [];
const ids = new Set(fn.locals.map((local) => local.id));
const fresh = (name: string): string => {
+25 -1
View File
@@ -103,6 +103,11 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"punycode.warn",
// DEP0005 for the deprecated Buffer constructor.
"buffer.deprecation",
// util.aborted answers a checked-dynamic promise, and so do the
// functions util.debounce and util.throttle return.
"util.aborted",
"util.debounce",
"util.throttle",
"timers.immediatePromise",
// The performance timeline (scr_perf.c, linked with this unit for its
// MaxPerformanceEntryBufferExceededWarning).
@@ -144,8 +149,11 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"util.promisifyDyn",
"util.debuglog",
"util.deprecate",
"util.diff",
"util.extend",
"util.inherits",
"util.inheritsPrototype",
"util.isArrayDeprecated",
"util.styleText",
]);
@@ -174,8 +182,11 @@ const UTIL_CALLABLE_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"util.callbackify",
"util.debuglog",
"util.deprecate",
"util.diff",
"util.extend",
"util.inherits",
"util.inheritsPrototype",
"util.isArrayDeprecated",
]);
/** Entry points that link the regex unit: the regex and glob matchers (fs.glob's walk compiles its patterns with the glob matcher), and readline's terminal unit (scr_readline_tty.c), which measures with the regex unit's stripVTControlCharacters. */
@@ -997,9 +1008,20 @@ const RUNTIME_FEATURE_RULES = {
(mod.classes ?? []).some(
(c) => c.name === RUNTIME_EVENT_TARGET_CLASS || c.name === RUNTIME_EVENT_CLASS,
),
fns: ["events.addAbortListener", ...ABORT_SIGNAL_LIB_FNS],
// util.debounce and util.throttle register options.signal's listener.
fns: [
"events.addAbortListener",
"util.aborted",
"util.debounce",
"util.throttle",
...ABORT_SIGNAL_LIB_FNS,
],
prefixes: ["event."],
}),
/** util.MIMEType and util.MIMEParams (scr_mime.c). */
mime: rule({ prefixes: ["mime."] }),
/** util.debounce and util.throttle (scr_util_timing.c). */
utilTiming: rule({ fns: ["util.debounce", "util.throttle"] }),
/** events.once(emitter, name, { signal }) (scr_events_once_signal.c). */
eventsOnceSignal: rule({ fns: ["events.onceSignal"] }),
fetch: rule({ prefixes: ["fetch."] }),
@@ -1221,6 +1243,8 @@ function noRuntimeFeatures(): RuntimeFeatures {
http: false,
inspect: false,
legacyTextDecoder: false,
mime: false,
utilTiming: false,
mockTimers: false,
net: false,
netLookup: false,
+9 -3
View File
@@ -554,17 +554,23 @@ export function scalarizeLiteralResults(mod: IrModule): IrModule {
}
if (producers.size === 0) return mod;
let changed = false;
const globalIds = new Set((mod.globals ?? []).map((g) => g.id));
const functions = mod.functions.map((fn): IrFunction => {
if (fn.async || fn.generator) return fn;
const locals = new Map(fn.locals.map((l) => [l.id, l]));
const used = new Set([...locals.keys(), ...(mod.globals ?? []).map((g) => g.id)]);
collectLabels(fn.body, used);
// The names a fresh binding must avoid, collected when the first one is
// made: most functions inline no producer.
let used: Set<string> | null = null;
let next = 0;
const fresh = (): string => {
if (used === null) {
used = new Set(locals.keys());
collectLabels(fn.body, used);
}
let id: string;
do {
id = `%literal.${next++}`;
} while (used.has(id));
} while (used.has(id) || globalIds.has(id));
used.add(id);
return id;
};
+2 -1
View File
@@ -107,7 +107,8 @@ export function resolveSelfCaptures(mod: IrModule): IrModule {
if (creator.captures?.some((c) => c.localId === source)) return false;
if (creator.classCaptures?.some((c) => c.localId === source)) return false;
if (declarations.get(owner)!.get(source) !== 1) return false;
flow ??= new CaptureFlow(mod.functions);
// Only unchanged() is asked: the flow reads just the bodies it needs.
flow ??= new CaptureFlow(mod.functions, false);
return flow.unchanged(owner, source, definition.writes);
});
if (qualifies) {
+1
View File
@@ -168,6 +168,7 @@ export class NativeCache {
"sanitizer",
"executable",
"kept",
"linker-job",
"runtime-pack",
"binary",
"dsym",
+6 -2
View File
@@ -70,6 +70,7 @@ import { prepareNativeExecutable, prepareNativeLibrary } from "./prepare.js";
import { buildSanitizedRuntime, sanitizerDriver, sanitizerFlags } from "./sanitizer.js";
import { lldLinkerArgs } from "../backend/lld.js";
import { openNativeExecutableCache } from "./executable-cache.js";
import { LinkerJobs } from "./linker-job.js";
import { nativeFileIdentity } from "./file-identity.js";
/** The first partition keeps the whole-object key used before partitioning. */
@@ -530,11 +531,14 @@ export class NativeCompiler {
"-o",
stagedOutput,
];
const cacheable = executableCache?.trace(linkArgs, stage) ?? false;
// Host Linux links with lld run the linker job the driver would.
const jobs = LinkerJobs.open(this.cache, toolchain, lld, options.sanitize ?? false);
const job = jobs?.link([...plan.inputs, stagedOutput], stage);
const cacheable = executableCache?.trace(linkArgs, stage, job) ?? false;
timing("link-inputs");
// A successful dependency trace already produced this executable.
// Keep its verified output instead of asking the linker to repeat it.
if (!cacheable) {
if (!cacheable && (job?.run(linkArgs, true) ?? null) === null) {
const driver = options.sanitize ? sanitizerDriver(toolchain) : toolchain.linker;
try {
runNativeTool(driver, linkArgs);
@@ -15,7 +15,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, expect, test, vi } from "vitest";
import { contentDigest, NativeCache } from "./cache.js";
import { NativeExecutableCache } from "./executable-cache.js";
import { identity, NativeExecutableCache } from "./executable-cache.js";
vi.mock("node:child_process", async (original) => ({
...(await original<typeof import("node:child_process")>()),
@@ -331,3 +331,43 @@ test("a Linux build that keeps its output does not read the deferred inputs", ()
expect(open().restore(f.destination)).toBe(true);
expect(deferred).toHaveBeenCalledTimes(3);
});
test("a Linux link that runs as its linker job is traced from the job's run, and otherwise records it", () => {
const f = fixture();
const lib = join(f.root, "lib");
mkdirSync(lib);
const crt = join(lib, "Scrt1.o");
writeFileSync(crt, "startup object");
const program = join(f.stage, "program.o");
writeFileSync(program, "program");
const job = {
run: vi.fn((_args: string[], _list: boolean) => ({
output: [crt, program].join("\n") + "\n",
driverInputs: [identity(crt), identity(lib)],
})),
record: vi.fn(),
};
const open = () => new NativeExecutableCache(f.cache, f.key, "clang", false, [f.runtime], true);
const entry = open();
expect(entry.trace(["-o", f.output], f.stage, job)).toBe(true);
// The job runs the traced link, and the driver does not.
expect(job.run.mock.calls[0]).toEqual([["-o", f.output, "-v", "-Wl,-t"], false]);
expect(spawnSync).not.toHaveBeenCalled();
expect(job.record).not.toHaveBeenCalled();
entry.recordOutput(f.output);
const recorded = (
JSON.parse(f.cache.read("kept", f.key)!.toString()) as { inputs: { path: string }[] }
).inputs.map((saved) => saved.path);
expect(recorded).toEqual(expect.arrayContaining([crt, lib]));
expect(recorded).not.toContain(program);
expect(open().keep(f.output)).toBe(true);
// A link without a job runs through the driver, and records the job from
// the driver's output.
const record = vi.fn();
expect(open().trace(["-o", f.output], f.stage, { run: () => null, record })).toBe(true);
expect(spawnSync).toHaveBeenCalledTimes(1);
expect(record).toHaveBeenCalledWith(["-o", f.output, "-v", "-Wl,-t"], f.library + "\n\n", [
identity(f.sdk),
identity(f.library),
]);
});
@@ -14,8 +14,9 @@ import type { NativeOptimization } from "../backend/optimization.js";
import type { NativeRuntimeSelection } from "../backend/runtime-pack-native.js";
import type { NativeToolchain } from "./toolchain.js";
import { contentDigest, NativeCache } from "./cache.js";
import type { LinkerJobLink } from "./linker-job.js";
interface InputIdentity {
export interface InputIdentity {
path: string;
canonical: string;
kind: "file" | "directory";
@@ -33,7 +34,7 @@ interface ExecutableEntry {
symbols: string | null;
}
function identity(path: string): InputIdentity {
export function identity(path: string): InputIdentity {
path = resolve(path);
const info = statSync(path);
if (!info.isFile() && !info.isDirectory())
@@ -63,7 +64,7 @@ function sameIdentity(a: InputIdentity, b: InputIdentity): boolean {
);
}
function stillMatches(inputs: InputIdentity[]): boolean {
export function stillMatches(inputs: InputIdentity[]): boolean {
try {
return inputs.every((input) => sameIdentity(identity(input.path), input));
} catch {
@@ -71,7 +72,7 @@ function stillMatches(inputs: InputIdentity[]): boolean {
}
}
function commandPath(command: string): string {
export function commandPath(command: string): string {
if (command.includes("/") || command.includes("\\")) return resolve(command);
for (const directory of (process.env["PATH"] ?? "/usr/bin:/bin").split(delimiter)) {
const candidate = join(directory || process.cwd(), command);
@@ -92,7 +93,7 @@ function toolOutput(executable: string, args: string[]): string {
return result.stdout + "\n" + result.stderr;
}
function tracePaths(output: string, driver: boolean, roots: string[]): string[] {
export function tracePaths(output: string, driver: boolean, roots: string[]): string[] {
const paths = new Set<string>();
for (const line of output.split(/\r?\n/)) {
for (const candidate of driver ? driverTraceCandidates(line) : linkTraceCandidate(line)) {
@@ -134,7 +135,7 @@ interface KeptEntry {
inputs: InputIdentity[];
}
function validIdentity(input: InputIdentity): boolean {
export function validIdentity(input: InputIdentity): boolean {
return (
input !== null &&
typeof input === "object" &&
@@ -290,8 +291,10 @@ export class NativeExecutableCache {
* Private build inputs already belong to the LLVM/runtime cache identity.
* A true result means the requested output was linked successfully and its
* inputs remained stable. The caller can publish that output directly. On
* false, the caller links the output itself. */
trace(args: string[], stage: string): boolean {
* false, the caller links the output itself. On Linux, `job` runs the
* traced link as its recorded linker job when it can, and otherwise records
* the job the driver ran (see LinkerJobs). */
trace(args: string[], stage: string, job?: LinkerJobLink): boolean {
this.traced = false;
try {
this.addDeferredInputs();
@@ -300,8 +303,13 @@ export class NativeExecutableCache {
? []
: tracePaths(toolOutput(this.linker, [...args, "-###"]), true, roots).map(identity);
// On Linux, -v prints the jobs that -### would, while the link runs.
const linked = toolOutput(this.linker, [...args, ...(this.linux ? ["-v"] : []), "-Wl,-t"]);
if (this.linux) driverInputs = tracePaths(linked, true, roots).map(identity);
const traced = [...args, ...(this.linux ? ["-v"] : []), "-Wl,-t"];
const direct = this.linux ? (job?.run(traced, false) ?? null) : null;
const linked = direct?.output ?? toolOutput(this.linker, traced);
if (this.linux) {
driverInputs = direct?.driverInputs ?? tracePaths(linked, true, roots).map(identity);
if (direct === null) job?.record(traced, linked, driverInputs);
}
const paths = tracePaths(linked, false, roots);
if (paths.length === 0 || !stillMatches(this.inputs) || !stillMatches(driverInputs))
return false;
@@ -0,0 +1,408 @@
import { spawnSync } from "node:child_process";
import {
chmodSync,
mkdirSync,
mkdtempSync,
readFileSync,
renameSync,
rmSync,
statSync,
utimesSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, expect, test } from "vitest";
import {
LINUX_ARM64_GNU_TARGET,
LINUX_X64_GNU_TARGET,
MACOS_ARM64_TARGET,
type NativeTargetSpec,
} from "../backend/targets.js";
import { NativeCache } from "./cache.js";
import { LinkerJobs } from "./linker-job.js";
import type { NativeToolchain } from "./toolchain.js";
const directories: string[] = [];
const saved = new Map<string, string | undefined>();
const variables = [
"SCRIPTC_LINKER",
"COMPILER_PATH",
"LOG",
"LD",
"CRT",
"LIB",
"FAIL",
"BROKEN",
"DOUBLE",
];
beforeEach(() => {
for (const name of variables) {
saved.set(name, process.env[name]);
delete process.env[name];
}
});
afterEach(() => {
for (const [name, value] of saved) {
if (value === undefined) delete process.env[name];
else process.env[name] = value;
}
saved.clear();
for (const path of directories.splice(0)) rmSync(path, { recursive: true, force: true });
});
function script(path: string, text: string): string {
writeFileSync(path, text);
chmodSync(path, 0o755);
return path;
}
/** A driver named `name` and an ld.lld that log their arguments to $LOG. The
* driver's job runs $LD with `-pie`, the startup object $CRT, `-L$LIB` and
* its own arguments (without `-fuse-ld=lld` and `-v`, `-Wl,` removed). With
* -### it prints that job as clang does, every argument quoted; with -v it
* prints the job it runs as clang does, only the program quoted. With
* $BROKEN, -### prints the job twice; with $DOUBLE, it names the path $DOUBLE
* once more. The linker writes its inputs to its output, prints them with
* `-t`, and fails with $FAIL. */
function fixture(name = "clang") {
const root = mkdtempSync(join(tmpdir(), "scriptc-linker-job-"));
directories.push(root);
const bin = join(root, "bin");
const lib = join(root, "lib");
// The recorded inputs include the parents of the toolchain's paths (here
// `root`); stages are elsewhere, as a build's are.
const stages = join(root, "stages");
for (const directory of [bin, lib, stages]) mkdirSync(directory);
const log = join(root, "log");
writeFileSync(log, "");
const driver = script(
join(bin, name),
`#!/bin/sh
echo "driver $*" >> "$LOG"
job=" \\"$LD\\" \\"-pie\\" \\"$CRT\\" \\"-L$LIB\\""
raw=" \\"$LD\\" -pie $CRT -L$LIB"
listing=0; verbose=0
for a; do
case "$a" in
-'###') listing=1 ;;
-v) verbose=1 ;;
-fuse-ld=lld) ;;
-Wl,*) job="$job \\"\${a#-Wl,}\\""; raw="$raw \${a#-Wl,}" ;;
*) job="$job \\"$a\\""; raw="$raw $a" ;;
esac
done
if [ $listing = 1 ]; then
echo "InstalledDir: $(dirname "$0")" >&2
[ -z "$DOUBLE" ] && echo "$job" >&2
[ -n "$BROKEN" ] && echo "$job" >&2
[ -n "$DOUBLE" ] && echo "$job \\"$DOUBLE\\"" >&2 && exit 0
exit 0
fi
[ $verbose = 1 ] && echo "$raw" >&2
eval "exec $job"
`,
);
const ld = script(
join(bin, "ld.lld"),
`#!/bin/sh
echo "ld.lld $*" >> "$LOG"
[ -n "$FAIL" ] && exit 1
out=""; prev=""
for a; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; done
printf '%s\\n' "$@" > "$out"
for a; do [ "$a" = "-t" ] && printf '%s\\n' "$@" | grep '^/'; done
exit 0
`,
);
const crt = join(lib, "Scrt1.o");
writeFileSync(crt, "startup object");
Object.assign(process.env, { LOG: log, LD: ld, CRT: crt, LIB: lib });
// A directory's identity is its stat: an old time makes a later change visible.
utimesSync(lib, 0, 0);
const cache = new NativeCache(join(root, "cache"));
const toolchain = (target: NativeTargetSpec = LINUX_X64_GNU_TARGET, helper = target) =>
({
target,
helper: helper.helper,
linker: driver,
}) as unknown as NativeToolchain;
let count = 0;
/** A stage with a program object, the link's paths and its driver arguments. */
const stage = () => {
const directory = join(stages, `stage-${++count}`);
mkdirSync(directory);
const program = join(directory, "program.o");
const output = join(directory, "out");
writeFileSync(program, "program");
return {
directory,
output,
paths: [program, output],
args: ["-fuse-ld=lld", "-pthread", program, "-lm", "-o", output],
};
};
const calls = () =>
readFileSync(log, "utf8")
.split("\n")
.filter((line) => line !== "");
return { root, bin, lib, crt, ld, driver, cache, toolchain, stage, calls };
}
test.skipIf(process.platform === "win32")(
"a link records the driver's job once, and later links run ld.lld with their own paths",
() => {
const f = fixture();
const jobs = LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!;
expect(jobs).not.toBeNull();
const first = f.stage();
const run = jobs.run(first.args, first.paths, first.directory, true);
expect(run).not.toBeNull();
// The driver only listed the job; ld.lld linked.
expect(f.calls()).toEqual([
`driver ${first.args.join(" ")} -v -###`,
`ld.lld -pie ${f.crt} -L${f.lib} -pthread ${first.paths[0]} -lm -o ${first.output}`,
]);
expect(readFileSync(first.output, "utf8")).toContain(first.paths[0]);
const recorded = run!.driverInputs.map((input) => input.path);
expect(recorded).toEqual(expect.arrayContaining([f.ld, f.bin, f.crt, f.lib]));
expect(recorded.some((path) => path.startsWith(first.directory))).toBe(false);
// Another build, with other paths, runs the recorded job without the driver.
const second = f.stage();
const again = LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!;
expect(again.run(second.args, second.paths, second.directory, true)).not.toBeNull();
expect(f.calls().slice(2)).toEqual([
`ld.lld -pie ${f.crt} -L${f.lib} -pthread ${second.paths[0]} -lm -o ${second.output}`,
]);
// Other arguments are another job.
const third = f.stage();
expect(
again.run([...third.args, "-Wl,-t"], third.paths, third.directory, true)?.output,
).toContain(third.paths[0]);
expect(f.calls().slice(3)[0]).toBe(`driver ${third.args.join(" ")} -Wl,-t -v -###`);
},
);
test.skipIf(process.platform === "win32")(
"a driver link records its job from its -v output, and later links run it",
() => {
const f = fixture();
const open = () => LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!;
const first = f.stage();
const traced = [...first.args, "-v", "-Wl,-t"];
// Without an entry, a link that is not listed runs through the driver.
const link = open().link(first.paths, first.directory);
expect(link.run(traced, false)).toBeNull();
expect(f.calls()).toEqual([]);
const linked = spawnSync(f.driver, traced, { encoding: "utf8" });
expect(linked.status).toBe(0);
link.record(traced, linked.stdout + "\n" + linked.stderr);
const second = f.stage();
const run = open()
.link(second.paths, second.directory)
.run([...second.args, "-v", "-Wl,-t"], false);
expect(run?.output).toContain(second.paths[0]);
expect(run!.driverInputs.map((input) => input.path)).toEqual(
expect.arrayContaining([f.ld, f.bin, f.crt, f.lib]),
);
expect(f.calls().slice(2)).toEqual([
`ld.lld -pie ${f.crt} -L${f.lib} -pthread ${second.paths[0]} -lm -o ${second.output} -t`,
]);
},
);
test.skipIf(process.platform === "win32")(
"a changed startup object, library directory or linker sends the link to the driver",
() => {
for (const change of ["crt", "lib", "ld"] as const) {
const f = fixture();
const open = () => LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!;
const first = f.stage();
expect(open().run(first.args, first.paths, first.directory, true)).not.toBeNull();
if (change === "lib") writeFileSync(join(f.lib, "libm.so"), "new library");
else {
const path = change === "crt" ? f.crt : f.ld;
const before = statSync(path);
writeFileSync(path + ".new", readFileSync(path));
chmodSync(path + ".new", 0o755);
utimesSync(path + ".new", before.atime, before.mtime);
renameSync(path + ".new", path);
}
const second = f.stage();
const count = f.calls().length;
expect(open().run(second.args, second.paths, second.directory, true)).toBeNull();
expect(f.calls().length).toBe(count);
}
},
);
test.skipIf(process.platform === "win32")("a changed driver records the job again", () => {
const f = fixture();
const first = f.stage();
expect(
LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!.run(
first.args,
first.paths,
first.directory,
true,
),
).not.toBeNull();
writeFileSync(f.driver, readFileSync(f.driver, "utf8") + "\n");
const second = f.stage();
expect(
LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!.run(
second.args,
second.paths,
second.directory,
true,
),
).not.toBeNull();
expect(f.calls()[2]).toBe(`driver ${second.args.join(" ")} -v -###`);
});
test.skipIf(process.platform === "win32")(
"a job that fails sends this and the build's later links to the driver",
() => {
const f = fixture();
const first = f.stage();
const open = () => LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!;
expect(open().run(first.args, first.paths, first.directory, true)).not.toBeNull();
process.env["FAIL"] = "1";
const jobs = open();
const second = f.stage();
expect(jobs.run(second.args, second.paths, second.directory, true)).toBeNull();
const count = f.calls().length;
expect(jobs.run(second.args, second.paths, second.directory, true)).toBeNull();
expect(f.calls().length).toBe(count);
},
);
test.skipIf(process.platform === "win32")(
"a listing without one ld.lld job naming each path once keeps the driver, and is not asked again",
() => {
for (const listing of ["two jobs", "another linker", "a path twice"] as const) {
const f = fixture();
if (listing === "two jobs") process.env["BROKEN"] = "1";
if (listing === "another linker")
process.env["LD"] = script(join(f.bin, "ld.bfd"), readFileSync(f.ld, "utf8"));
const open = () => LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!;
const first = f.stage();
if (listing === "a path twice") process.env["DOUBLE"] = first.paths[0];
expect(open().run(first.args, first.paths, first.directory, true)).toBeNull();
expect(f.calls()).toEqual([`driver ${first.args.join(" ")} -v -###`]);
// The answer is stored: the next build does not list the job again.
expect(open().run(first.args, first.paths, first.directory, true)).toBeNull();
expect(f.calls().length).toBe(1);
delete process.env["BROKEN"];
delete process.env["DOUBLE"];
}
},
);
test.skipIf(process.platform === "win32")(
"arguments that name the stage besides the link's paths keep the driver",
() => {
const f = fixture();
const first = f.stage();
const jobs = LinkerJobs.open(f.cache, f.toolchain(), ["-fuse-ld=lld"], false)!;
const script = `-Wl,--version-script=${join(first.directory, "exports")}`;
expect(jobs.run([...first.args, script], first.paths, first.directory, true)).toBeNull();
expect(f.calls()).toEqual([]);
},
);
test.skipIf(process.platform === "win32")(
"only host Linux links through a clang driver with lld run the job",
() => {
const f = fixture();
const lld = ["-fuse-ld=lld"];
expect(LinkerJobs.open(f.cache, f.toolchain(), lld, false)).not.toBeNull();
// A cross-target link: the arm64 target with the x64 host's helper.
expect(
LinkerJobs.open(
f.cache,
f.toolchain(LINUX_ARM64_GNU_TARGET, LINUX_X64_GNU_TARGET),
lld,
false,
),
).toBeNull();
expect(
LinkerJobs.open(
f.cache,
f.toolchain(LINUX_X64_GNU_TARGET, LINUX_ARM64_GNU_TARGET),
lld,
false,
),
).toBeNull();
expect(LinkerJobs.open(f.cache, f.toolchain(MACOS_ARM64_TARGET), lld, false)).toBeNull();
// The driver's default linker, a sanitizer link, no cache.
expect(LinkerJobs.open(f.cache, f.toolchain(), [], false)).toBeNull();
expect(LinkerJobs.open(f.cache, f.toolchain(), lld, true)).toBeNull();
expect(LinkerJobs.open(null, f.toolchain(), lld, false)).toBeNull();
// A linker that is not a clang driver.
const gcc = fixture("gcc");
expect(LinkerJobs.open(gcc.cache, gcc.toolchain(), lld, false)).toBeNull();
// An explicit linker, or an environment that names mutable toolchain inputs.
process.env["SCRIPTC_LINKER"] = f.driver;
expect(LinkerJobs.open(f.cache, f.toolchain(), lld, false)).toBeNull();
delete process.env["SCRIPTC_LINKER"];
process.env["COMPILER_PATH"] = f.bin;
expect(LinkerJobs.open(f.cache, f.toolchain(), lld, false)).toBeNull();
},
);
const hostTarget =
process.arch === "x64"
? LINUX_X64_GNU_TARGET
: process.arch === "arm64"
? LINUX_ARM64_GNU_TARGET
: null;
const hasLld =
process.platform === "linux" &&
spawnSync("clang", ["-fuse-ld=lld", "-###", "-x", "c", "/dev/null"], {
encoding: "utf8",
}).stderr?.includes("ld.lld") === true;
test.skipIf(hostTarget === null || !hasLld)("the job links the executable the driver links", () => {
const root = mkdtempSync(join(tmpdir(), "scriptc-linker-job-clang-"));
directories.push(root);
const stage = join(root, "stage");
mkdirSync(stage);
const program = join(stage, "program.o");
const compiled = spawnSync("clang", ["-c", "-x", "c", "-", "-o", program], {
input: '#include <stdio.h>\nint main(void) { puts("linked"); return 0; }\n',
});
expect(compiled.status).toBe(0);
const toolchain = {
target: hostTarget,
helper: hostTarget!.helper,
linker: "clang",
} as unknown as NativeToolchain;
const args = (output: string) => ["-fuse-ld=lld", "-pthread", program, "-lm", "-o", output];
const viaDriver = join(stage, "driver");
expect(spawnSync("clang", args(viaDriver)).status).toBe(0);
const cache = new NativeCache(join(root, "cache"));
// Recorded from the -v output of a traced link, and listed with -###.
const traced = (output: string) => [...args(output), "-v", "-Wl,-t"];
const tracedOutput = join(stage, "traced");
const linked = spawnSync("clang", traced(tracedOutput), { encoding: "utf8" });
expect(linked.status).toBe(0);
LinkerJobs.open(cache, toolchain, ["-fuse-ld=lld"], false)!
.link([program, tracedOutput], stage)
.record(traced(tracedOutput), linked.stdout + "\n" + linked.stderr);
for (const [name, list] of [
["first", false],
["second", true],
["third", true],
] as const) {
const output = join(stage, name);
const link = LinkerJobs.open(cache, toolchain, ["-fuse-ld=lld"], false)!.link(
[program, output],
stage,
);
const run = list ? link.run(args(output), true) : link.run(traced(output), false);
expect(run).not.toBeNull();
expect(readFileSync(output).equals(readFileSync(viaDriver))).toBe(true);
expect(spawnSync(output, { encoding: "utf8" }).stdout).toBe("linked\n");
}
});
+286
View File
@@ -0,0 +1,286 @@
import { spawnSync } from "node:child_process";
import { realpathSync } from "node:fs";
import { basename, extname, isAbsolute, resolve } from "node:path";
import {
toolchainEnvironmentCachePolicy,
toolchainEnvironmentFingerprint,
} from "../backend/toolchain-environment.js";
import type { NativeToolchain } from "./toolchain.js";
import { contentDigest, type NativeCache } from "./cache.js";
import {
commandPath,
identity,
type InputIdentity,
stillMatches,
tracePaths,
validIdentity,
} from "./executable-cache.js";
/** A recorded linker job. In `job`, a number is the index of one of the link's
* own paths (its inputs and output); `inputs` are the identities of the
* other files and directories the job and the driver's `-v` lines name. */
interface LinkerJobEntry {
schema: "scriptc.linker-job.v1";
/** null when the driver's job could not be recorded: the link runs through
* the driver. */
job: (string | number)[] | null;
inputs: InputIdentity[];
}
/** A link that ran as its recorded job: the linker's output, and the
* identities the job depends on besides the link's own paths. */
export interface LinkerJobRun {
output: string;
driverInputs: InputIdentity[];
}
/** One link's use of LinkerJobs, with its own paths and stage. */
export interface LinkerJobLink {
run(args: string[], list: boolean): LinkerJobRun | null;
record(args: string[], output: string, driverInputs?: InputIdentity[]): void;
}
const MAX_ENTRY_BYTES = 1024 * 1024;
function validEntry(value: unknown, paths: number): value is LinkerJobEntry {
if (value === null || typeof value !== "object") return false;
const entry = value as Partial<LinkerJobEntry>;
if (entry.schema !== "scriptc.linker-job.v1" || !Array.isArray(entry.inputs)) return false;
if (!entry.inputs.every(validIdentity)) return false;
if (entry.job === null) return true;
if (!Array.isArray(entry.job) || typeof entry.job[0] !== "string") return false;
const used = new Set<number>();
for (const token of entry.job) {
if (typeof token === "string") continue;
if (!Number.isInteger(token) || token < 0 || token >= paths || used.has(token)) return false;
used.add(token);
}
return used.size === paths;
}
function underRoots(text: string, roots: string[]): boolean {
return roots.some((root) => text.includes(root));
}
/** The arguments of one job line as the driver prints them: with `-###`
* each argument is quoted, with `-v` only the program and the arguments that
* contain a space, `"`, `\` or `$`; a quoted argument escapes `"`, `\` and
* `$` with `\`. Null for any other line. */
function jobArguments(line: string): string[] | null {
const pattern = /\s*(?:"((?:\\.|[^"\\])*)"|([^\s"\\]+))/y;
const args: string[] = [];
let end = 0;
let match: RegExpExecArray | null;
while ((match = pattern.exec(line)) !== null) {
args.push(match[1] === undefined ? match[2]! : match[1].replace(/\\(.)/g, "$1"));
end = pattern.lastIndex;
}
return args.length === 0 || line.slice(end).trim() !== "" ? null : args;
}
/**
* Linux links through a clang driver with lld. The driver only starts ld.lld
* with a job it derives from its arguments, its environment, its own
* installation and the system's (startup objects, the GCC installation,
* library directories). The job is recorded once per driver identity,
* arguments and environment, with the identities of the files and
* directories it and the driver's `-v` lines name. A later link whose recorded
* paths still have those identities runs ld.lld with the job directly; every
* other link runs through the driver.
*/
export class LinkerJobs {
/** A job run that failed in this build: later links run through the
* driver, which reports the error. */
private failed = false;
private constructor(
private readonly cache: NativeCache,
private readonly driver: string,
private readonly driverIdentity: InputIdentity,
) {}
/** Null unless the link is a host Linux link through a clang driver with
* `-fuse-ld=lld` (`lld`, the link's lld arguments), outside the sanitizer,
* with a native cache and a toolchain environment that allows complete
* artifacts. */
static open(
cache: NativeCache | null,
toolchain: NativeToolchain,
lld: readonly string[],
sanitize: boolean,
): LinkerJobs | null {
const target = toolchain.target;
if (
cache === null ||
sanitize ||
target.platform !== "linux" ||
// A cross-target link keeps the driver.
target.llvmTriple !== toolchain.helper.defaultTarget ||
!lld.includes("-fuse-ld=lld") ||
process.env["SCRIPTC_LINKER"] !== undefined ||
!toolchainEnvironmentCachePolicy().completeArtifacts
)
return null;
try {
const driver = commandPath(toolchain.linker);
if (!/^clang(-\d+)?$/.test(basename(realpathSync(driver)))) return null;
return new LinkerJobs(cache, driver, identity(driver));
} catch {
return null;
}
}
/** The link whose own inputs and output are `paths`, in `stage`. */
link(paths: readonly string[], stage: string): LinkerJobLink {
return {
run: (args, list) => this.run(args, paths, stage, list),
record: (args, output, driverInputs) => this.record(args, paths, stage, output, driverInputs),
};
}
/** The entry key of the link `args` with its own `paths`, or null when
* the arguments name the stage besides those paths. */
private key(args: readonly string[], paths: readonly string[], roots: string[]): string | null {
const index = new Map(paths.map((path, i) => [path, i]));
if (index.size !== paths.length || !paths.every((path) => args.includes(path))) return null;
if (args.some((arg) => !index.has(arg) && underRoots(arg, roots))) return null;
const shape = args.map((arg) => {
const at = index.get(arg);
return at === undefined ? arg : { path: at, extension: extname(arg) };
});
return contentDigest(
JSON.stringify({
schema: 1,
driver: this.driverIdentity,
args: shape,
environment: toolchainEnvironmentFingerprint(),
cwd: process.cwd(),
path: process.env["PATH"] ?? null,
}),
);
}
/**
* Runs the link that the driver arguments `args` describe as its recorded
* job. `paths` are the link's own inputs and output among `args`, which may
* differ from build to build; `stage` is the directory they are in. Without
* an entry, `list` records the job first with the driver's `-v -###`;
* otherwise the caller links through the driver and passes its `-v` output
* to record(). Null when the link has to run through the driver: no job is
* recorded, a recorded path changed, or the job failed.
*/
run(
args: readonly string[],
paths: readonly string[],
stage: string,
list: boolean,
): LinkerJobRun | null {
if (this.failed) return null;
try {
const roots = [resolve(stage), realpathSync(stage)];
const key = this.key(args, paths, roots);
if (key === null) return null;
let entry = this.read(key, paths.length);
if (entry === null && list) {
const result = spawnSync(this.driver, [...args, "-v", "-###"], {
encoding: "utf8",
stdio: "pipe",
});
if (result.error !== undefined || result.status !== 0 || result.signal !== null)
return null;
entry = this.store(key, result.stdout + "\n" + result.stderr, paths, roots);
}
if (entry === null || entry.job === null || !stillMatches(entry.inputs)) return null;
const job = entry.job.map((token) => (typeof token === "number" ? paths[token]! : token));
const result = spawnSync(job[0]!, job.slice(1), {
encoding: "utf8",
stdio: "pipe",
});
if (result.error !== undefined || result.status !== 0 || result.signal !== null) {
this.failed = true;
return null;
}
return { output: result.stdout + "\n" + result.stderr, driverInputs: entry.inputs };
} catch {
return null;
}
}
private read(key: string, paths: number): LinkerJobEntry | null {
const bytes = this.cache.read("linker-job", key, MAX_ENTRY_BYTES);
if (bytes === null) return null;
try {
const entry: unknown = JSON.parse(bytes.toString("utf8"));
return validEntry(entry, paths) ? entry : null;
} catch {
return null;
}
}
/** Records the job of the link `args` from `output`, the `-v` output of
* the driver's successful link, unless an entry exists. `driverInputs`
* are the identities of the paths `output` names, when the caller has
* them. */
record(
args: readonly string[],
paths: readonly string[],
stage: string,
output: string,
driverInputs?: InputIdentity[],
): void {
try {
const roots = [resolve(stage), realpathSync(stage)];
const key = this.key(args, paths, roots);
if (key !== null && this.read(key, paths.length) === null)
this.store(key, output, paths, roots, driverInputs);
} catch {
/* The next link records it. */
}
}
/** Stores the entry for `output`, the driver's `-v` lines and job. */
private store(
key: string,
output: string,
paths: readonly string[],
roots: string[],
driverInputs?: InputIdentity[],
): LinkerJobEntry {
const lines = output.split(/\r?\n/).filter((line) => line.startsWith(' "'));
const job = lines.length === 1 ? this.jobOf(jobArguments(lines[0]!), paths, roots) : null;
const entry: LinkerJobEntry = {
schema: "scriptc.linker-job.v1",
job,
inputs: job === null ? [] : (driverInputs ?? tracePaths(output, true, roots).map(identity)),
};
this.cache.write("linker-job", key, JSON.stringify(entry));
return entry;
}
/** `job` with each of `paths` as its index: null unless the job runs
* ld.lld and names each of the paths once, as one whole argument, and names
* nothing else in the stage. */
private jobOf(
job: string[] | null,
paths: readonly string[],
roots: string[],
): (string | number)[] | null {
if (job === null || job.length === 0) return null;
if (!isAbsolute(job[0]!) || basename(job[0]!) !== "ld.lld") return null;
const index = new Map(paths.map((path, i) => [path, i]));
const used = new Set<number>();
const tokens: (string | number)[] = [];
for (const arg of job) {
const at = index.get(arg);
if (at === undefined) {
if (underRoots(arg, roots)) return null;
tokens.push(arg);
} else {
if (used.has(at)) return null;
used.add(at);
tokens.push(at);
}
}
return used.size === paths.length ? tokens : null;
}
}
+2
View File
@@ -431,6 +431,7 @@ export function prepareNativeExecutable(
(outputKind === "exe" || outputKind === "llvm" || outputKind === "asm") &&
toolchain.target.platform === "linux",
objectAudit: options.sanitize === true,
sanitizeAttributes: options.sanitize === true,
lowRegion: lowRegionTarget(toolchain.target.platform, options.sanitize === true),
functionWorkers: emitWorkers(),
forkPrimitives: emitForkPrimitives(),
@@ -545,6 +546,7 @@ export function prepareNativeLibrary(
pointerBits: toolchain.target.pointerBits,
wasi: toolchain.target.platform === "wasi",
objectAudit: options.sanitize === true,
sanitizeAttributes: options.sanitize === true,
...inlineRcOption(profile.optimization, options.sanitize === true),
});
timing("llvm-emit");
@@ -83,6 +83,8 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
declAdopt: false,
dynForAwait: false,
eventsOnceSignal: false,
mime: false,
utilTiming: false,
dynBuffer: false,
builtinValues: false,
warningReplace: false,
+67 -2
View File
@@ -214,7 +214,7 @@ test("typePatches: malformed keys, ranges, selectors and types are SC8004", () =
"A.c": "string\nnumber",
"A.d": "| undefined",
},
files: {},
patches: {},
},
"other@^1.0.0": [],
},
@@ -225,7 +225,7 @@ test("typePatches: malformed keys, ranges, selectors and types are SC8004", () =
`package.json 'scriptc.typePatches.ms@^banana' has the version range "^banana", which is not a semver range`,
"package.json 'scriptc.typePatches.@types/node@*' patches @types/node, whose declarations ship with scriptc or TypeScript",
"package.json 'scriptc.typePatches.typescript@5' patches typescript, whose declarations ship with scriptc or TypeScript",
"package.json 'scriptc.typePatches.lib@^1.0.0.files' is not a known type patch setting",
"package.json 'scriptc.typePatches.lib@^1.0.0.patches' is not a known type patch setting",
"package.json 'scriptc.typePatches.lib@^1.0.0.declarations.1bad' is not a selector: it must start with a declared name, 'export=' or 'default'",
"package.json 'scriptc.typePatches.lib@^1.0.0.declarations.A.b' must map to a type string, not 3",
"package.json 'scriptc.typePatches.lib@^1.0.0.declarations.A.c' has a type that must be one line",
@@ -241,3 +241,68 @@ test("typePatches: malformed keys, ranges, selectors and types are SC8004", () =
});
expect(partial.typePatches[0]!.declarations.map((d) => d.selector)).toEqual(["A.d"]);
});
test("typePatches: files and types name local declaration files", () => {
const root = mkdtempSync(join(tmpdir(), "scriptc-project-config-"));
try {
mkdirSync(join(root, "patches"));
writeFileSync(join(root, "patches", "entry.d.ts"), "export {};\n");
const text = JSON.stringify({
scriptc: {
typePatches: {
"lib@^1.0.0": {
files: {
"dist/entry.d.ts": "./patches/entry.d.ts",
"@types/lib/index.d.ts": "patches/entry.d.ts",
"dist/index.js": "./patches/entry.d.ts",
"../outside.d.ts": "./patches/entry.d.ts",
"dist/other.d.ts": "./patches/missing.d.ts",
},
types: "./patches/entry.d.ts",
},
"other@^1.0.0": { types: "./patches/entry.ts" },
},
},
});
const c = parseProjectConfig(join(root, "package.json"), text);
expect(c.diagnostics.map((d) => d.message)).toEqual([
"package.json 'scriptc.typePatches.lib@^1.0.0.files.dist/index.js' must name a declaration file inside the package",
"package.json 'scriptc.typePatches.lib@^1.0.0.files.../outside.d.ts' must name a declaration file inside the package",
`package.json 'scriptc.typePatches.lib@^1.0.0.files.dist/other.d.ts' names "./patches/missing.d.ts", which does not exist`,
`package.json 'scriptc.typePatches.other@^1.0.0.types' names "./patches/entry.ts", which is not a declaration file`,
]);
const entry = c.typePatches.find((e) => e.pkg === "lib")!;
expect([...entry.files]).toEqual([
["dist/entry.d.ts", join(root, "patches/entry.d.ts")],
["@types/lib/index.d.ts", join(root, "patches/entry.d.ts")],
]);
expect(entry.types).toBe(join(root, "patches/entry.d.ts"));
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("readProjectConfig checks local type patch files again for an unchanged package.json", () => {
const root = mkdtempSync(join(tmpdir(), "scriptc-project-config-"));
try {
writeFileSync(
join(root, "package.json"),
JSON.stringify({ scriptc: { typePatches: { "lib@^1.0.0": { types: "./lib.d.ts" } } } }),
);
const entry = join(root, "main.ts");
const missing = readProjectConfig(entry);
expect(missing.diagnostics.map((d) => d.message)).toEqual([
`package.json 'scriptc.typePatches.lib@^1.0.0.types' names "./lib.d.ts", which does not exist`,
]);
expect(readProjectConfig(entry)).toBe(missing);
writeFileSync(join(root, "lib.d.ts"), "export {};\n");
const created = readProjectConfig(entry);
expect(created.diagnostics).toEqual([]);
expect(created.typePatches[0]!.types).toBe(join(root, "lib.d.ts"));
expect(readProjectConfig(entry)).toBe(created);
rmSync(join(root, "lib.d.ts"));
expect(readProjectConfig(entry).diagnostics.map((d) => d.code)).toEqual(["SC8004"]);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
+79 -3
View File
@@ -27,7 +27,7 @@
* to their own readers. */
import { dirname, join, resolve } from "node:path";
import { projectConfigDiag, type ScrDiagnostic } from "./diagnostics/diagnostic.js";
import { trackedReadFile } from "./frontend/input-tracker.js";
import { trackedFileExists, trackedReadFile } from "./frontend/input-tracker.js";
import { parseSelector, parseTypeText } from "./frontend/type-patches/selector.js";
import { isValidRange } from "./frontend/type-patches/semver.js";
@@ -79,6 +79,13 @@ export interface TypePatchEntry {
pkg: string;
range: string;
declarations: TypePatchDeclaration[];
/** `files`: declaration files of the package (or of its `@types/` twin,
* written `@types/<name>/<path>`) replaced by local files, by
* package-relative path. The local paths are absolute. */
files: Map<string, string>;
/** `types`: a local declaration file that becomes the package's
* declaration entry (absolute), or null. */
types: string | null;
/** Offset of the key in the manifest. */
offset: number;
}
@@ -94,6 +101,10 @@ export interface ProjectConfig {
wasm: string[];
/** `scriptc.typePatches`, in manifest order. */
typePatches: TypePatchEntry[];
/** The local files the configuration checked for existence, with the
* answer. A configuration read again from an unchanged package.json is
* reused only while every answer still holds. */
localFiles: { path: string; exists: boolean }[];
/** SC8004 for every unknown key or value. */
diagnostics: ScrDiagnostic[];
}
@@ -146,6 +157,7 @@ export function emptyProjectConfig(): ProjectConfig {
nodeIslands: defaultNodeIslands(),
wasm: [],
typePatches: [],
localFiles: [],
diagnostics: [],
};
}
@@ -341,7 +353,8 @@ export function parseProjectConfig(manifest: string, text: string): ProjectConfi
return config;
}
const TYPE_PATCH_KEYS = ["declarations", "note"];
const TYPE_PATCH_KEYS = ["declarations", "files", "types", "note"];
const DECLARATION_FILE = /\.d\.[cm]?ts$/;
/** Packages whose declarations ship with scriptc or TypeScript: a wrong
* one is a bug to report, not a project-local patch. */
const UNPATCHABLE = /^(?:@types\/node|typescript|@typescript\/.*)$/;
@@ -404,8 +417,66 @@ function readTypePatches(
pkg,
range,
declarations: [],
files: new Map(),
types: null,
offset: locate.offsetOf(["scriptc", "typePatches", key]),
};
const base = dirname(config.manifest!);
const localFile = (path: string[], v: unknown): string | null => {
if (typeof v !== "string" || v === "") {
problem(
path,
`must be the path of a local declaration file, not ${shown(v)}`,
`write "./type-patches/${pkg}.d.ts"`,
);
return null;
}
const abs = resolve(base, v);
if (!DECLARATION_FILE.test(abs)) {
problem(
path,
`names "${v}", which is not a declaration file`,
"use a .d.ts, .d.mts or .d.cts file",
);
return null;
}
const exists = trackedFileExists(abs);
config.localFiles.push({ path: abs, exists });
if (!exists) {
problem(
path,
`names "${v}", which does not exist`,
"give the path relative to this package.json",
);
return null;
}
return abs;
};
const files = value["files"];
if (files !== undefined && !isRecord(files)) {
problem(
["typePatches", key, "files"],
`must be an object mapping declaration files of the package to local files, not ${shown(files)}`,
`write "files": { "dist/index.d.ts": "./type-patches/${pkg}.d.ts" }`,
);
} else if (files !== undefined) {
for (const target of Object.keys(files)) {
const path = ["typePatches", key, "files", target];
const rel = target.replace(/^\.\//, "");
if (!DECLARATION_FILE.test(rel) || rel.startsWith("/") || rel.split("/").includes("..")) {
problem(
path,
"must name a declaration file inside the package",
`write a package-relative .d.ts path such as "dist/index.d.ts", or "@types/<name>/index.d.ts" for its @types package`,
);
continue;
}
const abs = localFile(path, files[target]);
if (abs !== null) entry.files.set(rel, abs);
}
}
if (value["types"] !== undefined)
entry.types = localFile(["typePatches", key, "types"], value["types"]);
let valid = true;
for (const k of Object.keys(value)) {
if (!TYPE_PATCH_KEYS.includes(k)) {
@@ -482,7 +553,12 @@ export function readProjectConfig(entryPath: string): ProjectConfig {
const manifest = join(dir, "package.json");
const text = trackedReadFile(manifest);
if (text !== null) {
if (cachedConfig !== null && cachedManifest === manifest && cachedText === text)
if (
cachedConfig !== null &&
cachedManifest === manifest &&
cachedText === text &&
cachedConfig.localFiles.every((file) => trackedFileExists(file.path) === file.exists)
)
return cachedConfig;
// A manifest without a `scriptc` key (none spelled out, none behind a
// \u escape) has the empty configuration; builds of such projects
+42
View File
@@ -2482,12 +2482,36 @@
"status": "static",
"note": "recognized module (bare and node:-prefixed specifiers)"
},
{
"id": "node-builtin.util._extend",
"kind": "node-builtin",
"name": "util._extend",
"status": "static"
},
{
"id": "node-builtin.util.aborted",
"kind": "node-builtin",
"name": "util.aborted",
"status": "static"
},
{
"id": "node-builtin.util.callbackify",
"kind": "node-builtin",
"name": "util.callbackify",
"status": "static"
},
{
"id": "node-builtin.util.convertProcessSignalToExitCode",
"kind": "node-builtin",
"name": "util.convertProcessSignalToExitCode",
"status": "static"
},
{
"id": "node-builtin.util.debounce",
"kind": "node-builtin",
"name": "util.debounce",
"status": "static"
},
{
"id": "node-builtin.util.debug",
"kind": "node-builtin",
@@ -2506,6 +2530,12 @@
"name": "util.deprecate",
"status": "static"
},
{
"id": "node-builtin.util.diff",
"kind": "node-builtin",
"name": "util.diff",
"status": "static"
},
{
"id": "node-builtin.util.getSystemErrorMap",
"kind": "node-builtin",
@@ -2530,6 +2560,12 @@
"name": "util.inherits",
"status": "static"
},
{
"id": "node-builtin.util.isArray",
"kind": "node-builtin",
"name": "util.isArray",
"status": "static"
},
{
"id": "node-builtin.util.isDeepStrictEqual",
"kind": "node-builtin",
@@ -2568,6 +2604,12 @@
"name": "util.styleText",
"status": "static"
},
{
"id": "node-builtin.util.throttle",
"kind": "node-builtin",
"name": "util.throttle",
"status": "static"
},
{
"id": "node-builtin.util.toUSVString",
"kind": "node-builtin",
@@ -136,3 +136,111 @@ test("worker programs poll counted loops over typed arrays between blocks of ite
await rm(dir, { recursive: true, force: true });
}
});
test("worker programs poll no loop whose proven ranges bound its runs inside another loop", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-bounded-loop-polls-"));
try {
const entry = join(dir, "bounded.ts");
await writeFile(
entry,
[
'import { isMainThread, Worker } from "node:worker_threads";',
"const LIMIT = 50;",
"export function rows(cr: Float64Array, n: number): number {",
" let t = 0;",
" for (let r = 0; r < n; r++)",
" for (let i = 0; i < 8; i += 2) {",
" const c = cr[i]!;",
" for (let j = 0; j < LIMIT; j++) {",
" t = (t * 31 + j + c) % 1000003;",
" if (t === 7) break;",
" }",
" }",
" return t;",
"}",
"export function open(cr: Float64Array, n: number, k: number): number {",
" let t = 0;",
" for (let r = 0; r < n; r++)",
" for (let i = 0; i < 8; i += 2) {",
" const c = cr[i]!;",
" for (let j = 0; j < k; j++) {",
" t = (t * 31 + j + c) % 1000003;",
" if (t === 7) break;",
" }",
" }",
" return t;",
"}",
"export function wide(n: number): number {",
" let t = 0;",
" for (let r = 0; r < n; r++) for (let j = 0; j < 5000; j++) t = (t * 31 + j) % 1000003;",
" return t;",
"}",
"export function exact(n: number): number {",
" let t = 0;",
" for (let r = 0; r < n; r++)",
" for (let j = 9007199254740900; j < 9007199254740950; j++) t = (t + (j % 7)) % 1000003;",
" return t;",
"}",
"export function stalled(n: number): number {",
" let t = 0;",
" for (let r = 0; r < n; r++)",
" for (let j = 9007199254740990; j < 9007199254741002; j++) t = (t + (j % 7)) % 1000003;",
" return t;",
"}",
"export function single(): number {",
" let t = 0;",
" for (let j = 0; j < 50; j++) t = (t * 31 + j) % 1000003;",
" return t;",
"}",
"const cr = new Float64Array(8).fill(0.5);",
'const k = Number(process.argv[2] ?? "50");',
"console.log(rows(cr, 3), open(cr, 3, k), wide(2), exact(2), stalled(0), single());",
"if (!isMainThread) new Worker(new URL(import.meta.url));",
].join("\n"),
);
const outPath = join(dir, "bounded.ir.json");
const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" });
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8")));
const body = (name: string): string =>
[
...ll.matchAll(
new RegExp(
`define internal [^\\n]+ @sc_b?f_${name}(?:\\.i32)?\\([^\\n]*\\n([\\s\\S]*?)\\n\\}`,
"g",
),
),
]
.map((match) => match[1]!)
.join("\n");
const polls = (text: string): number => text.match(/@scr_exc_flag/g)?.length ?? 0;
// The i and j loops of `rows` run at most 4 × (1 + 50) bodies: only the
// r loop polls, and the i loop over the typed array is not strip-mined.
const rows = body("rows");
expect(rows).not.toMatch(/^loop\.poll\d+:$/m);
expect(polls(rows)).toBeGreaterThan(0);
// With a limit known only at run time the inner loops keep their polls.
// The innermost one is strip-mined, and a block that ends at the limit
// leaves it without polling: only a block that ends short of it polls.
const open = body("open");
expect(polls(open)).toBeGreaterThan(polls(rows));
expect(open).toMatch(/^loop\.poll\d+:$/m);
const ends = [...open.matchAll(/^loop\.end\d+:$[\s\S]*?^\S/gm)].map((match) => match[0]);
expect(ends.length).toBeGreaterThan(0);
for (const end of ends) expect(end).toMatch(/br i1 %\w+, label %loop\.poll\d+, label %/);
// A run of up to 16384 iterations is one block, polled as it leaves when
// it took more than 64.
expect(open).toMatch(/^loop\.poll\.run\d+:$/m);
// Steps of 1 below 2^53 are exact, so 50 runs near it are bounded. From
// 2^53, `j + 1 === j`: a counter ranging up to it can run without end
// and keeps its poll.
expect(polls(body("exact"))).toBe(polls(body("single")));
expect(polls(body("stalled"))).toBeGreaterThan(polls(body("exact")));
// A nest over the bound, or a loop no other loop encloses, polls.
expect(polls(body("wide"))).toBeGreaterThan(0);
expect(body("wide")).toMatch(/^loop\.poll\d+:$/m);
expect(polls(body("single"))).toBeGreaterThan(0);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
@@ -50,7 +50,7 @@ console.log(sum);
expect(definition(llvm, "sc_f_range")).not.toContain("@scr_coro_yield_f64(");
expect(llvm).not.toContain("@scr_gen_take_out_f64(");
expect(llvm).toContain("%ScrGenHead = type { i64, ptr, ptr, i32, %ScrGenSlot }");
expect(llvm).toContain("attributes #1 = { sanitize_address presplitcoroutine }");
expect(llvm).toContain("attributes #1 = { presplitcoroutine }");
expect(definition(llvm, "sc_gs_range")).toContain("call ptr @scr_gen_new_stackless(");
expect(llvm).not.toContain("@scr_gen_yield_f64(");
});
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7520-util-format-runtime.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7521-util-format-runtime.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7522-promisify-untyped.cjs"
],
"diags": []
}

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