mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Merge base branch
This commit is contained in:
@@ -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 }}"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"] },
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -168,6 +168,7 @@ export class NativeCache {
|
||||
"sanitizer",
|
||||
"executable",
|
||||
"kept",
|
||||
"linker-job",
|
||||
"runtime-pack",
|
||||
"binary",
|
||||
"dsym",
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(");
|
||||
});
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7520-util-format-runtime.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7521-util-format-runtime.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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
Reference in New Issue
Block a user