mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Merge main
This commit is contained in:
+46
-28
@@ -4,21 +4,23 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
# Nightly full macOS sweep, including on days without a push to main.
|
||||
# Nightly full macOS sweep. Pushes to main run no macOS jobs, and pull requests run them only when they change
|
||||
# darwin code (scripts/ci-lanes.mjs).
|
||||
schedule:
|
||||
- cron: "17 7 * * *"
|
||||
# A manual run; macos_full runs the complete suite on macOS as the nightly sweep does (for example on a
|
||||
# release candidate commit).
|
||||
# A manual run, for example on a release candidate commit. It runs every lane; macos_suite selects the complete
|
||||
# suite on macOS, as the nightly sweep runs it, or the darwin suite that pull requests run.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
macos_full:
|
||||
description: Run the complete test suite on macOS
|
||||
type: boolean
|
||||
default: false
|
||||
macos_suite:
|
||||
description: The test suite to run on macOS
|
||||
type: choice
|
||||
options: [full, darwin]
|
||||
default: full
|
||||
|
||||
concurrency:
|
||||
# A new push to a pull request cancels its previous run. Every push to main runs to completion in a group of
|
||||
# its own commit, so each merged commit gets a full result (releases require a green run on their commit). The
|
||||
# its own commit, so each merged commit gets a result (releases require a green run on their commit). The
|
||||
# nightly sweep and manual runs neither cancel nor are cancelled by main pushes.
|
||||
group: ci-${{ github.event_name == 'schedule' && 'schedule' || github.event_name == 'workflow_dispatch' && format('dispatch-{0}', github.run_id) || github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
@@ -59,7 +61,8 @@ jobs:
|
||||
NODE
|
||||
|
||||
# Which expensive lanes a pull request needs (scripts/ci-lanes.mjs, run from the base commit against the pull
|
||||
# request's diff, so a pull request cannot change its own selection). Pushes and the nightly sweep run every lane.
|
||||
# request's diff, so a pull request cannot change its own selection). Pushes to main run every lane except macos,
|
||||
# whose jobs the nightly sweep runs on main. The nightly sweep and manual runs run every lane.
|
||||
lanes:
|
||||
needs: validation_scope
|
||||
if: ${{ needs.validation_scope.outputs.full == 'true' }}
|
||||
@@ -86,7 +89,9 @@ jobs:
|
||||
HEAD: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
every() { printf 'linux=true\nmacos=true\neffects=true\nbootstrap=true\n' | tee -a "$GITHUB_OUTPUT"; }
|
||||
if [ "$EVENT" != pull_request ]; then
|
||||
if [ "$EVENT" = push ]; then
|
||||
printf 'linux=true\nmacos=false\neffects=true\nbootstrap=true\n' | tee -a "$GITHUB_OUTPUT"
|
||||
elif [ "$EVENT" != pull_request ]; then
|
||||
every
|
||||
elif git show "$BASE:scripts/ci-lanes.mjs" > "$RUNNER_TEMP/ci-lanes.mjs" 2> /dev/null; then
|
||||
node "$RUNNER_TEMP/ci-lanes.mjs" --base "$BASE" --head "$HEAD" --github-output
|
||||
@@ -219,25 +224,25 @@ jobs:
|
||||
tests_macos:
|
||||
needs: [validation_scope, lanes, native_macos]
|
||||
if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.macos == 'true' }}
|
||||
name: test (macOS, ${{ matrix.flavor }}, ${{ (github.event_name != 'schedule' && !inputs.macos_full) && 'darwin ' || '' }}${{ matrix.shard }}/${{ (github.event_name != 'schedule' && !inputs.macos_full) && '3' || '10' }})
|
||||
name: test (macOS, ${{ matrix.flavor }}, ${{ (github.event_name != 'schedule' && inputs.macos_suite != 'full') && 'darwin ' || '' }}${{ matrix.shard }}/${{ (github.event_name != 'schedule' && inputs.macos_suite != 'full') && '3' || '10' }})
|
||||
# arm64 macOS: same clang/libc the differential + number-formatting
|
||||
# oracles were verified against. Pull requests and pushes to main run the
|
||||
# darwin suite (scripts/ci-test-plan.mjs) in three slices; the nightly
|
||||
# schedule and manual runs with macos_full run the complete suite in ten.
|
||||
# The complete suite on every push needed more macOS runners than the
|
||||
# repository has.
|
||||
# oracles were verified against. Pull requests that change darwin code and
|
||||
# manual runs with macos_suite set to darwin run the darwin suite
|
||||
# (scripts/ci-test-plan.mjs) in three slices; the nightly schedule and
|
||||
# other manual runs run the complete suite in ten. The complete suite on
|
||||
# every push needed more macOS runners than the repository has.
|
||||
runs-on: macos-15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
flavor: [plain, san]
|
||||
shard: ${{ fromJSON((github.event_name != 'schedule' && !inputs.macos_full) && '[1, 2, 3]' || '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]') }}
|
||||
shard: ${{ fromJSON((github.event_name != 'schedule' && inputs.macos_suite != 'full') && '[1, 2, 3]' || '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]') }}
|
||||
env:
|
||||
# See the Linux tests job for the worker cap and case partitioning.
|
||||
SCRIPTC_TEST_WORKERS: "4"
|
||||
SCRIPTC_SAN: ${{ matrix.flavor == 'san' && '1' || '' }}
|
||||
SCRIPTC_TEST_SHARD: ${{ matrix.shard }}/${{ (github.event_name != 'schedule' && !inputs.macos_full) && '3' || '10' }}
|
||||
SCRIPTC_CI_SUITE: ${{ (github.event_name != 'schedule' && !inputs.macos_full) && 'darwin' || 'full' }}
|
||||
SCRIPTC_TEST_SHARD: ${{ matrix.shard }}/${{ (github.event_name != 'schedule' && inputs.macos_suite != 'full') && '3' || '10' }}
|
||||
SCRIPTC_CI_SUITE: ${{ (github.event_name != 'schedule' && inputs.macos_suite != 'full') && 'darwin' || 'full' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
@@ -254,7 +259,7 @@ jobs:
|
||||
path: ${{ runner.temp }}
|
||||
- run: tar -xzf "$RUNNER_TEMP/native-darwin-arm64.tar.gz"
|
||||
- run: pnpm build
|
||||
- name: Tests (${{ (github.event_name != 'schedule' && !inputs.macos_full) && 'darwin suite' || 'corpus and integration suites' }})
|
||||
- name: Tests (${{ (github.event_name != 'schedule' && inputs.macos_suite != 'full') && 'darwin suite' || 'corpus and integration suites' }})
|
||||
run: node scripts/ci-test.mjs
|
||||
|
||||
native_linux:
|
||||
@@ -868,14 +873,25 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runtime: runtime-ios-arm64
|
||||
runner: macos-15
|
||||
- runtime: runtime-ios-simulator-arm64
|
||||
runner: macos-15
|
||||
- runtime: runtime-android-arm64
|
||||
runner: ubuntu-24.04
|
||||
runs-on: ${{ matrix.runner }}
|
||||
runtime: [runtime-android-arm64]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/mobile-runtime-pack
|
||||
with:
|
||||
runtime: ${{ matrix.runtime }}
|
||||
|
||||
# The iOS runtime packs build on macOS runners, so they run in the macos lane.
|
||||
mobile_runtime_packs_ios:
|
||||
needs: [validation_scope, lanes]
|
||||
if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.macos == 'true' }}
|
||||
name: test (${{ matrix.runtime }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runtime: [runtime-ios-arm64, runtime-ios-simulator-arm64]
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -908,6 +924,7 @@ jobs:
|
||||
windows_cli,
|
||||
llvm_artifacts_windows_x64,
|
||||
mobile_runtime_packs,
|
||||
mobile_runtime_packs_ios,
|
||||
]
|
||||
# Run even after failed or cancelled lanes: skipped required jobs allow merging.
|
||||
if: ${{ always() }}
|
||||
@@ -939,6 +956,7 @@ jobs:
|
||||
lane windows_cli "${{ needs.windows_cli.result }}" "${{ needs.lanes.outputs.linux }}"
|
||||
lane llvm_artifacts_windows_x64 "${{ needs.llvm_artifacts_windows_x64.result }}" "${{ needs.lanes.outputs.linux }}"
|
||||
lane mobile_runtime_packs "${{ needs.mobile_runtime_packs.result }}" "${{ needs.lanes.outputs.linux }}"
|
||||
lane mobile_runtime_packs_ios "${{ needs.mobile_runtime_packs_ios.result }}" "${{ needs.lanes.outputs.macos }}"
|
||||
;;
|
||||
false)
|
||||
echo "::notice::Full CI is deferred until this PR becomes the lowest unmerged layer. Merge the stack from the bottom up."
|
||||
|
||||
@@ -219,6 +219,12 @@ jobs:
|
||||
Move-Item "$expanded\clang+llvm-22.1.8-x86_64-pc-windows-msvc" $llvm
|
||||
echo "LLVM_DIR=$llvm\lib\cmake\llvm" >> $env:GITHUB_ENV
|
||||
echo "$llvm\bin" >> $env:GITHUB_PATH
|
||||
# A musl CLI ships tsc-ts's musl executable, and these jobs run on glibc
|
||||
# runners, where pnpm installs only the glibc platform packages.
|
||||
- name: Let pnpm install musl platform packages
|
||||
if: endsWith(matrix.cli_target, '-musl')
|
||||
run: |
|
||||
printf 'supportedArchitectures:\n libc: [current, musl]\n' >> pnpm-workspace.yaml
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Build native packages (POSIX)
|
||||
if: matrix.platform != 'windows-x64'
|
||||
|
||||
+67
-2
@@ -6,6 +6,73 @@ All notable changes to scriptc will be documented in this file.
|
||||
|
||||
<!-- release:start -->
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- **tsc-ts is the default TypeScript 7 checker** on Linux and macOS. scriptc installs it with the compiler (`tsc-ts` 0.0.5 and its platform package for the host). Programs get the same diagnostics and generated code as with tsgo, and compiling takes less checker memory. `SCRIPTC_CHECKER=tsgo` selects tsgo, which stays the default on Windows. A build cache entry made with one checker isn't reused with the other.
|
||||
- **`--npm-static=auto`, the default, compiles more packages:** packages typed by their DefinitelyTyped twin (`@types/<name>`), and untyped dependencies that another package's JavaScript imports. A program whose dependencies were refused before can now compile them, and a twin whose declarations disagree with the package's JavaScript fails the build at the use. Use `--npm-static <pkg,…>` to name the packages to compile.
|
||||
- **Program type errors are reported as type errors** instead of as refusals of the npm package that contains them.
|
||||
|
||||
### Highlights
|
||||
|
||||
- **WebAssembly:** builds translate every WebAssembly module whose bytes they can see into native code, and `WebAssembly.compile`, `instantiate`, `validate`, `new WebAssembly.Module` and `new WebAssembly.Instance` run them. There is no interpreter. `WebAssembly.Memory`, `Table` and `Global`, `Module.imports`, `Module.exports` and `Module.customSections`, and function references from tables are supported, and modules can be sent to workers. `scriptc coverage` lists the modules a program loads, with `--wasm <file.wasm>` for modules loaded from paths the sources don't spell out.
|
||||
- **Async functions that suspend only at their `await`s run as stackless coroutines** instead of on fibers with their own stack, so a suspended call keeps only the values live across its awaits.
|
||||
- **Discriminated unions of records are the record pointer itself,** with no box around each value.
|
||||
- **The dynamic tier computes with NaN-boxed values:** numbers, booleans, `undefined` and `null` between operators no longer allocate. `scriptc coverage --explain-dynamic` lists the functions that run in the dynamic tier and why, and `--tier=static-only` keeps a program fully static.
|
||||
- **npm packages:** esbuild's bundled CommonJS chunks, CommonJS cycles and lazily initialized ES modules compile statically, as do classic CommonJS packages (late and JSON `require`s, require cycles), class hierarchies across a package's modules, and the packages unblocked by type-only imports, `@types` packages, `punycode` and `constants`. chalk 5 and the jose root entry compile.
|
||||
|
||||
### Features
|
||||
|
||||
- **`@scriptc/experimental` adds `ThreadSlots`,** one value per key and thread on shared objects, read without a lock.
|
||||
- **`node:test` adds `mock.timers`, `getTestContext`, `t.assert.snapshot` and `t.assert.fileSnapshot`,** the `TestContext` values `filePath`, `attempt`, `passed`, `workerId` and `tags`, module-level `before` and `after` hooks, the `SuiteContext` and hook contexts, and unnamed `test` and `describe` calls. A timed-out test cancels its running subtests.
|
||||
- **`node:worker_threads` adds `BroadcastChannel`,** `setEnvironmentData`/`getEnvironmentData`, `receiveMessageOnPort`, `markAsUntransferable`, `markAsUncloneable`, `threadName`, `resourceLimits.stackSizeMb`, the `stdout`/`stderr`/`stdin` options and streams, and `worker[Symbol.asyncDispose]`.
|
||||
- **`node:net` adds `SocketAddress` and `BlockList`,** and `net.connect` resolves host names.
|
||||
- **`node:http` pools keep-alive sockets in `Agent`s,** uses `globalAgent` for default requests, reads `http.request` options at run time, and speaks HTTP over Unix domain sockets. `fetch` tunnels through `HTTP_PROXY`/`HTTPS_PROXY` with `CONNECT`.
|
||||
- **`DisposableStack`, `Float16Array`, `Error.isError`, `Promise.try` and the regular expression `v` flag** are supported.
|
||||
- **Iterator helpers** run on generators, `Map` and `Set` iterators, string and typed array iterators, `Iterator.from` and untyped iterators.
|
||||
- **`Object.groupBy` and `Map.groupBy`** group Sets, Maps and generators.
|
||||
- **`v8`, `stream/web`, `dns/promises`, `EvalError` and `graceful-fs`** are available, and `Temporal` is reported as absent, as in Node 24.
|
||||
- **`util.promisify`** works over the crypto callback functions, dns lookups, `child_process.exec`'s callback forms and callback-style functions.
|
||||
- **`events.once`** takes an `AbortSignal` and works over streams.
|
||||
- **`AsyncLocalStorage` stores** carry into timer, immediate, `process.nextTick` and microtask callbacks.
|
||||
- **JavaScript classes compile like TypeScript classes** when their layout or JSDoc types allow it.
|
||||
- **Classes that extend `EventEmitter` or a stream class can be used as values,** and so can `EventEmitter`, the stream classes and the error classes themselves. Constructor functions can inherit from them.
|
||||
- **Class instances can be frozen and their fields redefined.**
|
||||
- **Method overrides may differ from the method they override** in parameter and result types, as long as each value converts.
|
||||
- **Library profiles add the `cstring` class.**
|
||||
- **An experimental young-object nursery** for compiled classes and records is available at compile time with `SCRIPTC_YOUNG_NURSERY=1`.
|
||||
|
||||
### Performance
|
||||
|
||||
- **Dense arrays have head room,** so `shift`, `splice(0, n)` and `unshift` runs cost amortized O(1), and `pop` of a packed array is inline.
|
||||
- **Index loops over arrays and typed arrays are versioned on one bounds guard,** with stable lengths proven and typed-array memory tagged for alias analysis.
|
||||
- **Retain and release pairs are removed** through private-array precision and last-use moves, borrowed string slices go into stack buffers, and records, tuples and spread updates stay in frame storage.
|
||||
- **SHA-384 and SHA-512 use AVX2, AVX-512VL or the arm64 SHA-512 instructions,** and MD5's block function is unrolled.
|
||||
- **String character tests, optional-string `switch`es, `String.fromCharCode` and long-string `length` are faster,** and `Buffer.write` widens ASCII to UTF-16 sixteen bytes per step.
|
||||
- **The cycle collector walks dead cycles once,** frees large dead cycle sets in address order and no longer creates self-capture cycles on the release fast path.
|
||||
- **Workers return their memory** while the event loop waits and when they finish, and default-region memory is committed as it is used.
|
||||
- **Lifetime regions carry across `await`,** and freed region chunks are reused.
|
||||
- **Large closure environments compile in linear time,** and two frontend scans no longer repeat.
|
||||
- **Programs keep stack frames only when they can read `stack`.** Before, a computed-key read on a dynamic value kept frames in every function; tsc-ts's own check is back to 0.3.6's speed.
|
||||
- **The child-process runtime unit links only into programs that spawn.**
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Node 24 parity:** `util.format`, `util.types` and `util.inspect`, `URLSearchParams`, `querystring` (null-prototype results), `string_decoder`, `os`, `events` and `timers/promises` match Node 24. Class instances, iterators, iterator helper objects and holes in arrays print as Node prints them, and `%d`/`%i` format dynamic arguments as Node does.
|
||||
- **Silent wrong output is fixed** for object identity, `Object.prototype` values, `Date.parse` (V8's date grammar), stored builtins, constructor returns, `arr && arr[0]`, ES5 class names, builtin function values, uninitialized `let` writes, module-export `this` and `1 ** Infinity`.
|
||||
- **Strings compare in UTF-16 code-unit order.**
|
||||
- **Dates format with Node's local time zone names,** convert to local time once for their local getters, and follow `TZ` changes. `Date.prototype.toJSON` gives `null` for invalid dates.
|
||||
- **Errors:** thrown Maps, Sets, typed arrays, functions and arrays keep their identity. Errors list their own name where they were created, errors held as `any` have their own `stack` accessor and convert with `Error.prototype.toString`, and errors that cross into dynamic code carry Node's error properties.
|
||||
- **`matchAll` rows and `exec` results have `index`, `input` and `groups`,** and named groups keep their null prototype through optional chains.
|
||||
- **Unhandled rejections are reported after each timer and immediate callback,** and async generator completions settle on Node's microtask turns.
|
||||
- **Missing `Record` and index-signature keys read as `undefined`,** and generic functions that read a missing element return `undefined`.
|
||||
- **Sockets:** `'data'` listeners get strings after `setEncoding`, socket and stream chunks are Buffers, and an idle pooled http socket that receives data is destroyed.
|
||||
- **Diagnostic code frames window long source lines.**
|
||||
- **npm crashes, internal errors and false TDZ errors** found while compiling popular packages are fixed.
|
||||
|
||||
<!-- release:end -->
|
||||
|
||||
## 0.3.6
|
||||
|
||||
### Features
|
||||
@@ -38,8 +105,6 @@ All notable changes to scriptc will be documented in this file.
|
||||
- **`Object.setPrototypeOf` returns primitive receivers,** and `createHmac` without a key throws Node's error.
|
||||
- **Boxed records that hold an async function link the dynamic promise unit.**
|
||||
|
||||
<!-- release:end -->
|
||||
|
||||
## 0.3.5
|
||||
|
||||
### Features
|
||||
|
||||
@@ -30,6 +30,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains |
|
||||
| `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` |
|
||||
| `log-lines` | cpu | 200k immediate `console.log`/`process.stdout.write` lines into the runner's pipe |
|
||||
| `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts |
|
||||
| `log-summary`, `inventory-report` | cpu | the multi-module applications from `benchmarks/builds`, on 300k-line inputs |
|
||||
| `cli-config` | startup | a `deploy-plan` CLI: read a ~110 KB JSON manifest, validate, interpolate env values, order 80 services into dependency waves, print a summary |
|
||||
| `http-api` | server | a `node:http` JSON API: routing, query parsing, a seeded catalog, per-cart state, typed JSON bodies, `ETag`/`If-None-Match`, auth and request-id headers |
|
||||
|
||||
@@ -231,6 +231,13 @@
|
||||
"4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "unicode-lexer",
|
||||
"entry": "workloads/unicode-lexer.ts",
|
||||
"args": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "log-summary",
|
||||
"entry": "../builds/log-summary/main.ts",
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// A lexer over a large bundled module whose comments and strings are in
|
||||
// several languages: line starts, tokens by kind and comment text, read
|
||||
// with charCodeAt as hand-written scanners do. Each pass relexes the whole
|
||||
// text, as a bundler or an editor does after a change.
|
||||
const comments = [
|
||||
"// Berechnet die Größe des Puffers für übergroße Einträge",
|
||||
"// Calcule la taille maximale avant la prochaine itération",
|
||||
"// バッファのサイズを計算します",
|
||||
"/* 计算缓冲区大小,然后返回结果 */",
|
||||
"// Returns the cached value (fast path) — see the note above",
|
||||
"/** Résumé: the état of the café's ñandú registry */",
|
||||
];
|
||||
const strings = ['"Grüße"', '"naïve façade"', '"東京"', '"ok ✓"', '"plain ascii"', "'mañana'"];
|
||||
const names = ["buffer", "größe", "index", "resultat", "count", "value", "entry", "table"];
|
||||
|
||||
let seed = 11;
|
||||
function random(): number {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed / 4294967296;
|
||||
}
|
||||
function pick(list: readonly string[]): string {
|
||||
return list[Math.floor(random() * list.length)]!;
|
||||
}
|
||||
|
||||
function makeSource(lines: number): string {
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < lines; i++) {
|
||||
const r = random();
|
||||
if (r < 0.2) out.push(pick(comments));
|
||||
else if (r < 0.6) out.push(` const ${pick(names)}${i} = ${pick(names)}(${Math.floor(random() * 1000)}, ${pick(strings)});`);
|
||||
else if (r < 0.85) out.push(` if (${pick(names)} >= ${i % 97}) return ${pick(names)}.${pick(names)} + 1;`);
|
||||
else out.push(`function ${pick(names)}${i}(a, b) { return a * b - ${i}; }`);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
interface Counts {
|
||||
lines: number;
|
||||
identifiers: number;
|
||||
numbers: number;
|
||||
strings: number;
|
||||
comments: number;
|
||||
commentChars: number;
|
||||
punctuation: number;
|
||||
}
|
||||
|
||||
function isIdentifierStart(ch: number): boolean {
|
||||
return (ch >= 97 && ch <= 122) || (ch >= 65 && ch <= 90) || ch === 95 || ch === 36 || ch > 127;
|
||||
}
|
||||
|
||||
function isIdentifierPart(ch: number): boolean {
|
||||
return isIdentifierStart(ch) || (ch >= 48 && ch <= 57);
|
||||
}
|
||||
|
||||
function lex(text: string): Counts {
|
||||
const counts: Counts = { lines: 1, identifiers: 0, numbers: 0, strings: 0, comments: 0, commentChars: 0, punctuation: 0 };
|
||||
let pos = 0;
|
||||
const end = text.length;
|
||||
while (pos < end) {
|
||||
const ch = text.charCodeAt(pos);
|
||||
if (ch === 10) {
|
||||
counts.lines++;
|
||||
pos++;
|
||||
} else if (ch === 32 || ch === 9 || ch === 13) {
|
||||
pos++;
|
||||
} else if (ch === 47 && text.charCodeAt(pos + 1) === 47) {
|
||||
const start = pos;
|
||||
while (pos < end && text.charCodeAt(pos) !== 10) pos++;
|
||||
counts.comments++;
|
||||
counts.commentChars += pos - start;
|
||||
} else if (ch === 47 && text.charCodeAt(pos + 1) === 42) {
|
||||
const start = pos;
|
||||
pos += 2;
|
||||
while (pos < end && !(text.charCodeAt(pos) === 42 && text.charCodeAt(pos + 1) === 47)) {
|
||||
if (text.charCodeAt(pos) === 10) counts.lines++;
|
||||
pos++;
|
||||
}
|
||||
pos += 2;
|
||||
counts.comments++;
|
||||
counts.commentChars += pos - start;
|
||||
} else if (ch === 34 || ch === 39) {
|
||||
pos++;
|
||||
while (pos < end && text.charCodeAt(pos) !== ch) pos++;
|
||||
pos++;
|
||||
counts.strings++;
|
||||
} else if (ch >= 48 && ch <= 57) {
|
||||
while (pos < end && text.charCodeAt(pos) >= 48 && text.charCodeAt(pos) <= 57) pos++;
|
||||
counts.numbers++;
|
||||
} else if (isIdentifierStart(ch)) {
|
||||
while (pos < end && isIdentifierPart(text.charCodeAt(pos))) pos++;
|
||||
counts.identifiers++;
|
||||
} else {
|
||||
counts.punctuation++;
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function lineStarts(text: string): number[] {
|
||||
const starts = [0];
|
||||
for (let pos = 0; pos < text.length; pos++) if (text.charCodeAt(pos) === 10) starts.push(pos + 1);
|
||||
return starts;
|
||||
}
|
||||
|
||||
const scale = Number(process.argv[2] ?? "1");
|
||||
const source = makeSource(60000);
|
||||
const passes = Math.round(6 * scale);
|
||||
let last = lex(source);
|
||||
let total = last.identifiers + lineStarts(source).length;
|
||||
for (let i = 1; i < passes; i++) {
|
||||
last = lex(source);
|
||||
total += last.identifiers + lineStarts(source).length;
|
||||
}
|
||||
console.log("units", source.length, "passes", passes, "total", total);
|
||||
console.log(JSON.stringify(last));
|
||||
@@ -1,4 +1,5 @@
|
||||
- **EventEmitter:** Literal event names use fixed argument tuples.
|
||||
Computed string names require a constant prefix or suffix that excludes `error`, meta events, and stream-internal events; their arguments must match the exact arity and support checked conversion.
|
||||
Unrestricted names, symbols, names that may reach internal events, and typed `listeners()`/`rawListeners()` results for names with possible computed registrations are unsupported.
|
||||
`events.once` accepts EventEmitter and stream instances with a literal event name and an optional object-literal `{ signal }`; a stream's `data` event, other options values, and EventTarget emitters are unsupported.
|
||||
Reading `defaultMaxListeners` through a CommonJS destructured or member binding (`const { defaultMaxListeners } = require("events")`) is unsupported.
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
- **Streams and readline:** Stream chunks are Buffers or strings.
|
||||
`NodeJS.ReadableStream` and `NodeJS.WritableStream`, and `stream.Readable` and `stream.Writable` under `@types/node`, accept every stream: when a program passes file, zlib, or `node:stream` streams through them, child-process pipes and `process.stdout` and `process.stderr` join those streams as `Readable` and `Writable` instances.
|
||||
`readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream.
|
||||
A `pipeline` stage that arrives as an untyped value must be a native stream at run time; other values throw a not-supported error.
|
||||
When an untyped value reaches `NodeJS.ReadableStream` or `NodeJS.WritableStream`, those two interfaces hold any value, so JavaScript stream objects (constructor-function streams, plain objects with `on` and `pipe`) pass through TypeScript functions typed with them and come back to JavaScript as the same objects.
|
||||
Member calls on such a value use the native stream when it is one and call the object's own members otherwise; `readable.pipe()` into a JavaScript destination follows Node's pipe protocol.
|
||||
A `pipeline` stage and a `for await` source that arrive as untyped values must be native streams at run time; other values throw a not-supported error.
|
||||
Native streams expose no `emit` to JavaScript, so a JavaScript stream whose own `pipe()` emits on its destination (the legacy `Stream` pipe) cannot pipe into one.
|
||||
`Readable.from` accepts arrays, Sets, Map and Set iterators, generators, async generators, and class iterables and async iterables whose values are strings or Buffers, and in JavaScript any iterable or async iterable of them; a stream that mixes strings and Buffers is unsupported.
|
||||
In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported.
|
||||
Object-mode streams of other values (including `Readable.from` over a `Map`, whose values are entry arrays) are unsupported.
|
||||
|
||||
@@ -19,7 +19,11 @@ Native calls accept standard process streams; custom native streams and mutation
|
||||
In JavaScript, a constructor function can inherit from `EventEmitter`, the legacy `Stream` constructor, or a stream class through `util.inherits` or `Object.setPrototypeOf`.
|
||||
`EventEmitter.call(this)` and `Stream.call(this)` initialize the object as Node does, and its listener methods keep their state in the object's own `_events`, `_eventsCount`, and `_maxListeners` properties.
|
||||
The `captureRejections` option and `events.errorMonitor` listeners are not supported on such objects.
|
||||
Calling `Readable`, `Writable`, `Duplex`, `Transform`, or `PassThrough` with a constructor function's receiver throws `SC2020` where the call runs.
|
||||
`Readable.call(this, options)` and the same call on `Writable`, `Duplex`, `Transform`, or `PassThrough` give the object a native stream.
|
||||
The stream calls the object's `_read`, `_write`, `_final`, `_transform`, `_flush`, and `_destroy` with the object as `this`, and the stream prototypes' methods and accessors work on it.
|
||||
Its listeners live in the native stream rather than in `_events`, and `_readableState` and `_writableState` are read-only views that answer the commonly read fields.
|
||||
Typed code that receives such an object as a stream class uses its native stream, and piping a native stream into it works.
|
||||
The options the native streams refuse (`objectMode`, `decodeStrings: false`, `signal`, and others) are refused here too, as are `_writev`, `_construct`, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, strings written in encodings other than UTF-8, and pipes into a destination that overrides `write()` or `end()`.
|
||||
Other class constructors passed to `util.inherits` or `util.deprecate` are refused.
|
||||
Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported.
|
||||
A stored `util.deprecate` reports a `length` of 4, where Node reports 3.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-darwin-arm64",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.0",
|
||||
"description": "Native scriptc compiler for darwin-arm64",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-darwin-x64",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.0",
|
||||
"description": "Native scriptc compiler for darwin-x64",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-arm64-gnu",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.0",
|
||||
"description": "Native scriptc compiler for linux-arm64-gnu",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-arm64-musl",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.0",
|
||||
"description": "Native scriptc compiler for linux-arm64-musl",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-x64-gnu",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.0",
|
||||
"description": "Native scriptc compiler for linux-x64-gnu",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-linux-x64-musl",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.0",
|
||||
"description": "Native scriptc compiler for linux-x64-musl",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/cli-win32-x64-msvc",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.0",
|
||||
"description": "Native scriptc compiler for win32-x64-msvc",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scriptc",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.0",
|
||||
"description": "Compile ordinary TypeScript and JavaScript to small, fast native executables and WebAssembly modules that run without Node.js",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
|
||||
+1
-1
@@ -6569,7 +6569,7 @@ declare module "events" {
|
||||
export { internal as EventEmitter };
|
||||
// The event's arguments, as checked-dynamic values.
|
||||
export function once(
|
||||
emitter: EventEmitter,
|
||||
emitter: EventEmitter | NodeJS.ReadableStream | NodeJS.WritableStream,
|
||||
eventName: string,
|
||||
options?: { signal?: AbortSignal },
|
||||
): Promise<unknown[]>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scriptc/compiler",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.0",
|
||||
"description": "The scriptc compiler — TypeScript/JavaScript frontend, typed IR, LLVM backend",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://scriptc.dev",
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@scriptc/runtime": "workspace:*",
|
||||
"tsc-ts": "0.0.3",
|
||||
"tsc-ts": "0.0.5",
|
||||
"typescript": "7.0.2",
|
||||
"typescript5": "npm:typescript@5.9.3"
|
||||
},
|
||||
|
||||
@@ -26,6 +26,8 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
dc: features.dc,
|
||||
dynAsync: features.dynAsync,
|
||||
...(features.dynTier ? { dynTier: true } : {}),
|
||||
...(features.streamDyn ? { streamDyn: true } : {}),
|
||||
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
|
||||
events: features.processEvents,
|
||||
emitter: features.emitter,
|
||||
symbol: features.symbol,
|
||||
@@ -33,6 +35,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
searchParams: features.searchParams,
|
||||
qs: features.qs,
|
||||
dynBuiltinClass: features.dynBuiltinClass,
|
||||
dynBuiltinStream: features.dynBuiltinStream,
|
||||
parseArgs: features.parseArgs,
|
||||
stream: features.stream,
|
||||
net: features.net,
|
||||
|
||||
@@ -43,6 +43,7 @@ export async function compilePackedLibrary(
|
||||
searchParams: options.searchParams ?? false,
|
||||
qs: false,
|
||||
dynBuiltinClass: false,
|
||||
dynBuiltinStream: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
isDirentShape,
|
||||
isDynTypedRefType,
|
||||
isRefCounted,
|
||||
isUnitType,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
import { dynDesc, streamTypedRefEligible } from "../../ir/analysis.js";
|
||||
@@ -130,6 +131,8 @@ export interface DynHost extends WalkerHost {
|
||||
/** The shared adapter record: live array capsules read its `arrayOps`, so a
|
||||
* narrower return type would hand the native compiler a copy without it. */
|
||||
liveDynRefAdapter(t: IrType): LlStreamTypedRefAdapter;
|
||||
/** See moduleUsesDynBuiltinStream. */
|
||||
usesDynBuiltinStream?(): boolean;
|
||||
liveDynUnionRefAdapter(t: IrType & { kind: "union" }): string;
|
||||
dynPromiseAdapter(t: IrType): string;
|
||||
isErrorClass(className: string): boolean;
|
||||
@@ -140,6 +143,19 @@ export interface DynHost extends WalkerHost {
|
||||
|
||||
const FN_ATTRS = "#0";
|
||||
|
||||
/** scr_dyn_builtin_stream_check's kind for a runtime stream class (the
|
||||
* numbering of scr_dyn_builtin_class.c), or null. */
|
||||
function builtinStreamKind(className: string): number | null {
|
||||
const kinds: Record<string, number> = {
|
||||
"%Readable": 2,
|
||||
"%Writable": 3,
|
||||
"%Duplex": 4,
|
||||
"%Transform": 5,
|
||||
"%PassThrough": 6,
|
||||
};
|
||||
return kinds[className] ?? null;
|
||||
}
|
||||
|
||||
export class LlDyn {
|
||||
private readonly dynMatchers = new Map<string, string>();
|
||||
private readonly dynBuilders = new Map<string, string>();
|
||||
@@ -682,6 +698,46 @@ export class LlDyn {
|
||||
|
||||
/** Emit the standard in-helper pending check: on a pending exception
|
||||
* run `cleanup` and return `dummy`. */
|
||||
/** The failure block of a union dynCheck whose value matched no arm. A
|
||||
* value of the one container kind exactly one arm can take (an object
|
||||
* for the union's only record arm, an array for its only array arm,
|
||||
* beside unit and scalar arms) runs that arm's builder, which throws the
|
||||
* deepest failing path and the reason there (`expected string at
|
||||
* $.order.owner.id, got undefined`) instead of the union's own outer
|
||||
* path. Other values, and an arm builder that unexpectedly succeeds,
|
||||
* fall through to the union's message. Only the failure path changes. */
|
||||
private unionArmFailure(B: BlockBuilder, def: IrUnionDef, kind: string): void {
|
||||
const scalar = (arm: IrType): boolean =>
|
||||
isUnitType(arm) ||
|
||||
arm.kind === "f64" ||
|
||||
arm.kind === "bool" ||
|
||||
arm.kind === "string" ||
|
||||
arm.kind === "bigint";
|
||||
const containers = def.arms.filter((arm) => !scalar(arm));
|
||||
const arm = containers.length === 1 ? containers[0]! : null;
|
||||
const container =
|
||||
arm?.kind === "array"
|
||||
? DYN_KIND.ARR
|
||||
: arm?.kind === "record" && !this.host.recordsById.get(arm.shapeId)?.tuple
|
||||
? DYN_KIND.OBJ
|
||||
: null;
|
||||
if (!arm || container === null) return;
|
||||
const host = this.host;
|
||||
const deep = B.newLabel("dcu.deep");
|
||||
const outer = B.newLabel("dcu.outer");
|
||||
const same = B.tmp();
|
||||
B.line(`${same} = icmp eq i32 ${kind}, ${container}`);
|
||||
B.condBr(same, deep, outer);
|
||||
B.startBlock(deep);
|
||||
const built = B.tmp();
|
||||
B.line(`${built} = call ptr @${this.dynCheckHelper(arm)}(ptr %d, ptr %path)`);
|
||||
this.pendingBail(B, "dcu.deep", () => {}, "ptr null");
|
||||
const rc = vAdapters(host, arm);
|
||||
B.line(`call void ${rc.release}(ptr ${built})`);
|
||||
B.br(outer);
|
||||
B.startBlock(outer);
|
||||
}
|
||||
|
||||
private pendingBail(B: BlockBuilder, hint: string, cleanup: () => void, dummy: string): void {
|
||||
const p = B.tmp();
|
||||
for (const line of this.host.pendingTestLines(p)) B.line(line.trimStart());
|
||||
@@ -1491,6 +1547,24 @@ export class LlDyn {
|
||||
B.line(`${ref} = call ptr @scr_dyn_typed_ref_unbox(ptr %d)`);
|
||||
B.terminate(`ret ptr ${ref}`);
|
||||
B.startBlock(lNext);
|
||||
// A constructor-function stream (scr_dyn_builtin_stream.c) carries
|
||||
// its runtime stream.
|
||||
const streamKind = t.kind === "object" ? builtinStreamKind(t.className) : null;
|
||||
if (streamKind !== null && this.host.usesDynBuiltinStream?.() === true) {
|
||||
host.declare(`declare ptr @scr_dyn_builtin_stream_check(ptr, double)`);
|
||||
const own = B.tmp(),
|
||||
hit = B.tmp();
|
||||
B.line(
|
||||
`${own} = call ptr @scr_dyn_builtin_stream_check(ptr %d, double ${f64Lit(streamKind)})`,
|
||||
);
|
||||
B.line(`${hit} = icmp ne ptr ${own}, null`);
|
||||
const lOwn = B.newLabel("dc.ctor"),
|
||||
lNotOwn = B.newLabel("dc.notctor");
|
||||
B.condBr(hit, lOwn, lNotOwn);
|
||||
B.startBlock(lOwn);
|
||||
B.terminate(`ret ptr ${own}`);
|
||||
B.startBlock(lNotOwn);
|
||||
}
|
||||
if (t.kind !== "union" && preserveRefs) {
|
||||
const kind = this.kindOf(B, "%d");
|
||||
const capsule = B.tmp();
|
||||
@@ -2258,6 +2332,7 @@ export class LlDyn {
|
||||
B.line(`call void @scr_dyn_release_v(ptr ${materialized})`);
|
||||
B.terminate(`ret ptr ${checked}`);
|
||||
B.startBlock(lFail);
|
||||
this.unionArmFailure(B, def, kind);
|
||||
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
|
||||
B.terminate(`ret ptr null`);
|
||||
break;
|
||||
@@ -2326,6 +2401,7 @@ export class LlDyn {
|
||||
);
|
||||
B.terminate(`ret ptr ${checked}`);
|
||||
B.startBlock(lFail);
|
||||
this.unionArmFailure(B, def, kind);
|
||||
B.line(`call void @scr_dyn_check_fail(ptr %path, ptr ${want}, ptr %d)`);
|
||||
B.terminate(`ret ptr null`);
|
||||
break;
|
||||
|
||||
@@ -1333,14 +1333,17 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
// of the capacity word (SCR_STR_ASCII_BIT, the low half's sign bit on
|
||||
// little-endian targets) makes UTF-16 units bytes. Unproven strings and
|
||||
// anything but an in-range index take the runtime's UTF-16 mapping.
|
||||
// A scan's receiver reads inline too where runtime state is ordinary
|
||||
// global storage (ScrStrScan; SCR_TL makes it thread-local otherwise).
|
||||
const scanInline = emitter.mod.workers !== true && emitter.mod.lib?.threadInstances !== true;
|
||||
// A scan's receiver reads inline too (ScrStrScan): from ordinary global
|
||||
// storage, or, in worker executables, from the thread's static TLS block,
|
||||
// one thread-pointer-relative load (see executableTls).
|
||||
const scanInline =
|
||||
emitter.executableTls ||
|
||||
(emitter.mod.workers !== true && emitter.mod.lib?.threadInstances !== true);
|
||||
let scanDeclared = false;
|
||||
for (const helper of STRING_READ_HELPERS) {
|
||||
if (!emitter.decls.has(helper.decl)) continue;
|
||||
if (scanInline && !scanDeclared) {
|
||||
defs.push(stringScanDecl(emitter.sizeType), ``);
|
||||
defs.push(stringScanDecl(emitter.sizeType, emitter.executableTls), ``);
|
||||
scanDeclared = true;
|
||||
}
|
||||
defs.push(...helper.define(emitter.sizeType, FN_ATTRS, scanInline), ``);
|
||||
|
||||
@@ -53,7 +53,11 @@ import {
|
||||
VOID,
|
||||
classJsName,
|
||||
} from "../../ir/ir.js";
|
||||
import { moduleRuntimeFeatures, moduleUsesMockTimers } from "../../ir/runtime-features.js";
|
||||
import {
|
||||
moduleRuntimeFeatures,
|
||||
moduleUsesDynBuiltinStream,
|
||||
moduleUsesMockTimers,
|
||||
} from "../../ir/runtime-features.js";
|
||||
import { captureFreeFunctions, observesStacks } from "../stack-observations.js";
|
||||
import { type DenseArrayLoop } from "./dense-array-loops.js";
|
||||
import { findInitializedByteLoopBindings } from "./byte-windows.js";
|
||||
@@ -902,6 +906,11 @@ export class LlEmitter {
|
||||
/** Identity-preserving static→dyn capsules for program classes and Web
|
||||
* APIs whose values remain directly observable. */
|
||||
readonly liveDynRefAdapters = new Map<string, LlStreamTypedRefAdapter>();
|
||||
private dynBuiltinStreamUse: boolean | undefined;
|
||||
/** See moduleUsesDynBuiltinStream. */
|
||||
get usesDynBuiltinStream(): boolean {
|
||||
return (this.dynBuiltinStreamUse ??= moduleUsesDynBuiltinStream(this.mod));
|
||||
}
|
||||
/** Runtime-arm dispatchers for live union values. */
|
||||
readonly liveDynUnionRefAdapters = new Map<string, string>();
|
||||
readonly dynPromiseAdapters = new Map<string, string>();
|
||||
@@ -1405,6 +1414,7 @@ export class LlEmitter {
|
||||
fieldAbsentTestIn: (B, value, unionId) => this.fieldAbsentTestIn(B, value, unionId),
|
||||
absentInstanceRef: (unionId) => this.absentInstanceRef(unionId),
|
||||
liveDynRefAdapter: (type) => this.liveDynRefAdapter(type),
|
||||
usesDynBuiltinStream: () => this.usesDynBuiltinStream,
|
||||
liveDynUnionRefAdapter: (type) => this.liveDynUnionRefAdapter(type),
|
||||
dynPromiseAdapter: (type) => this.dynPromiseAdapter(type),
|
||||
isErrorClass: (name) => this.classMeta.get(name)?.root.def.name === "%Error",
|
||||
@@ -5599,8 +5609,11 @@ export class LlEmitter {
|
||||
return emitterFixedAdapter(this, cbT);
|
||||
}
|
||||
|
||||
emitterOnceAdapter(boxerT: IrType & { kind: "func" }): { fn: string; shim: string } {
|
||||
return emitterOnceAdapter(this, boxerT);
|
||||
emitterOnceAdapter(
|
||||
boxerT: IrType & { kind: "func" },
|
||||
fire?: string,
|
||||
): { fn: string; shim: string } {
|
||||
return emitterOnceAdapter(this, boxerT, fire);
|
||||
}
|
||||
|
||||
wrapEmitterListener(target: string, adapterFn: string): string {
|
||||
|
||||
@@ -864,23 +864,27 @@ export function emitterFixedAdapter(
|
||||
* (the event's tuple to any[]): the runtime-built listener closure holds
|
||||
* the boxer in caps[0], so the adapter calls it with the event's typed
|
||||
* arguments, as emitterFixedAdapter calls a listener, and hands the array
|
||||
* to scr_events_once_fire. */
|
||||
* to scr_events_once_fire (or `fire`, the signal form's entry). */
|
||||
export function emitterOnceAdapter(
|
||||
host: LlvmEmitterContext,
|
||||
boxerT: IrType & { kind: "func" },
|
||||
fire = "scr_events_once_fire",
|
||||
): { fn: string; shim: string } {
|
||||
const cursor = boxerT.params.length > 4;
|
||||
const shim = cursor ? "scr_ee_inv_args" : `scr_ee_inv_fixed${boxerT.params.length}`;
|
||||
if (cursor) host.declare("declare ptr @scr_ee_arg_next(ptr)");
|
||||
host.declare(`declare void @${shim}(ptr, ptr)`);
|
||||
const key = `eeOnce:${typeKey(boxerT)}`;
|
||||
const key =
|
||||
fire === "scr_events_once_fire"
|
||||
? `eeOnce:${typeKey(boxerT)}`
|
||||
: `eeOnce:${fire}:${typeKey(boxerT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return { fn: sym, shim };
|
||||
sym = `sc_ee_once_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
|
||||
host.declare(`declare void @scr_closure_release(ptr)`);
|
||||
host.declare(`declare void @scr_events_once_fire(ptr, ptr)`);
|
||||
host.declare(`declare void @${fire}(ptr, ptr)`);
|
||||
const params = cursor ? "ptr %args" : boxerT.params.map((_, i) => `ptr %a${i}`).join(", ");
|
||||
const d: string[] = [
|
||||
`define internal void @${sym}(ptr %cb${params ? ", " + params : ""}) ${FN_ATTRS} { ; events.once adapter ${typeKey(boxerT)}`,
|
||||
@@ -911,7 +915,7 @@ export function emitterOnceAdapter(
|
||||
` %fn = load ptr, ptr %fnp`,
|
||||
` %arr = call ptr %fn(${passed.join(", ")}) ; the boxed arguments, +1`,
|
||||
` call void @scr_closure_release(ptr %boxer)`,
|
||||
` call void @scr_events_once_fire(ptr %cb, ptr %arr) ; moves %arr`,
|
||||
` call void @${fire}(ptr %cb, ptr %arr) ; moves %arr`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
|
||||
@@ -306,3 +306,44 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits"
|
||||
}
|
||||
expect(body("number_add")).not.toMatch(/ = add i32 |fcmp ord/);
|
||||
});
|
||||
|
||||
test("string reads of a scan receiver load it inline where emitted code can address it", () => {
|
||||
const receiver: IrExpr = { kind: "varRef", localId: "text", type: STRING, loc };
|
||||
const mod = fixture();
|
||||
mod.functions = [
|
||||
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
|
||||
{
|
||||
name: "units",
|
||||
params: [{ name: "text", localId: "text", type: STRING }],
|
||||
returnType: F64,
|
||||
locals: [{ id: "text", name: "text", type: STRING, mutable: false }],
|
||||
loc,
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
loc,
|
||||
value: { kind: "strIntrinsic", method: "length", receiver, args: [], type: F64, loc },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const helper = (llvm: string): string =>
|
||||
/^define internal double @sc_str_utf16_len\([^]*?^}/m.exec(llvm)![0];
|
||||
const plain = emitLlvmModule(mod);
|
||||
expect(plain).toContain("@scr_str_scan = external global");
|
||||
expect(helper(plain)).toContain("%scan.same");
|
||||
// ELF worker executables keep the receiver in the thread's static TLS block.
|
||||
const elf = emitLlvmModule(
|
||||
{ ...mod, workers: true },
|
||||
{ targetTriple: "x86_64-unknown-linux-gnu" },
|
||||
);
|
||||
expect(elf).toContain("@scr_str_scan = external thread_local(localexec) global");
|
||||
expect(helper(elf)).toContain("%scan.same");
|
||||
// Darwin worker executables leave every non-ASCII read to the runtime.
|
||||
const darwin = emitLlvmModule(
|
||||
{ ...mod, workers: true },
|
||||
{ targetTriple: "arm64-apple-macosx14.0.0" },
|
||||
);
|
||||
expect(darwin).not.toContain("@scr_str_scan");
|
||||
expect(helper(darwin)).not.toContain("%scan.same");
|
||||
});
|
||||
|
||||
@@ -766,8 +766,13 @@ export function streamTypedRefMaterializeAdapter(
|
||||
!meta.def.instancePrototypeHelper &&
|
||||
!meta.def.prototypeDataHelper
|
||||
) {
|
||||
host.declare(`declare ptr @scr_stream_dyn_view(ptr)`);
|
||||
B.line(`${out} = call ptr @scr_stream_dyn_view(ptr %p)`);
|
||||
// Programs with constructor-function streams view them through
|
||||
// scr_dyn_builtin_stream.c, which answers their own objects.
|
||||
const view = host.usesDynBuiltinStream
|
||||
? "scr_dyn_builtin_stream_view"
|
||||
: "scr_stream_dyn_view";
|
||||
host.declare(`declare ptr @${view}(ptr)`);
|
||||
B.line(`${out} = call ptr @${view}(ptr %p)`);
|
||||
} else B.line(`${out} = call ptr @scr_dyn_new_obj()`);
|
||||
const withAttributes = meta.def.fields.some((field) => field.name === CLASS_ATTRIBUTES);
|
||||
if (meta.def.tracksOwnFields) {
|
||||
|
||||
@@ -548,7 +548,12 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
mockTimerSym(host, e) ??
|
||||
(e.fn === "timers.queueMicrotaskDyn" && host.schedulesInContext
|
||||
? "scr_queue_microtask_dyn_als"
|
||||
: LIB_FN_SYMS[e.fn]);
|
||||
: // Programs with constructor-function streams unbox untyped pipe
|
||||
// destinations and pipeline stages through scr_dyn_builtin_stream.c,
|
||||
// which also takes those streams.
|
||||
e.fn === "stream.unboxDyn" && host.usesDynBuiltinStream
|
||||
? "scr_dyn_builtin_stream_unbox"
|
||||
: LIB_FN_SYMS[e.fn]);
|
||||
if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
|
||||
const args = READ_ONLY_STRING_LIB_FNS.has(e.fn)
|
||||
? emitBorrowedInputs(host, e.args)
|
||||
@@ -755,7 +760,8 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
}
|
||||
const prefix = e.fn.slice(0, e.fn.indexOf(".")) as LibCallPrefix;
|
||||
// events.once builds typed listener adapters beside the emitter calls.
|
||||
if (e.fn === "events.once") return host.emitErrorsEventsLibCall(e);
|
||||
if (e.fn === "events.once" || e.fn === "events.onceSignal")
|
||||
return host.emitErrorsEventsLibCall(e);
|
||||
switch (prefix) {
|
||||
case "fetch":
|
||||
case "abort":
|
||||
|
||||
@@ -731,27 +731,34 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
host.emitAlwaysPendingUnwind();
|
||||
return out;
|
||||
}
|
||||
if (e.fn === "events.once") {
|
||||
// (recv, name, boxer, errorBoxer, listenForError): scr_events_once
|
||||
// registers one-shot typed listeners behind adapters that box the
|
||||
// event's tuple (and the 'error' tuple) into the fulfilled array. The
|
||||
// runtime retains the boxers; the frame's temporaries release as usual.
|
||||
// May-throw ('newListener' meta listeners run inside).
|
||||
if (e.fn === "events.once" || e.fn === "events.onceSignal") {
|
||||
// (recv, name, boxer, errorBoxer, listenForError[, signal]):
|
||||
// scr_events_once registers one-shot typed listeners behind adapters
|
||||
// that box the event's tuple (and the 'error' tuple) into the fulfilled
|
||||
// array. The runtime retains the boxers; the frame's temporaries
|
||||
// release as usual. May-throw ('newListener' meta listeners run
|
||||
// inside). The signal form's adapters end in its own fire entry.
|
||||
const boxerT = e.args[2]!.type;
|
||||
const errorT = e.args[3]!.type;
|
||||
if (boxerT.kind !== "func" || errorT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: events.once boxer not a func");
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} boxer not a func`);
|
||||
const signal = e.fn === "events.onceSignal";
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
const event = host.emitterOnceAdapter(boxerT);
|
||||
const error = host.emitterOnceAdapter(errorT);
|
||||
host.declare(`declare ptr @scr_events_once(ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr)`);
|
||||
const fire = signal ? "scr_events_once_signal_fire" : undefined;
|
||||
const event = host.emitterOnceAdapter(boxerT, fire);
|
||||
const error = host.emitterOnceAdapter(errorT, fire);
|
||||
const entry = signal ? "scr_events_once_signal" : "scr_events_once";
|
||||
host.declare(
|
||||
`declare ptr @${entry}(ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr${signal ? ", ptr" : ""})`,
|
||||
);
|
||||
const errorBoxer = B.tmp();
|
||||
B.line(`${errorBoxer} = select i1 ${args[4]!.name}, ptr ${args[3]!.name}, ptr null`);
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call ptr @scr_events_once(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ` +
|
||||
`${t} = call ptr @${entry}(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ` +
|
||||
`ptr ${args[2]!.name}, ptr @${event.fn}, ptr @${event.shim}, ` +
|
||||
`ptr ${errorBoxer}, ptr @${error.fn}, ptr @${error.shim})`,
|
||||
`ptr ${errorBoxer}, ptr @${error.fn}, ptr @${error.shim}` +
|
||||
`${signal ? `, ptr ${args[5]!.name}` : ""})`,
|
||||
);
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
|
||||
@@ -742,6 +742,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"procStream.write": "scr_proc_stream_write",
|
||||
"stream.stdioWritable": "scr_stream_stdio_writable",
|
||||
"stream.unboxDyn": "scr_stream_unbox_dyn",
|
||||
"stream.pipeDyn": "scr_stream_pipe_dyn",
|
||||
"stream.unpipeDyn": "scr_stream_unpipe_dyn",
|
||||
"process.kill": "scr_process_kill_named",
|
||||
"process.killNum": "scr_process_kill",
|
||||
// fs.promises (scr_bytes_io.c / scr_lib.c): the promise forms settle
|
||||
@@ -1459,6 +1461,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.classBasePrototype": "scr_dyn_class_base_prototype",
|
||||
"dyn.classInherit": "scr_dyn_class_inherit",
|
||||
"dyn.builtinClass": "scr_dyn_builtin_class",
|
||||
"dyn.builtinStreamClass": "scr_dyn_builtin_stream_class",
|
||||
"dyn.builtinInstanceOf": "scr_dyn_builtin_instanceof",
|
||||
"dyn.functionInstanceOf": "scr_dyn_function_instanceof",
|
||||
"dyn.classSuper": "scr_dyn_class_super",
|
||||
@@ -1537,6 +1540,8 @@ export function mockTimerSym(
|
||||
|
||||
export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"process.emitWarning",
|
||||
// Constructor-function streams defer their events on the stream ticks.
|
||||
"dyn.builtinStreamClass",
|
||||
// Decipheriv.setAuthTag reports DEP0182 on the next tick.
|
||||
"crypto.cipherNew",
|
||||
// WebCrypto results settle through the deferred crypto queue.
|
||||
|
||||
@@ -36,6 +36,29 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
}
|
||||
if (e.fn === "stream.dynNative" || e.fn === "stream.dynNativePipe") {
|
||||
// Checked-dynamic stream slots: the boxed native stream (+1) or NULL.
|
||||
if (e.type.kind !== "union")
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} result is not a union`);
|
||||
const def = host.unionsById.get(e.type.unionId);
|
||||
const streamTag = def ? def.arms.findIndex((a) => a.kind === "object") : -1;
|
||||
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
||||
if (streamTag < 0 || nullTag < 0)
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} union lacks its arms`);
|
||||
const value = host.emitExpr(e.args[0]!);
|
||||
const side = host.emitExpr(e.args[1]!);
|
||||
// A pipe or unpipe destination in programs with constructor-function
|
||||
// streams can be one of those streams too (scr_dyn_builtin_stream.c),
|
||||
// as stream.unboxDyn does there.
|
||||
const fn =
|
||||
e.fn === "stream.dynNativePipe" && host.usesDynBuiltinStream
|
||||
? "scr_dyn_builtin_stream_dyn_native"
|
||||
: "scr_stream_dyn_native";
|
||||
host.declare(`declare ptr @${fn}(ptr, double)`);
|
||||
const raw = B.tmp();
|
||||
B.line(`${raw} = call ptr @${fn}(ptr ${value.name}, double ${side.name}) ; +1 or NULL`);
|
||||
return host.wrapNullable(raw, raw, def!.arms[streamTag]!, streamTag, e.type, nullTag);
|
||||
}
|
||||
if (e.fn === "stream.onData") {
|
||||
// The callback MOVES into the stream's listener registry; the
|
||||
// adapter is per callback shape — runtime-provided for the
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
* With `scan`, a string that is not proven ASCII is first compared with
|
||||
* the runtime's published scan receiver (ScrStrScan, scr_runtime.h): the
|
||||
* string an indexed scan converted to UTF-16 units, whose length and units
|
||||
* then read inline. Only targets whose runtime state is not thread-local
|
||||
* publish it to emitted code.
|
||||
* then read inline. Where the runtime keeps it per thread, the declaration
|
||||
* is thread-local.
|
||||
*/
|
||||
|
||||
import { exactIndexLines, sizeToDouble } from "./common.js";
|
||||
@@ -53,8 +53,8 @@ export function stringScanType(sz: string): string {
|
||||
return `{ ptr, ptr, ${sz} }`;
|
||||
}
|
||||
|
||||
export function stringScanDecl(sz: string): string {
|
||||
return `@scr_str_scan = external global ${stringScanType(sz)}`;
|
||||
export function stringScanDecl(sz: string, threadLocal: boolean): string {
|
||||
return `@scr_str_scan = external ${threadLocal ? "thread_local " : ""}global ${stringScanType(sz)}`;
|
||||
}
|
||||
|
||||
/** The scan receiver test of `%s`: branches to `hit` with `%scan.n` (its
|
||||
|
||||
@@ -43,6 +43,12 @@ export interface NativeLinkFeatures {
|
||||
/** Dynamic-tier jsval operations: the scr_js_* units. Absent (not false)
|
||||
* for programs without them, so their link features stay unchanged. */
|
||||
dynTier?: boolean;
|
||||
/** Checked-dynamic stream slots: the scr_stream_dyn.c unit. Absent (not
|
||||
* false) for programs without them. */
|
||||
streamDyn?: boolean;
|
||||
/** events.once with options: scr_events_once_signal.c. Absent (not
|
||||
* false) for programs without it. */
|
||||
eventsOnceSignal?: boolean;
|
||||
events: boolean;
|
||||
emitter: boolean;
|
||||
symbol: boolean;
|
||||
@@ -50,6 +56,7 @@ export interface NativeLinkFeatures {
|
||||
searchParams: boolean;
|
||||
qs: boolean;
|
||||
dynBuiltinClass: boolean;
|
||||
dynBuiltinStream: boolean;
|
||||
parseArgs: boolean;
|
||||
stream: boolean;
|
||||
net: boolean;
|
||||
|
||||
@@ -113,6 +113,12 @@ export interface CcOptions {
|
||||
* the IR): compiles the scr_js_* units (two-tier design I3). Programs
|
||||
* without them link no scr_js_ symbol. */
|
||||
dynTier?: boolean;
|
||||
/** Checked-dynamic stream slots (stream.dynNative / stream.pipeDyn):
|
||||
* compiles scr_stream_dyn.c. */
|
||||
streamDyn?: boolean;
|
||||
/** events.once with options (events.onceSignal): compiles
|
||||
* scr_events_once_signal.c. */
|
||||
eventsOnceSignal?: boolean;
|
||||
/** The program constructs EventTarget/Event/CustomEvent values or
|
||||
* calls events.addAbortListener (the event.* libCalls), or uses the
|
||||
* native fetch AbortSignal built on the same engine: compiles
|
||||
@@ -157,6 +163,10 @@ export interface CcOptions {
|
||||
* scr_dyn_builtin_class.c, which also references the inspection,
|
||||
* dyn-invoke and symbol units. */
|
||||
dynBuiltinClass?: boolean;
|
||||
/** The program uses a stream class as an untyped constructor value:
|
||||
* compiles scr_dyn_builtin_stream.c, the constructors' stream half, which
|
||||
* also references the stream unit. */
|
||||
dynBuiltinStream?: boolean;
|
||||
/** The program uses native util parsing, comparison, or styling. Optional
|
||||
* utility units also require their symbol, inspection, and warning features. */
|
||||
parseArgs?: boolean;
|
||||
|
||||
@@ -220,6 +220,9 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
// measures with.
|
||||
unit((u) => u.opts.events && u.regex, "scr_readline_tty.c"),
|
||||
unit((u) => u.opts.emitter || u.opts.workers, "scr_events_emitter.c"),
|
||||
// events.once with options registers through the emitter and
|
||||
// event-target units.
|
||||
unit((u) => u.opts.eventsOnceSignal === true, "scr_events_once_signal.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
|
||||
@@ -247,11 +250,24 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
(u.opts.emitter || u.opts.workers),
|
||||
"scr_dyn_builtin_class.c",
|
||||
),
|
||||
unit(
|
||||
(u) =>
|
||||
u.opts.dynBuiltinStream &&
|
||||
u.opts.dynBuiltinClass &&
|
||||
u.opts.stream &&
|
||||
u.opts.inspect &&
|
||||
u.opts.dynInvoke &&
|
||||
u.opts.symbol,
|
||||
"scr_dyn_builtin_stream.c",
|
||||
),
|
||||
unit((u) => u.opts.stream, "scr_stream.c", "scr_fs_stream.c"),
|
||||
// The zlib stream classes reference both the zlib engine and the
|
||||
// stream unit.
|
||||
unit((u) => u.opts.zlib && u.opts.stream, "scr_zlib_stream.c"),
|
||||
unit((u) => u.opts.stream && u.opts.dynAsync, "scr_stream_ops.c"),
|
||||
// Checked-dynamic stream slots test values and call JavaScript stream
|
||||
// members through the dyn-invoke unit.
|
||||
unit((u) => u.opts.streamDyn === true, "scr_stream_dyn.c"),
|
||||
// FileHandle streams reference both the stream unit and FileHandle.
|
||||
unit((u) => u.opts.stream && u.opts.fileHandle, "scr_fs_stream_fh.c"),
|
||||
// The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD,
|
||||
|
||||
@@ -84,6 +84,8 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
|
||||
eventTarget: boolean;
|
||||
wasm: boolean;
|
||||
dynTier: boolean;
|
||||
streamDyn: boolean;
|
||||
eventsOnceSignal: boolean;
|
||||
warningReplace: boolean;
|
||||
stdioExtended: boolean;
|
||||
nativeFetch: boolean;
|
||||
@@ -275,6 +277,8 @@ export function effectiveRuntimeFeatures(
|
||||
eventTarget: features.eventTarget === true,
|
||||
wasm: features.wasm === true,
|
||||
dynTier: features.dynTier === true,
|
||||
streamDyn: features.streamDyn === true,
|
||||
eventsOnceSignal: features.eventsOnceSignal === true,
|
||||
warningReplace: features.warningReplace === true,
|
||||
stdioExtended: features.stdioExtended === true,
|
||||
nativeFetch,
|
||||
|
||||
@@ -108,6 +108,7 @@ const features = {
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
dynBuiltinClass: false,
|
||||
dynBuiltinStream: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -45,6 +45,7 @@ const BASE: NativeLinkFeatures = {
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
dynBuiltinClass: false,
|
||||
dynBuiltinStream: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -84,6 +84,97 @@ test("a computed key reads the stack when the program spells it", () => {
|
||||
expect(observesStacks(mod([fn("%main", [computed])]))).toBe(false);
|
||||
});
|
||||
|
||||
test('a computed key that cannot be "stack" reads no stack', () => {
|
||||
const value: IrExpr = { loc, kind: "varRef", localId: "d", type: DYN };
|
||||
const spelled: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "strLit", value: "stack", type: STRING },
|
||||
};
|
||||
const read = (key: IrExpr): IrStmt => ({
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "dynKeyGet", value, key, type: DYN },
|
||||
});
|
||||
const index: IrExpr = { loc, kind: "varRef", localId: "i", type: F64 };
|
||||
const indexText: IrExpr = { loc, kind: "toString", operand: index, type: STRING };
|
||||
// A number key, and a key converted from one.
|
||||
expect(observesStacks(mod([fn("%main", [read(index), spelled])]))).toBe(false);
|
||||
expect(observesStacks(mod([fn("%main", [read(indexText), spelled])]))).toBe(false);
|
||||
// An immutable local answers with its initializer (the shape of the
|
||||
// dispatch helpers' formatted index keys).
|
||||
const key: IrExpr = { loc, kind: "varRef", localId: "key", type: STRING };
|
||||
const local = (init: IrExpr, mutable = false): IrFunction => ({
|
||||
...fn("%main", [{ loc, kind: "varDecl", localId: "key", init }, read(key), spelled]),
|
||||
locals: [{ id: "key", name: "key", type: STRING, mutable }],
|
||||
});
|
||||
expect(observesStacks(mod([local(indexText)]))).toBe(false);
|
||||
expect(observesStacks(mod([local(indexText, true)]))).toBe(true);
|
||||
// A parameter of a function called by name answers with its arguments.
|
||||
const helper: IrFunction = {
|
||||
...fn("readKey", [read(key)]),
|
||||
params: [{ localId: "key", name: "key", type: STRING }],
|
||||
locals: [{ id: "key", name: "key", type: STRING, mutable: false }],
|
||||
};
|
||||
const callWith = (arg: IrExpr): IrStmt => ({
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "call", callee: "readKey", args: [arg], type: DYN },
|
||||
});
|
||||
const numbers = mod([fn("%main", [callWith(indexText), spelled]), helper]);
|
||||
expect(observesStacks(numbers)).toBe(false);
|
||||
const stack = mod([
|
||||
fn("%main", [
|
||||
callWith(indexText),
|
||||
callWith({ loc, kind: "strLit", value: "stack", type: STRING }),
|
||||
]),
|
||||
helper,
|
||||
]);
|
||||
expect(observesStacks(stack)).toBe(true);
|
||||
});
|
||||
|
||||
test("a key forwarded through functions with several call sites is searched once", () => {
|
||||
// f0 calls f1 twice, f1 calls f2 twice, and so on, each passing its
|
||||
// parameter on: 2^40 call paths reach the read in f40.
|
||||
const depth = 40;
|
||||
const value: IrExpr = { loc, kind: "varRef", localId: "d", type: DYN };
|
||||
const key: IrExpr = { loc, kind: "varRef", localId: "key", type: STRING };
|
||||
const forward = (callee: string): IrStmt => ({
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "call", callee, args: [key], type: VOID },
|
||||
});
|
||||
const chain = (root: IrExpr): IrModule => {
|
||||
const functions: IrFunction[] = [];
|
||||
for (let i = 0; i <= depth; i++) {
|
||||
const body: IrStmt[] =
|
||||
i < depth
|
||||
? [forward(`f${i + 1}`), forward(`f${i + 1}`)]
|
||||
: [{ loc, kind: "exprStmt", expr: { loc, kind: "dynKeyGet", value, key, type: DYN } }];
|
||||
functions.push({
|
||||
...fn(`f${i}`, body),
|
||||
params: [{ localId: "key", name: "key", type: STRING }],
|
||||
locals: [{ id: "key", name: "key", type: STRING, mutable: false }],
|
||||
});
|
||||
}
|
||||
const spelled: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "strLit", value: "stack", type: STRING },
|
||||
};
|
||||
const start: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "call", callee: "f0", args: [root], type: VOID },
|
||||
};
|
||||
return mod([fn("%main", [start, spelled]), ...functions]);
|
||||
};
|
||||
expect(observesStacks(chain({ loc, kind: "strLit", value: "message", type: STRING }))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(observesStacks(chain({ loc, kind: "strLit", value: "stack", type: STRING }))).toBe(true);
|
||||
});
|
||||
|
||||
test("programs without stack reads are not", () => {
|
||||
expect(observesStacks(mod([fn("%main", [call("helper")]), fn("helper", [ret])]))).toBe(false);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* it answers true. */
|
||||
import { exprFunctionReference, moduleFunctionReferences } from "../ir/function-references.js";
|
||||
import { MAY_THROW_LIB_FNS } from "../ir/builtin-effects.js";
|
||||
import type { IrExpr, IrFunction, IrModule, IrStmt } from "../ir/ir.js";
|
||||
import type { IrExpr, IrFunction, IrModule, IrStmt, IrType } from "../ir/ir.js";
|
||||
import { MAY_THROW_ARR_METHODS } from "../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild, everyStmtList } from "../ir/traverse.js";
|
||||
import { unreachableStatements } from "./may-throw.js";
|
||||
@@ -13,16 +13,135 @@ import { writesPublished } from "./publication.js";
|
||||
/** Lib calls that read a captured stack trace. */
|
||||
const STACK_READS: ReadonlySet<string> = new Set(["error.stack"]);
|
||||
|
||||
/** Key types whose property names are never `stack`: a number, bigint or
|
||||
* boolean key names its decimal or `true`/`false` property. */
|
||||
const NON_STACK_KEY_TYPES: ReadonlySet<IrType["kind"]> = new Set(["f64", "bigint", "bool"]);
|
||||
|
||||
/** Whether a computed key can be the string "stack", the question a read
|
||||
* on a checked-dynamic value asks before it counts as a stack read.
|
||||
* - A string literal answers by its value; a key of a number, bigint or
|
||||
* boolean type, or one converted from such a value, cannot be "stack".
|
||||
* - An immutable local answers with its one initializer, and a parameter of
|
||||
* a function the program only calls by name answers with the arguments
|
||||
* its call sites pass.
|
||||
* - Any other key can be "stack" when the program spells the literal
|
||||
* "stack" somewhere, since the literal can reach the key through fields,
|
||||
* containers or calls; a key the program assembles from other strings or
|
||||
* from data it reads keeps the static answer. */
|
||||
class StackKeys {
|
||||
private readonly spelled: boolean;
|
||||
private readonly calls = new Map<string, { caller: IrFunction; args: IrExpr[] }[]>();
|
||||
private readonly named: Set<string>;
|
||||
private readonly inits = new Map<IrFunction, Map<string, IrExpr | null>>();
|
||||
/** Settled answers by function and local: true where a search reached a
|
||||
* key that can be "stack", false where a search that found none passed. */
|
||||
private readonly answers = new Map<string, boolean>();
|
||||
|
||||
constructor(mod: IrModule) {
|
||||
this.spelled = spellsStack(mod);
|
||||
this.named = new Set(moduleFunctionReferences(mod));
|
||||
// Class methods, accessors and constructors are called through dispatch.
|
||||
const classNames = new Set((mod.classes ?? []).map((cls) => cls.name));
|
||||
for (const fn of mod.functions) {
|
||||
if (isClassMember(fn.name, classNames)) this.named.add(fn.name);
|
||||
everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (expr) => {
|
||||
if (expr.kind === "call") {
|
||||
const sites = this.calls.get(expr.callee) ?? [];
|
||||
sites.push({ caller: fn, args: expr.args });
|
||||
this.calls.set(expr.callee, sites);
|
||||
}
|
||||
const referenced = exprFunctionReference(expr);
|
||||
if (referenced !== undefined) this.named.add(referenced);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mayBeStack(fn: IrFunction, key: IrExpr): boolean {
|
||||
const visited = new Set<string>();
|
||||
const answer = this.keyMayBeStack(fn, key, visited);
|
||||
// A search that found no way to "stack" explored everything it visited
|
||||
// to the end, so none of it can be "stack" either.
|
||||
if (!answer) for (const visit of visited) this.answers.set(visit, false);
|
||||
return answer;
|
||||
}
|
||||
|
||||
private keyMayBeStack(fn: IrFunction, key: IrExpr, visited: Set<string>): boolean {
|
||||
if (key.kind === "strLit") return key.value === "stack";
|
||||
if (!this.spelled) return false;
|
||||
if (NON_STACK_KEY_TYPES.has(key.type.kind)) return false;
|
||||
if (key.kind === "toString" && NON_STACK_KEY_TYPES.has(key.operand.type.kind)) return false;
|
||||
if (key.kind === "dynFrom" && NON_STACK_KEY_TYPES.has(key.value.type.kind)) return false;
|
||||
if (key.kind !== "varRef") return true;
|
||||
const visit = `${fn.name}\0${key.localId}`;
|
||||
const known = this.answers.get(visit);
|
||||
if (known !== undefined) return known;
|
||||
// Each local is searched once per question: a cycle, or a second path
|
||||
// to a local already searched, adds no value of its own.
|
||||
if (visited.has(visit)) return false;
|
||||
visited.add(visit);
|
||||
const answer = this.localMayBeStack(fn, key.localId, visited);
|
||||
if (answer) this.answers.set(visit, true);
|
||||
return answer;
|
||||
}
|
||||
|
||||
private localMayBeStack(fn: IrFunction, localId: string, visited: Set<string>): boolean {
|
||||
const local = fn.locals.find((candidate) => candidate.id === localId);
|
||||
if (local === undefined || local.mutable) return true;
|
||||
const index = fn.params.findIndex((param) => param.localId === localId);
|
||||
if (index !== -1) {
|
||||
const sites = this.calls.get(fn.name);
|
||||
if (this.named.has(fn.name) || sites === undefined) return true;
|
||||
return sites.some(
|
||||
(site) =>
|
||||
site.args.length !== fn.params.length ||
|
||||
this.keyMayBeStack(site.caller, site.args[index]!, visited),
|
||||
);
|
||||
}
|
||||
const init = this.initializerOf(fn, localId);
|
||||
return init === null || this.keyMayBeStack(fn, init, visited);
|
||||
}
|
||||
|
||||
/** The one initializer of an immutable local, or null when it has none
|
||||
* or more than one declaration. */
|
||||
private initializerOf(fn: IrFunction, localId: string): IrExpr | null {
|
||||
let inits = this.inits.get(fn);
|
||||
if (inits === undefined) {
|
||||
inits = new Map();
|
||||
everyStmtList(fn.body, {
|
||||
stmt: (stmt) => {
|
||||
if (stmt.kind === "varDecl")
|
||||
inits!.set(stmt.localId, inits!.has(stmt.localId) ? null : (stmt.init ?? null));
|
||||
return true;
|
||||
},
|
||||
expr: () => true,
|
||||
});
|
||||
this.inits.set(fn, inits);
|
||||
}
|
||||
return inits.get(localId) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether `name` follows the `%<class>.<member>` convention of class
|
||||
* methods, accessors and constructors for one of `classNames`. */
|
||||
function isClassMember(name: string, classNames: ReadonlySet<string>): boolean {
|
||||
if (!name.startsWith("%")) return false;
|
||||
for (let dot = name.indexOf("."); dot !== -1; dot = name.indexOf(".", dot + 1)) {
|
||||
if (classNames.has(name.slice(1, dot))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** A typed `error.stack` read, or a read on a checked-dynamic value that
|
||||
* can reach an error's own `stack` accessor: the constant key `stack`, or
|
||||
* a computed key when `computedKeys` (the program spells "stack", so a
|
||||
* computed key can name it). A key the program assembles from other
|
||||
* strings or from data it reads keeps the static answer. */
|
||||
function readsStack(expr: IrExpr, computedKeys: boolean): boolean {
|
||||
* can reach an error's own `stack` accessor: one whose key can be "stack"
|
||||
* (StackKeys). */
|
||||
function readsStack(fn: IrFunction, expr: IrExpr, keys: StackKeys): boolean {
|
||||
if (expr.kind === "libCall") return STACK_READS.has(expr.fn);
|
||||
if (expr.kind !== "dynKeyGet") return false;
|
||||
if (expr.key.kind === "strLit") return expr.key.value === "stack";
|
||||
return computedKeys;
|
||||
return keys.mayBeStack(fn, expr.key);
|
||||
}
|
||||
|
||||
/** Whether the module contains the string literal "stack". */
|
||||
@@ -36,10 +155,10 @@ function spellsStack(mod: IrModule): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function containsStackRead(fn: IrFunction, computedKeys: boolean): boolean {
|
||||
function containsStackRead(fn: IrFunction, keys: StackKeys): boolean {
|
||||
return !everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (expr) => !readsStack(expr, computedKeys),
|
||||
expr: (expr) => !readsStack(fn, expr, keys),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,22 +184,14 @@ function visitLiveExprs(body: IrStmt[], visit: (expr: IrExpr) => void): void {
|
||||
* and those reads must not cost every call of the program a frame.
|
||||
* Programs without any stack read answer false without the walk. */
|
||||
export function observesStacks(mod: IrModule): boolean {
|
||||
const computedKeys = spellsStack(mod);
|
||||
if (!mod.functions.some((fn) => containsStackRead(fn, computedKeys))) return false;
|
||||
const keys = new StackKeys(mod);
|
||||
if (!mod.functions.some((fn) => containsStackRead(fn, keys))) return false;
|
||||
const byName = new Map(mod.functions.map((fn) => [fn.name, fn]));
|
||||
const classNames = new Set((mod.classes ?? []).map((cls) => cls.name));
|
||||
const pending: string[] = [...moduleFunctionReferences(mod)];
|
||||
// Class methods, accessors and constructors are reachable through
|
||||
// dispatch the IR does not name per site: all of them are roots.
|
||||
for (const fn of mod.functions) {
|
||||
if (!fn.name.startsWith("%")) continue;
|
||||
for (let dot = fn.name.indexOf("."); dot !== -1; dot = fn.name.indexOf(".", dot + 1)) {
|
||||
if (classNames.has(fn.name.slice(1, dot))) {
|
||||
pending.push(fn.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const fn of mod.functions) if (isClassMember(fn.name, classNames)) pending.push(fn.name);
|
||||
const reached = new Set<string>();
|
||||
let observed = false;
|
||||
while (pending.length > 0 && !observed) {
|
||||
@@ -90,7 +201,7 @@ export function observesStacks(mod: IrModule): boolean {
|
||||
const fn = byName.get(name);
|
||||
if (fn === undefined) continue;
|
||||
visitLiveExprs(fn.body, (expr) => {
|
||||
if (readsStack(expr, computedKeys)) observed = true;
|
||||
if (readsStack(fn, expr, keys)) observed = true;
|
||||
if (expr.kind === "call" && !reached.has(expr.callee)) pending.push(expr.callee);
|
||||
const referenced = exprFunctionReference(expr);
|
||||
if (referenced !== undefined && !reached.has(referenced)) pending.push(referenced);
|
||||
|
||||
@@ -51,6 +51,7 @@ export async function warmNativeCaches(
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
dynBuiltinClass: false,
|
||||
dynBuiltinStream: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -46,6 +46,7 @@ const native: EarlyExecutableNativeFeatures = {
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
dynBuiltinClass: false,
|
||||
dynBuiltinStream: false,
|
||||
parseArgs: false,
|
||||
stream: false,
|
||||
net: false,
|
||||
|
||||
@@ -66,6 +66,8 @@ export interface EarlyExecutableNativeFeatures {
|
||||
dc: boolean;
|
||||
dynAsync: boolean;
|
||||
dynTier?: boolean;
|
||||
streamDyn?: boolean;
|
||||
eventsOnceSignal?: boolean;
|
||||
events: boolean;
|
||||
emitter: boolean;
|
||||
symbol: boolean;
|
||||
@@ -73,6 +75,7 @@ export interface EarlyExecutableNativeFeatures {
|
||||
searchParams: boolean;
|
||||
qs: boolean;
|
||||
dynBuiltinClass: boolean;
|
||||
dynBuiltinStream: boolean;
|
||||
parseArgs: boolean;
|
||||
stream: boolean;
|
||||
net: boolean;
|
||||
@@ -193,6 +196,7 @@ const BOOLEAN_NATIVE_KEYS = [
|
||||
"searchParams",
|
||||
"qs",
|
||||
"dynBuiltinClass",
|
||||
"dynBuiltinStream",
|
||||
"parseArgs",
|
||||
"stream",
|
||||
"net",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as ts from "./ts7/syntax.js";
|
||||
import { tokenStartOfNode } from "./ts7/node-functions.js";
|
||||
|
||||
export interface CjsLexedExports {
|
||||
/** Names Node's lexer detects in THIS file (no reexport resolution). */
|
||||
@@ -30,7 +31,7 @@ function identRunEnd(text: string, pos: number): number {
|
||||
* a `a`-escaped name parses to the same AST as its plain spelling,
|
||||
* but merve reads bytes and stops at the backslash. */
|
||||
function sourceSpellsIdentifier(node: ts.Node, sf: ts.SourceFile): boolean {
|
||||
const ch = sf.text[node.getStart(sf)];
|
||||
const ch = sf.text[tokenStartOfNode(node)];
|
||||
return ch !== undefined && isIdentStartChar(ch);
|
||||
}
|
||||
|
||||
@@ -39,16 +40,16 @@ function sourceSpellsIdentifier(node: ts.Node, sf: ts.SourceFile): boolean {
|
||||
* defineProperty getter's `return IDENT.IDENT` because `this` lexes as an
|
||||
* identifier run — probed), escapes and private names do not. */
|
||||
function isIdentTokenNode(node: ts.Node, sf: ts.SourceFile): boolean {
|
||||
const start = node.getStart(sf);
|
||||
const start = tokenStartOfNode(node);
|
||||
const ch = sf.text[start];
|
||||
return ch !== undefined && isIdentStartChar(ch) && identRunEnd(sf.text, start) === node.getEnd();
|
||||
return ch !== undefined && isIdentStartChar(ch) && identRunEnd(sf.text, start) === node.end;
|
||||
}
|
||||
|
||||
/** The identifier run at the node's START — the whole name when its
|
||||
* spelling is plain, the byte prefix merve consumed when a unicode escape
|
||||
* interrupts the run mid-name. */
|
||||
function identPrefixOf(node: ts.Node, sf: ts.SourceFile): string {
|
||||
const start = node.getStart(sf);
|
||||
const start = tokenStartOfNode(node);
|
||||
return sf.text.slice(start, identRunEnd(sf.text, start));
|
||||
}
|
||||
|
||||
@@ -84,20 +85,17 @@ export function isModuleExports(e: ts.Expression): boolean {
|
||||
* follows — so the recognizer descends the leftmost-child chain while it
|
||||
* still starts where `e` starts. A ParenthesizedExpression breaks the
|
||||
* chain naturally (its child starts after the `(`), matching merve. */
|
||||
function leadingRequireOf(
|
||||
e: ts.Expression,
|
||||
sf: ts.SourceFile,
|
||||
): { spec: string; end: number } | null {
|
||||
const start = e.getStart(sf);
|
||||
function leadingRequireOf(e: ts.Expression): { spec: string; end: number } | null {
|
||||
const start = tokenStartOfNode(e);
|
||||
let cur: ts.Node = e;
|
||||
for (;;) {
|
||||
const spec = bareRequireSpecOf(cur);
|
||||
if (spec !== null && cur.getStart(sf) === start) return { spec, end: cur.getEnd() };
|
||||
if (spec !== null && tokenStartOfNode(cur) === start) return { spec, end: cur.end };
|
||||
let first: ts.Node | undefined;
|
||||
ts.forEachChild(cur, (c) => {
|
||||
first ??= c;
|
||||
});
|
||||
if (first === undefined || first.getStart(sf) !== start) return null;
|
||||
if (first === undefined || tokenStartOfNode(first) !== start) return null;
|
||||
cur = first;
|
||||
}
|
||||
}
|
||||
@@ -179,13 +177,13 @@ function scanTableLiteral(
|
||||
continue; // whitespace before `,` is skipped on the shorthand path
|
||||
}
|
||||
if (ts.isSpreadAssignment(prop)) {
|
||||
const req = leadingRequireOf(prop.expression, sf);
|
||||
const req = leadingRequireOf(prop.expression);
|
||||
let consumedEnd: number;
|
||||
if (req !== null) {
|
||||
reexports.push(req.spec);
|
||||
consumedEnd = req.end;
|
||||
} else if (sourceSpellsIdentifier(prop.expression, sf)) {
|
||||
consumedEnd = identRunEnd(text, prop.expression.getStart(sf));
|
||||
consumedEnd = identRunEnd(text, tokenStartOfNode(prop.expression));
|
||||
} else {
|
||||
return; // `...{}` and friends stop the scan
|
||||
}
|
||||
@@ -213,7 +211,7 @@ function scanTableLiteral(
|
||||
}
|
||||
if (key === null) return; // numeric / computed / template keys stop the scan
|
||||
const value = prop.initializer;
|
||||
const req = leadingRequireOf(value, sf);
|
||||
const req = leadingRequireOf(value);
|
||||
if (req !== null) {
|
||||
// A require VALUE adds the key, records the reexport, and stops
|
||||
// the scan UNCONDITIONALLY — probed: `{ a: require('x'), b: v }`
|
||||
@@ -223,7 +221,7 @@ function scanTableLiteral(
|
||||
reexports.push(req.spec);
|
||||
return;
|
||||
}
|
||||
const start = value.getStart(sf);
|
||||
const start = tokenStartOfNode(value);
|
||||
const ch0 = text[start];
|
||||
if (ch0 === undefined || !isIdentStartChar(ch0)) return; // literal values stop WITHOUT adding the key
|
||||
const consumedEnd = identRunEnd(text, start);
|
||||
@@ -351,8 +349,8 @@ function starExportSpecOf(call: ts.CallExpression, sf: ts.SourceFile): string |
|
||||
if (!sourceSpellsIdentifier(nameNode, sf)) return null;
|
||||
const arg0 = call.arguments.nodes[0];
|
||||
if (arg0 === undefined) return null;
|
||||
if (!byteAdjacentParen(sf, nameNode.getEnd(), arg0.getStart(sf))) return null;
|
||||
return leadingRequireOf(arg0, sf)?.spec ?? null;
|
||||
if (!byteAdjacentParen(sf, nameNode.end, tokenStartOfNode(arg0))) return null;
|
||||
return leadingRequireOf(arg0)?.spec ?? null;
|
||||
}
|
||||
|
||||
/** True when the bytes between a callee name ending at `nameEnd` and its
|
||||
@@ -384,7 +382,7 @@ function starAssignOf(stmt: ts.VariableStatement, sf: ts.SourceFile): [string, s
|
||||
init.arguments.nodes.length >= 1
|
||||
) {
|
||||
const arg0 = init.arguments.nodes[0]!;
|
||||
if (!byteAdjacentParen(sf, init.expression.getEnd(), arg0.getStart(sf))) return null;
|
||||
if (!byteAdjacentParen(sf, init.expression.end, tokenStartOfNode(arg0))) return null;
|
||||
const spec = bareRequireSpecOf(arg0);
|
||||
if (spec !== null) return [decl.name.text, spec];
|
||||
}
|
||||
@@ -717,15 +715,16 @@ export function cjsLexedExportsOfFile(sf: ts.SourceFile): CjsLexedExports {
|
||||
) {
|
||||
exports.add(lhs.argumentExpression.text);
|
||||
} else if (isModuleExports(lhs)) {
|
||||
events.push({ pos: n.getStart(sf), kind: "assign", rhs: n.right });
|
||||
events.push({ pos: tokenStartOfNode(n), kind: "assign", rhs: n.right });
|
||||
}
|
||||
} else if (ts.isCallExpression(n)) {
|
||||
scanDefineProperty(n, sf, exports);
|
||||
if (braceDepth === 0) {
|
||||
const starSpec = starExportSpecOf(n, sf);
|
||||
if (starSpec !== null) events.push({ pos: n.getStart(sf), kind: "spec", spec: starSpec });
|
||||
if (starSpec !== null)
|
||||
events.push({ pos: tokenStartOfNode(n), kind: "spec", spec: starSpec });
|
||||
const loopId = starLoopIdOf(n, sf);
|
||||
if (loopId !== null) events.push({ pos: n.getStart(sf), kind: "loop", id1: loopId });
|
||||
if (loopId !== null) events.push({ pos: tokenStartOfNode(n), kind: "loop", id1: loopId });
|
||||
}
|
||||
} else if (ts.isVariableStatement(n) && braceDepth === 0) {
|
||||
const assign = starAssignOf(n, sf);
|
||||
@@ -745,7 +744,7 @@ export function cjsLexedExportsOfFile(sf: ts.SourceFile): CjsLexedExports {
|
||||
for (const spec of starAssigns.get(ev.id1) ?? []) reexports.push(spec);
|
||||
} else {
|
||||
reexports = []; // every `module.exports =` clears — stars included (probed)
|
||||
const req = leadingRequireOf(ev.rhs, sf);
|
||||
const req = leadingRequireOf(ev.rhs);
|
||||
if (req !== null) {
|
||||
reexports.push(req.spec);
|
||||
} else if (ts.isObjectLiteralExpression(ev.rhs)) {
|
||||
|
||||
@@ -130,7 +130,7 @@ function markerNamed(
|
||||
function nameText(node: ts.Node | undefined): string {
|
||||
if (node === undefined) return "<anonymous>";
|
||||
if (ts.isIdentifier(node) || ts.isPrivateIdentifier(node)) return node.text;
|
||||
return node.getText();
|
||||
return ts.getTextOfNode(node);
|
||||
}
|
||||
|
||||
function contractDiag(
|
||||
|
||||
@@ -73,7 +73,7 @@ export function lifetimeRegionMemberOf(
|
||||
if (
|
||||
!ts.isClassDeclaration(owner) ||
|
||||
owner.name?.text !== "LifetimeRegion" ||
|
||||
!isExperimentalDeclarationFile(owner.getSourceFile(), known)
|
||||
!isExperimentalDeclarationFile(ts.getSourceFileOfNode(owner), known)
|
||||
)
|
||||
return null;
|
||||
const name = access.name.text;
|
||||
@@ -105,7 +105,7 @@ export function internTableMemberOf(
|
||||
if (
|
||||
!ts.isClassDeclaration(owner) ||
|
||||
owner.name?.text !== "InternTable" ||
|
||||
!isExperimentalDeclarationFile(owner.getSourceFile(), known)
|
||||
!isExperimentalDeclarationFile(ts.getSourceFileOfNode(owner), known)
|
||||
)
|
||||
return null;
|
||||
const name = access.name.text;
|
||||
@@ -135,7 +135,7 @@ export function threadSlotsMemberOf(
|
||||
if (
|
||||
!ts.isClassDeclaration(owner) ||
|
||||
owner.name?.text !== "ThreadSlots" ||
|
||||
!isExperimentalDeclarationFile(owner.getSourceFile(), known)
|
||||
!isExperimentalDeclarationFile(ts.getSourceFileOfNode(owner), known)
|
||||
)
|
||||
return null;
|
||||
const name = access.name.text;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { fallbackDtsPath } from "./dts-paths.js";
|
||||
import { Ts7Host } from "./ts7/program-host.js";
|
||||
import { Ts7Api } from "./ts7/rpc-api.js";
|
||||
import { ModuleKind, ModuleResolutionKind } from "./ts7/enums.js";
|
||||
import { getTextOfNode } from "./ts7/node-functions.js";
|
||||
|
||||
test("fork discovery batches only candidate bindings and preserves aliases and shadowing", () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "scriptc-fork-discovery-"));
|
||||
@@ -49,7 +50,7 @@ test("fork discovery batches only candidate bindings and preserves aliases and s
|
||||
["first.ts", "second.ts", "third.ts"].map((name) => join(directory, name)),
|
||||
);
|
||||
expect(batch).toHaveBeenCalledTimes(1);
|
||||
expect(batch.mock.calls[0]![0].map((node) => node.getText())).toEqual([
|
||||
expect(batch.mock.calls[0]![0].map((node) => getTextOfNode(node))).toEqual([
|
||||
"start",
|
||||
"child",
|
||||
"launch",
|
||||
|
||||
@@ -12,7 +12,7 @@ export function isNodeGlobal(program: ts.Program, expression: ts.Identifier): bo
|
||||
return (
|
||||
declarations.length > 0 &&
|
||||
declarations.every((declaration) => {
|
||||
const source = declaration.getSourceFile();
|
||||
const source = ts.getSourceFileOfNode(declaration);
|
||||
return (
|
||||
source.isDeclarationFile &&
|
||||
(tsgoPath(source.fileName) === fallbackDtsPath() || isNodeTypesPath(source.fileName))
|
||||
@@ -61,7 +61,7 @@ export function isBuiltinMemberImport(
|
||||
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
|
||||
const target = checker.getAliasedSymbol(symbol);
|
||||
const targetDecl = target.declarations[0];
|
||||
if (targetDecl !== undefined && targetDecl.getSourceFile().isDeclarationFile) {
|
||||
if (targetDecl !== undefined && ts.getSourceFileOfNode(targetDecl).isDeclarationFile) {
|
||||
for (
|
||||
let parent: ts.Node | undefined = targetDecl.parent;
|
||||
parent !== undefined && !ts.isSourceFile(parent);
|
||||
@@ -69,7 +69,8 @@ export function isBuiltinMemberImport(
|
||||
) {
|
||||
if (ts.isModuleDeclaration(parent) && ts.isStringLiteral(parent.name)) {
|
||||
return (
|
||||
canonicalBuiltinModule(parent.name.text) === moduleName && target.name === memberName
|
||||
canonicalBuiltinModule(parent.name.text) === moduleName &&
|
||||
ts.symbolName(target) === memberName
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -188,7 +189,7 @@ function staticString(
|
||||
): string | null {
|
||||
const current = strip(expr);
|
||||
if (ts.isStringLiteralLike(current)) return current.text;
|
||||
if (importMetaUrl(current)) return pathToFileURL(current.getSourceFile().fileName).href;
|
||||
if (importMetaUrl(current)) return pathToFileURL(ts.getSourceFileOfNode(current).fileName).href;
|
||||
if (ts.isTemplateExpression(current)) {
|
||||
let value = current.head.text;
|
||||
for (const span of current.templateSpans.nodes) {
|
||||
|
||||
@@ -16,7 +16,7 @@ export function inferredJsDiagnosticSuppressed(
|
||||
const checker = program.getTypeChecker();
|
||||
let suppressed = false;
|
||||
ts.walkPreorder(file, (node) => {
|
||||
if (node.getEnd() < diagnostic.pos || node.getStart(file) > diagnostic.end) return "skip";
|
||||
if (node.end < diagnostic.pos || ts.tokenStartOfNode(node) > diagnostic.end) return "skip";
|
||||
if (
|
||||
diagnostic.code === 2565 &&
|
||||
isJsSourceFileName(file.fileName) &&
|
||||
@@ -30,7 +30,7 @@ export function inferredJsDiagnosticSuppressed(
|
||||
ts.isPropertyDeclaration(decl) &&
|
||||
!decl.type &&
|
||||
!decl.initializer &&
|
||||
!/@type\b/.test(file.text.slice(decl.pos, decl.getStart(file))),
|
||||
!/@type\b/.test(file.text.slice(decl.pos, ts.tokenStartOfNode(decl))),
|
||||
)
|
||||
) {
|
||||
// Bare JS fields exist with value undefined before the first write;
|
||||
@@ -44,14 +44,14 @@ export function inferredJsDiagnosticSuppressed(
|
||||
const declaration = signature && signature.declaration;
|
||||
if (
|
||||
declaration &&
|
||||
isJsSourceFileName(declaration.getSourceFile().fileName) &&
|
||||
isJsSourceFileName(ts.getSourceFileOfNode(declaration).fileName) &&
|
||||
(ts.isFunctionDeclaration(declaration) ||
|
||||
ts.isFunctionExpression(declaration) ||
|
||||
ts.isMethodDeclaration(declaration)) &&
|
||||
node.arguments.nodes.length > declaration.parameters.nodes.length &&
|
||||
node.arguments.nodes[declaration.parameters.nodes.length]?.getStart(file) ===
|
||||
ts.tokenStartOfNode(node.arguments.nodes[declaration.parameters.nodes.length]!) ===
|
||||
diagnostic.pos &&
|
||||
node.arguments.nodes[node.arguments.nodes.length - 1]?.getEnd() === diagnostic.end &&
|
||||
node.arguments.nodes[node.arguments.nodes.length - 1]!.end === diagnostic.end &&
|
||||
bodyReadsArguments(declaration)
|
||||
) {
|
||||
suppressed = true;
|
||||
@@ -68,7 +68,7 @@ export function inferredJsDiagnosticSuppressed(
|
||||
const iterable = checker.getTypeAtLocation(node.expression);
|
||||
const iterator = checker
|
||||
.getPropertiesOfType(iterable)
|
||||
.find((property) => property.name.startsWith("__@iterator@"));
|
||||
.find((property) => ts.symbolName(property).startsWith("__@iterator@"));
|
||||
if (!iterator) return undefined;
|
||||
const signatures = checker.getCallSignatures(
|
||||
checker.getTypeOfSymbolAtLocation(iterator, node.expression),
|
||||
@@ -88,7 +88,7 @@ export function inferredJsDiagnosticSuppressed(
|
||||
declaration &&
|
||||
ts.isMethodDeclaration(declaration) &&
|
||||
declaration.type === undefined &&
|
||||
isJsSourceFileName(declaration.getSourceFile().fileName) &&
|
||||
isJsSourceFileName(ts.getSourceFileOfNode(declaration).fileName) &&
|
||||
declaration.body?.statements?.nodes.length === 1 &&
|
||||
ts.isThrowStatement(declaration.body.statements.nodes[0]!)
|
||||
) {
|
||||
|
||||
@@ -115,7 +115,7 @@ const CONTRACT_GLOBAL_TYPES = new Set(["Array", "ReadonlyArray", "Uint8Array"]);
|
||||
type TypeBindings = Map<ts.SourceFile, Set<string>>;
|
||||
|
||||
function locOf(file: ts.SourceFile, node: ts.Node): SrcLoc {
|
||||
return { file: file.fileName, start: node.getStart(), end: node.end };
|
||||
return { file: file.fileName, start: ts.tokenStartOfNode(node), end: node.end };
|
||||
}
|
||||
|
||||
function isExported(stmt: ts.Node): boolean {
|
||||
@@ -248,7 +248,7 @@ function typeShape(
|
||||
const lit = node.literal;
|
||||
if (ts.isStringLiteral(lit)) return { k: "stringLit", text: lit.text };
|
||||
if (lit.kind === ts.SyntaxKind.NullKeyword) return { k: "absent", unit: "null" };
|
||||
return { k: "unsupported", text: node.getText(file) };
|
||||
return { k: "unsupported", text: ts.getTextOfNode(node) };
|
||||
}
|
||||
if (ts.isArrayTypeNode(node))
|
||||
return { k: "array", elem: typeShape(file, node.elementType, bindings) };
|
||||
@@ -280,15 +280,19 @@ function typeShape(
|
||||
if (ts.isConditionalTypeNode(node)) {
|
||||
return {
|
||||
k: "unsupported",
|
||||
text: `a conditional type (${node.getText(file)})`,
|
||||
text: `a conditional type (${ts.getTextOfNode(node)})`,
|
||||
computed: "conditional",
|
||||
};
|
||||
}
|
||||
if (ts.isMappedTypeNode(node)) {
|
||||
return { k: "unsupported", text: `a mapped type (${node.getText(file)})`, computed: "mapped" };
|
||||
return {
|
||||
k: "unsupported",
|
||||
text: `a mapped type (${ts.getTextOfNode(node)})`,
|
||||
computed: "mapped",
|
||||
};
|
||||
}
|
||||
if (ts.isTypeReferenceNode(node)) {
|
||||
if (!ts.isIdentifier(node.typeName)) return { k: "unsupported", text: node.getText(file) };
|
||||
if (!ts.isIdentifier(node.typeName)) return { k: "unsupported", text: ts.getTextOfNode(node) };
|
||||
const name = node.typeName.text;
|
||||
const global =
|
||||
CONTRACT_GLOBAL_TYPES.has(name) && isUnshadowedGlobalType(file, node, name, bindings);
|
||||
@@ -302,10 +306,10 @@ function typeShape(
|
||||
return { k: "array", elem: typeShape(file, node.typeArguments.nodes[0]!, bindings) };
|
||||
}
|
||||
if ((node.typeArguments?.nodes.length ?? 0) > 0)
|
||||
return { k: "unsupported", text: node.getText(file) };
|
||||
return { k: "unsupported", text: ts.getTextOfNode(node) };
|
||||
return { k: "ref", name };
|
||||
}
|
||||
return { k: "unsupported", text: node.getText(file) };
|
||||
return { k: "unsupported", text: ts.getTextOfNode(node) };
|
||||
}
|
||||
|
||||
/** Unwrap `expr as const` / parenthesized initializers. */
|
||||
@@ -362,7 +366,7 @@ function envMsgArray(expr: ts.Expression): { env: string; msg: string }[] | null
|
||||
|
||||
/** One mergeable declaration site, `file:line` (1-based line). */
|
||||
function siteOf(file: ts.SourceFile, node: ts.Node): string {
|
||||
return `${file.fileName}:${ts.getLineAndCharacterOfPosition(file, node.getStart()).line + 1}`;
|
||||
return `${file.fileName}:${ts.getLineAndCharacterOfPosition(file, ts.tokenStartOfNode(node)).line + 1}`;
|
||||
}
|
||||
|
||||
/** Everything the sidecar projection needs from the program's syntax
|
||||
|
||||
@@ -35,7 +35,7 @@ export function entryFunctionExports(entry: ts.SourceFile): Map<string, EntryExp
|
||||
if (out.has(name) && stmt.body === undefined) continue; // overload signature
|
||||
out.set(name, {
|
||||
name,
|
||||
loc: { file: entry.fileName, start: stmt.getStart(), end: stmt.end },
|
||||
loc: { file: entry.fileName, start: ts.tokenStartOfNode(stmt), end: stmt.end },
|
||||
generic: (stmt.typeParameters?.nodes.length ?? 0) > 0,
|
||||
async: mods?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true,
|
||||
generator: stmt.asteriskToken !== undefined,
|
||||
|
||||
@@ -194,7 +194,7 @@ function onlyMethodCallReferences(lowerer: Lowerer, name: ts.Identifier): boolea
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(name.getSourceFile());
|
||||
visit(ts.getSourceFileOfNode(name));
|
||||
return ok;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ export function extendAssertedRecordMembers(
|
||||
symbol.declarations.length > 0 &&
|
||||
symbol.declarations.every(
|
||||
(d) =>
|
||||
!lowerer.isStdlibFile(d.getSourceFile()) &&
|
||||
!lowerer.isStdlibFile(ts.getSourceFileOfNode(d)) &&
|
||||
(ts.isTypeLiteralNode(d) ||
|
||||
ts.isInterfaceDeclaration(d) ||
|
||||
ts.isTypeAliasDeclaration(d)),
|
||||
|
||||
@@ -16,7 +16,7 @@ export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: t
|
||||
let written = false;
|
||||
// Text pre-check keeps the file walk cheap: only same-named
|
||||
// identifiers pay a symbol resolution.
|
||||
const symText = sym.name;
|
||||
const symText = ts.symbolName(sym);
|
||||
const namesSym = (e: ts.Node): boolean =>
|
||||
ts.isIdentifier(e) && e.text === symText && lowerer.resolveValueSymbol(e) === sym;
|
||||
const scanTarget = (t: ts.Expression): void => {
|
||||
@@ -31,7 +31,7 @@ export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: t
|
||||
if (ts.isArrayLiteralExpression(e) || ts.isObjectLiteralExpression(e)) {
|
||||
const walk = (n: ts.Node): void => {
|
||||
if (namesSym(n)) written = true;
|
||||
else n.forEachChild(walk);
|
||||
else ts.forEachChild(n, walk);
|
||||
};
|
||||
walk(e);
|
||||
}
|
||||
@@ -54,9 +54,9 @@ export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: t
|
||||
) {
|
||||
scanTarget(n.initializer as ts.Expression);
|
||||
}
|
||||
n.forEachChild(visit);
|
||||
ts.forEachChild(n, visit);
|
||||
};
|
||||
decl.getSourceFile().forEachChild(visit);
|
||||
ts.forEachChild(ts.getSourceFileOfNode(decl), visit);
|
||||
lowerer.neverReassignedCache.set(sym, !written);
|
||||
return !written;
|
||||
}
|
||||
@@ -123,7 +123,7 @@ export function nullishValueUnitOf(
|
||||
!decl ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
!ts.isIdentifier(decl.name) ||
|
||||
decl.getSourceFile().isDeclarationFile ||
|
||||
ts.getSourceFileOfNode(decl).isDeclarationFile ||
|
||||
decl.initializer === undefined ||
|
||||
!ts.isVariableStatement(decl.parent?.parent)
|
||||
) {
|
||||
@@ -154,7 +154,7 @@ export function nullishValueUnitOf(
|
||||
/** True when any identifier resolving to `sym` sits inside a class
|
||||
* heritage clause anywhere in the program. */
|
||||
function usedInHeritageClause(lowerer: Lowerer, sym: ts.Symbol): boolean {
|
||||
const symText = sym.name;
|
||||
const symText = ts.symbolName(sym);
|
||||
let found = false;
|
||||
const visit = (n: ts.Node): void => {
|
||||
if (found) return;
|
||||
@@ -171,12 +171,12 @@ function usedInHeritageClause(lowerer: Lowerer, sym: ts.Symbol): boolean {
|
||||
}
|
||||
return;
|
||||
}
|
||||
n.forEachChild(visit);
|
||||
ts.forEachChild(n, visit);
|
||||
};
|
||||
for (const file of lowerer.program.getImplementationSourceFiles()) {
|
||||
if (found) break;
|
||||
if (file.isDeclarationFile) continue;
|
||||
file.forEachChild(visit);
|
||||
ts.forEachChild(file, visit);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
@@ -236,7 +236,7 @@ export function nullishGenericBindingUnitOf(
|
||||
* value knowable. Compound assignments, ++/--, for-in/of cursors, and
|
||||
* destructuring targets all disqualify. */
|
||||
function allWritesNullish(lowerer: Lowerer, sym: ts.Symbol, decl: ts.Node): boolean {
|
||||
const symText = sym.name;
|
||||
const symText = ts.symbolName(sym);
|
||||
const namesSym = (e: ts.Node): boolean =>
|
||||
ts.isIdentifier(e) && e.text === symText && lowerer.resolveValueSymbol(e) === sym;
|
||||
let ok = true;
|
||||
@@ -253,7 +253,7 @@ function allWritesNullish(lowerer: Lowerer, sym: ts.Symbol, decl: ts.Node): bool
|
||||
} else if (ts.isArrayLiteralExpression(lhs) || ts.isObjectLiteralExpression(lhs)) {
|
||||
const walk = (m: ts.Node): void => {
|
||||
if (namesSym(m)) ok = false;
|
||||
else m.forEachChild(walk);
|
||||
else ts.forEachChild(m, walk);
|
||||
};
|
||||
walk(lhs);
|
||||
}
|
||||
@@ -274,9 +274,9 @@ function allWritesNullish(lowerer: Lowerer, sym: ts.Symbol, decl: ts.Node): bool
|
||||
t = (t as ts.ParenthesizedExpression).expression;
|
||||
if (namesSym(t)) ok = false;
|
||||
}
|
||||
n.forEachChild(visit);
|
||||
ts.forEachChild(n, visit);
|
||||
};
|
||||
decl.getSourceFile().forEachChild(visit);
|
||||
ts.forEachChild(ts.getSourceFileOfNode(decl), visit);
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ export function deadUnmappableBinding(
|
||||
// the no-storage family never claims it (catch bindings sit outside a
|
||||
// variable statement too and stay out the same way).
|
||||
if (!ts.isVariableStatement(decl.parent?.parent)) return false;
|
||||
const sf = decl.getSourceFile();
|
||||
const sf = ts.getSourceFileOfNode(decl);
|
||||
if (sf.isDeclarationFile || isJsSourceFile(sf)) return false;
|
||||
if (decl.initializer !== undefined && !sideEffectFreeValueExpr(lowerer, decl.initializer))
|
||||
return false;
|
||||
@@ -344,7 +344,7 @@ export function deadUnmappableBinding(
|
||||
// bug), so only survivors of the syntactic filters pay it. Mappable
|
||||
// types keep their real storage.
|
||||
if (lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(sym)) !== null) return false;
|
||||
const symText = sym.name;
|
||||
const symText = ts.symbolName(sym);
|
||||
const namesSym = (e: ts.Node): boolean =>
|
||||
ts.isIdentifier(e) && e.text === symText && lowerer.resolveValueSymbol(e) === sym;
|
||||
let dead = true;
|
||||
@@ -353,7 +353,7 @@ export function deadUnmappableBinding(
|
||||
if (ts.isIdentifier(n) && n.text === symText) {
|
||||
// Declaration-name occurrences are not reads.
|
||||
if (n.parent !== undefined && ts.isVariableDeclaration(n.parent) && n.parent.name === n) {
|
||||
n.forEachChild(visit);
|
||||
ts.forEachChild(n, visit);
|
||||
return;
|
||||
}
|
||||
// A plain-assignment LHS is a WRITE — dead only when the RHS
|
||||
@@ -375,12 +375,12 @@ export function deadUnmappableBinding(
|
||||
if (namesSym(n)) dead = false;
|
||||
return;
|
||||
}
|
||||
n.forEachChild(visit);
|
||||
ts.forEachChild(n, visit);
|
||||
};
|
||||
for (const file of lowerer.program.getImplementationSourceFiles()) {
|
||||
if (!dead) break;
|
||||
if (file.isDeclarationFile) continue;
|
||||
file.forEachChild(visit);
|
||||
ts.forEachChild(file, visit);
|
||||
}
|
||||
if (dead) lowerer.deadBindings.add(sym);
|
||||
return dead;
|
||||
|
||||
@@ -25,7 +25,7 @@ export function builtinFunctionFenceValue(
|
||||
`library functions as values ('${display}' has no function-value lowering; call it directly)`,
|
||||
);
|
||||
lowerer.runtimeFences.push(diag);
|
||||
const line = ts.getLineAndCharacterOfPosition(node.getSourceFile(), loc.start).line + 1;
|
||||
const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(node), loc.start).line + 1;
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [],
|
||||
|
||||
@@ -35,7 +35,7 @@ export function lowerDiagnosticsSubscriber(lowerer: Lowerer, node: ts.Expression
|
||||
if (ts.isIdentifier(node) && node.text === "setImmediate") {
|
||||
const sym = lowerer.checker.getSymbolAtLocation(node);
|
||||
const decls = sym ? sym.declarations : [];
|
||||
if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(d.getSourceFile()))) {
|
||||
if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d)))) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "timers.setImmediateFnValue",
|
||||
|
||||
@@ -52,10 +52,10 @@ export function blobClassOf(lowerer: Lowerer, node: ts.Expression): BlobClass |
|
||||
function blobReceiverClass(lowerer: Lowerer, node: ts.Expression): BlobClass | null {
|
||||
const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node));
|
||||
const sym = type.getAliasSymbol() ?? type.getSymbol();
|
||||
if (!sym || (sym.name !== "Blob" && sym.name !== "File") || !lowerer.isStdlibSymbol(sym)) {
|
||||
return null;
|
||||
}
|
||||
return sym.name;
|
||||
if (!sym) return null;
|
||||
const name = ts.symbolName(sym);
|
||||
if ((name !== "Blob" && name !== "File") || !lowerer.isStdlibSymbol(sym)) return null;
|
||||
return name;
|
||||
}
|
||||
|
||||
function memberNameOf(access: ts.PropertyAccessExpression | ts.ElementAccessExpression) {
|
||||
|
||||
@@ -111,8 +111,18 @@ export function builtinClassDynValueOf(lowerer: Lowerer, node: ts.Expression): I
|
||||
};
|
||||
}
|
||||
|
||||
/** EventEmitter and the legacy Stream are dyn.builtinClass; the stream
|
||||
* classes, which carry native stream state for constructor functions, are
|
||||
* dyn.builtinStreamClass, so only programs that use them link the stream
|
||||
* half. */
|
||||
export function builtinClassValue(kind: number, loc: SrcLoc): IrExpr {
|
||||
return { kind: "libCall", fn: "dyn.builtinClass", args: [numLit(kind, loc)], type: DYN, loc };
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: kind <= 1 ? "dyn.builtinClass" : "dyn.builtinStreamClass",
|
||||
args: [numLit(kind, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `Base.call(this, …)` / `Base.apply(this, args)` on one of those class
|
||||
@@ -130,7 +140,7 @@ export function lowerBuiltinClassCall(lowerer: Lowerer, call: ts.CallExpression)
|
||||
kind: "dynInvoke",
|
||||
recv: receiver,
|
||||
method,
|
||||
calleeName: callee.getText(),
|
||||
calleeName: ts.getTextOfNode(callee),
|
||||
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
||||
type: DYN,
|
||||
loc,
|
||||
|
||||
@@ -50,7 +50,7 @@ export function registerBuiltinCallableAlias(
|
||||
// These checked native functions retain a runtime slot in JavaScript so
|
||||
// aliases can escape without narrowing their arguments to string.
|
||||
if (
|
||||
isJsSourceFile(nameNode.getSourceFile()) &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(nameNode)) &&
|
||||
target.module === "util" &&
|
||||
["stripVTControlCharacters", "toUSVString", "isDeepStrictEqual", "styleText"].includes(
|
||||
target.member,
|
||||
|
||||
@@ -122,12 +122,14 @@ export function lowerChildMethodCall(
|
||||
}
|
||||
const listenerNode = stripTypeCasts(call.arguments.nodes[1]!);
|
||||
if (
|
||||
isJsSourceFile(listenerNode.getSourceFile()) &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(listenerNode)) &&
|
||||
(ts.isArrowFunction(listenerNode) || ts.isFunctionExpression(listenerNode)) &&
|
||||
listenerNode.parameters.nodes.some((param) => param.dotDotDotToken) &&
|
||||
listenerNode.parameters.nodes.every((param) => !param.type) &&
|
||||
!/@(?:param|type)\b/.test(
|
||||
listenerNode.getSourceFile().text.slice(listenerNode.pos, listenerNode.getStart()),
|
||||
ts
|
||||
.getSourceFileOfNode(listenerNode)
|
||||
.text.slice(listenerNode.pos, ts.tokenStartOfNode(listenerNode)),
|
||||
)
|
||||
) {
|
||||
for (const param of listenerNode.parameters.nodes) lowerer.checkedCallbackParams.add(param);
|
||||
@@ -142,10 +144,10 @@ export function lowerChildMethodCall(
|
||||
: lowerer.lowerExpr(call.arguments.nodes[1]!);
|
||||
if (
|
||||
narrowed.length > 0 ||
|
||||
(isJsSourceFile(listenerNode.getSourceFile()) && cb.type.kind === "dyn") ||
|
||||
(isJsSourceFile(ts.getSourceFileOfNode(listenerNode)) && cb.type.kind === "dyn") ||
|
||||
(cb.type.kind === "func" &&
|
||||
(cb.type.rest ||
|
||||
(isJsSourceFile(listenerNode.getSourceFile()) &&
|
||||
(isJsSourceFile(ts.getSourceFileOfNode(listenerNode)) &&
|
||||
cb.type.params.some((param) => param.kind === "dyn"))))
|
||||
) {
|
||||
if (
|
||||
@@ -756,7 +758,7 @@ function childStreamEncodedAt(
|
||||
* admit null. */
|
||||
function narrowedExitListenerParams(lowerer: Lowerer, listener: ts.Expression): ts.Symbol[] {
|
||||
if (!ts.isArrowFunction(listener) && !ts.isFunctionExpression(listener)) return [];
|
||||
if (isJsSourceFile(listener.getSourceFile())) return [];
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(listener))) return [];
|
||||
const symbols: ts.Symbol[] = [];
|
||||
const wanted: readonly string[] = ["f64", "string"];
|
||||
listener.parameters.nodes.slice(0, 2).forEach((param, i) => {
|
||||
|
||||
@@ -97,7 +97,7 @@ export function builtinClassAliasOwnsNoSlot(lowerer: Lowerer, symbol: ts.Symbol)
|
||||
if (decl === undefined || !ts.isVariableDeclaration(decl)) return false;
|
||||
if (builtinMemberRequireDecl(decl.name, decl.initializer)) return false;
|
||||
if (streamClassAliasDecl(lowerer, decl.name, decl.initializer)) return false;
|
||||
if (!isJsSourceFile(decl.getSourceFile())) return true;
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(decl))) return true;
|
||||
let init = decl.initializer;
|
||||
while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (init === undefined) return true;
|
||||
@@ -157,9 +157,10 @@ export function runtimeClassValueOfType(lowerer: Lowerer, type: ts.Type): string
|
||||
const symbol = type.getSymbol();
|
||||
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
|
||||
let info = lowerer.builtinEmitterInfoOf(symbol) ?? lowerer.builtinStreamInfoOf(symbol);
|
||||
if (!info && symbol.name.endsWith("Constructor")) {
|
||||
if (!info && ts.symbolName(symbol).endsWith("Constructor")) {
|
||||
for (const [irName, rec] of RUNTIME_ERROR_CLASSES) {
|
||||
if (`${rec.lib}Constructor` === symbol.name) info = lowerer.classes.get(irName) ?? null;
|
||||
if (`${rec.lib}Constructor` === ts.symbolName(symbol))
|
||||
info = lowerer.classes.get(irName) ?? null;
|
||||
}
|
||||
}
|
||||
return info?.valueCtorParams !== undefined ? info.def.name : null;
|
||||
|
||||
@@ -136,7 +136,7 @@ export function zlibStreamModeOf(
|
||||
if (alias !== null) return alias.kind === "zlib" ? alias.mode : null;
|
||||
const target =
|
||||
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
|
||||
const mode = ZLIB_STREAM_MODES.get(target.name);
|
||||
const mode = ZLIB_STREAM_MODES.get(ts.symbolName(target));
|
||||
if (mode === undefined) return null;
|
||||
return target.declarations.some((d) => ts.isClassDeclaration(d) && isZlibFallbackDecl(lowerer, d))
|
||||
? mode
|
||||
@@ -144,7 +144,7 @@ export function zlibStreamModeOf(
|
||||
}
|
||||
|
||||
function isZlibFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean {
|
||||
const file = d.getSourceFile();
|
||||
const file = ts.getSourceFileOfNode(d);
|
||||
if (!lowerer.isStdlibFile(file) || isNodeTypesPath(file.fileName)) return false;
|
||||
for (let node: ts.Node | undefined = d.parent; node; node = node.parent) {
|
||||
if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
|
||||
|
||||
@@ -170,7 +170,7 @@ export function fsConstantMemberRead(expr: ts.PropertyAccessExpression): IrExpr
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
return isJsSourceFile(expr.getSourceFile())
|
||||
return isJsSourceFile(ts.getSourceFileOfNode(expr))
|
||||
? read
|
||||
: { kind: "dynCheck", value: read, type: F64, loc };
|
||||
}
|
||||
|
||||
@@ -240,7 +240,9 @@ function cryptoKdfInput(lowerer: Lowerer, node: ts.Expression): IrExpr {
|
||||
const symbol = lowerer.typeOf(node).getSymbol();
|
||||
if (
|
||||
type?.kind === "bytes" ||
|
||||
(symbol?.name === "ArrayBuffer" && lowerer.isStdlibSymbol(symbol))
|
||||
(symbol !== undefined &&
|
||||
ts.symbolName(symbol) === "ArrayBuffer" &&
|
||||
lowerer.isStdlibSymbol(symbol))
|
||||
) {
|
||||
const value = lowerer.coerceInto(node, lowerer.lowerExpr(node), DYN);
|
||||
return { kind: "libCall", fn: "bytes.bufferSource", args: [value], type: BYTES_U8, loc };
|
||||
@@ -878,8 +880,8 @@ export function isCipherType(lowerer: Lowerer, type: ts.Type): boolean {
|
||||
const symbol = type.getAliasSymbol() ?? type.getSymbol();
|
||||
return (
|
||||
!!symbol &&
|
||||
CIPHER_TYPE_NAMES.has(symbol.name) &&
|
||||
symbol.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile()))
|
||||
CIPHER_TYPE_NAMES.has(ts.symbolName(symbol)) &&
|
||||
symbol.declarations.some((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d)))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -898,7 +900,7 @@ function v8CalleeText(node: ts.Expression): string {
|
||||
if (ts.isPropertyAccessExpression(node))
|
||||
return `${v8CalleeText(node.expression)}.${node.name.text}`;
|
||||
if (ts.isCallExpression(node)) return `${v8CalleeText(node.expression)}(...)`;
|
||||
return node.getText();
|
||||
return ts.getTextOfNode(node);
|
||||
}
|
||||
|
||||
/** Calls on native Cipheriv/Decipheriv handles dispatch through the
|
||||
@@ -959,7 +961,7 @@ export function lowerCryptoHashMethodCall(
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: evaluated,
|
||||
result: nodeThrowExpr(1, "", `${access.getText()} is not a function`, DYN, loc),
|
||||
result: nodeThrowExpr(1, "", `${ts.getTextOfNode(access)} is not a function`, DYN, loc),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
|
||||
@@ -9,7 +9,12 @@ import { varRef } from "../../../ir/build.js";
|
||||
function isStdlibDisposableStack(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node));
|
||||
for (const symbol of [type.getSymbol(), type.getAliasSymbol()]) {
|
||||
if (symbol?.name === "DisposableStack" && lowerer.isStdlibSymbol(symbol)) return true;
|
||||
if (
|
||||
symbol !== undefined &&
|
||||
ts.symbolName(symbol) === "DisposableStack" &&
|
||||
lowerer.isStdlibSymbol(symbol)
|
||||
)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -27,9 +27,10 @@ const ASSERTION_ERROR_PROPERTIES: ReadonlySet<string> = new Set([
|
||||
export function isExecExceptionType(lowerer: Lowerer, t: ts.Type): boolean {
|
||||
const sym = t.getSymbol();
|
||||
return (
|
||||
sym?.name === "ExecException" &&
|
||||
sym !== undefined &&
|
||||
ts.symbolName(sym) === "ExecException" &&
|
||||
sym.declarations.some(
|
||||
(d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
|
||||
(d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ export function lowerFsLadderCall(
|
||||
);
|
||||
}
|
||||
if (bi.module !== "fs" && bi.module !== "fs/promises") return null;
|
||||
if (!isJsSourceFile(expr.getSourceFile())) return null;
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null;
|
||||
const args = expr.arguments.nodes;
|
||||
if (args.some(ts.isSpreadElement)) return null;
|
||||
// Every ladder argument crosses as a dyn value; an argument that
|
||||
@@ -1564,7 +1564,7 @@ export function lowerDirentMethodCall(
|
||||
if (call.questionDotToken || access.questionDotToken) return null;
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
const recvSym = lowerer.typeOf(access.expression).getSymbol();
|
||||
if (recvSym?.name !== "Dirent") return null;
|
||||
if (recvSym === undefined || ts.symbolName(recvSym) !== "Dirent") return null;
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
if (receiver.type.kind !== "record") return null;
|
||||
const shape = lowerer.shapes.get(receiver.type.shapeId);
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from "../../../ir/ir.js";
|
||||
|
||||
function isFsFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean {
|
||||
const file = d.getSourceFile();
|
||||
const file = ts.getSourceFileOfNode(d);
|
||||
if (!lowerer.isStdlibFile(file) || isNodeTypesPath(file.fileName)) return false;
|
||||
for (let node: ts.Node | undefined = d.parent; node; node = node.parent) {
|
||||
if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
|
||||
@@ -153,11 +153,13 @@ export function fsStreamClassOf(
|
||||
lowerer: Lowerer,
|
||||
symbol: ts.Symbol | null | undefined,
|
||||
): "ReadStream" | "WriteStream" | null {
|
||||
if (!symbol || (symbol.name !== "ReadStream" && symbol.name !== "WriteStream")) return null;
|
||||
if (!symbol) return null;
|
||||
const name = ts.symbolName(symbol);
|
||||
if (name !== "ReadStream" && name !== "WriteStream") return null;
|
||||
const declared = symbol.declarations.some(
|
||||
(d) => ts.isClassDeclaration(d) && isFsFallbackDecl(lowerer, d),
|
||||
);
|
||||
return declared ? symbol.name : null;
|
||||
return declared ? name : null;
|
||||
}
|
||||
|
||||
/** `new fs.ReadStream(path, options)` / `new fs.WriteStream(...)`: the
|
||||
|
||||
@@ -504,7 +504,7 @@ function jsonNullishArgument(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(node);
|
||||
return (
|
||||
symbol !== undefined &&
|
||||
symbol.declarations.every((decl) => lowerer.isStdlibFile(decl.getSourceFile()))
|
||||
symbol.declarations.every((decl) => lowerer.isStdlibFile(ts.getSourceFileOfNode(decl)))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export function isKeyObjectClass(lowerer: Lowerer, node: ts.Expression): boolean
|
||||
export function isKeyObjectValue(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
const type = lowerer.typeOf(node);
|
||||
const sym = type.getAliasSymbol() ?? type.getSymbol();
|
||||
return !!sym && sym.name === "KeyObject" && lowerer.isStdlibSymbol(sym);
|
||||
return !!sym && ts.symbolName(sym) === "KeyObject" && lowerer.isStdlibSymbol(sym);
|
||||
}
|
||||
|
||||
/** `v instanceof KeyObject`. */
|
||||
@@ -51,7 +51,7 @@ export function lowerKeyObjectCall(lowerer: Lowerer, call: ts.CallExpression): I
|
||||
kind: "dynInvoke",
|
||||
recv: lowerer.lowerExprExpecting(access.expression, DYN),
|
||||
method: access.name.text,
|
||||
calleeName: access.getText(),
|
||||
calleeName: ts.getTextOfNode(access),
|
||||
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
||||
type: DYN,
|
||||
loc: locOf(call),
|
||||
|
||||
@@ -135,7 +135,7 @@ function builtinModuleBindingOf(
|
||||
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
|
||||
const target = lowerer.checker.getAliasedSymbol(symbol);
|
||||
const tdecl = target.declarations[0];
|
||||
if (tdecl !== undefined && tdecl.getSourceFile().isDeclarationFile) {
|
||||
if (tdecl !== undefined && ts.getSourceFileOfNode(tdecl).isDeclarationFile) {
|
||||
for (
|
||||
let p: ts.Node | undefined = tdecl.parent;
|
||||
p !== undefined && !ts.isSourceFile(p);
|
||||
@@ -143,7 +143,7 @@ function builtinModuleBindingOf(
|
||||
) {
|
||||
if (ts.isModuleDeclaration(p) && ts.isStringLiteral(p.name)) {
|
||||
const module = canonicalBuiltinModule(p.name.text);
|
||||
if (module !== null) return { module, member: target.name };
|
||||
if (module !== null) return { module, member: ts.symbolName(target) };
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -297,14 +297,14 @@ export function createRequireCalleeFileOf(
|
||||
): ts.SourceFile | null {
|
||||
const e = stripTypeCasts(callee);
|
||||
if (ts.isCallExpression(e)) {
|
||||
return createRequireBaseCallOf(lowerer, e) !== null ? e.getSourceFile() : null;
|
||||
return createRequireBaseCallOf(lowerer, e) !== null ? ts.getSourceFileOfNode(e) : null;
|
||||
}
|
||||
if (!ts.isIdentifier(e)) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(e);
|
||||
const decl = symbol ? symbol.declarations[0] : undefined;
|
||||
if (!decl || !ts.isVariableDeclaration(decl) || decl.initializer === undefined) return null;
|
||||
return createRequireBindingDecl(lowerer, decl.name, decl.initializer)
|
||||
? decl.getSourceFile()
|
||||
? ts.getSourceFileOfNode(decl)
|
||||
: null;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export function lowerBuiltinModuleCall(
|
||||
fn: BuiltinModuleFn,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const name = expr.expression.getText();
|
||||
const name = ts.getTextOfNode(expr.expression);
|
||||
if (bi.module === "util/types") {
|
||||
return lowerUtilTypeCall(lowerer, expr, bi, loc, name);
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ export function lowerImportMetaResolveCall(
|
||||
);
|
||||
}
|
||||
const result = resolveImportMetaRuntime(
|
||||
call.getSourceFile().fileName,
|
||||
ts.getSourceFileOfNode(call).fileName,
|
||||
specifier,
|
||||
lowerer.targetPlatform,
|
||||
lowerer.frontendServices,
|
||||
@@ -314,8 +314,8 @@ function requireResolverBaseFile(lowerer: Lowerer, receiver: ts.Expression): ts.
|
||||
const created = createRequireCalleeFileOf(lowerer, receiver);
|
||||
if (created !== null) return created;
|
||||
return lowerer.isStdlibGlobal(receiver, "require") &&
|
||||
!isNodeEsmFile(receiver.getSourceFile(), lowerer.program)
|
||||
? receiver.getSourceFile()
|
||||
!isNodeEsmFile(ts.getSourceFileOfNode(receiver), lowerer.program)
|
||||
? ts.getSourceFileOfNode(receiver)
|
||||
: null;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ export function isNavigatorGlobal(lowerer: Lowerer, node: ts.Expression): boolea
|
||||
if (!ts.isIdentifier(expr) || expr.text !== "navigator") return false;
|
||||
if (lowerer.isStdlibGlobal(expr, "navigator")) return true;
|
||||
return (
|
||||
isJsSourceFile(expr.getSourceFile()) && lowerer.checker.getSymbolAtLocation(expr) === undefined
|
||||
isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
|
||||
lowerer.checker.getSymbolAtLocation(expr) === undefined
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,10 @@ export function netAddressClassOf(lowerer: Lowerer, node: ts.Expression): NetAdd
|
||||
function netAddressValueClass(lowerer: Lowerer, node: ts.Expression): NetAddressClass | null {
|
||||
const type = lowerer.typeOf(node);
|
||||
const sym = type.getAliasSymbol() ?? type.getSymbol();
|
||||
if (!sym || (sym.name !== "SocketAddress" && sym.name !== "BlockList")) return null;
|
||||
return lowerer.isStdlibSymbol(sym) ? sym.name : null;
|
||||
if (!sym) return null;
|
||||
const name = ts.symbolName(sym);
|
||||
if (name !== "SocketAddress" && name !== "BlockList") return null;
|
||||
return lowerer.isStdlibSymbol(sym) ? name : null;
|
||||
}
|
||||
|
||||
/** `new SocketAddress(options?)` and `new BlockList()`: the argument
|
||||
@@ -127,7 +129,7 @@ export function lowerNetAddressCall(lowerer: Lowerer, call: ts.CallExpression):
|
||||
kind: "dynInvoke",
|
||||
recv: lowerer.lowerExprExpecting(access.expression, DYN),
|
||||
method: name,
|
||||
calleeName: access.getText(),
|
||||
calleeName: ts.getTextOfNode(access),
|
||||
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
||||
type: DYN,
|
||||
loc,
|
||||
|
||||
@@ -285,7 +285,7 @@ function lowerPerfArguments(lowerer: Lowerer, call: ts.CallExpression, loc: SrcL
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const spelling = arg.expression.getText();
|
||||
const spelling = ts.getTextOfNode(arg.expression);
|
||||
const items = checkedIterableSpread(lowerer, lowerer.lowerExpr(arg.expression), spelling, loc);
|
||||
stmts.push({
|
||||
kind: "exprStmt",
|
||||
@@ -527,7 +527,11 @@ export function fencePerfEntryKeyWalk(
|
||||
* receiver's checker type. Its value is the runtime histogram handle. */
|
||||
function isIntervalHistogram(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
const symbol = lowerer.typeOf(node).getSymbol();
|
||||
return symbol?.name === "IntervalHistogram" && lowerer.isStdlibSymbol(symbol);
|
||||
return (
|
||||
symbol !== undefined &&
|
||||
ts.symbolName(symbol) === "IntervalHistogram" &&
|
||||
lowerer.isStdlibSymbol(symbol)
|
||||
);
|
||||
}
|
||||
|
||||
/** IntervalHistogram's numeric members, as perf.eldStat selectors. */
|
||||
|
||||
@@ -192,14 +192,14 @@ export function lowerProcStreamMethodCall(
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
const name = access.name.text;
|
||||
const loc = locOf(call);
|
||||
if (isJsSourceFile(call.getSourceFile())) {
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(call))) {
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
if (receiver.type.kind === "dyn") {
|
||||
return {
|
||||
kind: "dynInvoke",
|
||||
recv: receiver,
|
||||
method: name,
|
||||
calleeName: call.expression.getText(),
|
||||
calleeName: ts.getTextOfNode(call.expression),
|
||||
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
||||
type: DYN,
|
||||
loc,
|
||||
@@ -214,7 +214,7 @@ export function lowerProcStreamMethodCall(
|
||||
kind: "dynInvoke",
|
||||
recv: receiver,
|
||||
method: name,
|
||||
calleeName: call.expression.getText(),
|
||||
calleeName: ts.getTextOfNode(call.expression),
|
||||
args: [],
|
||||
type: DYN,
|
||||
loc,
|
||||
@@ -266,6 +266,58 @@ export function isOptionalProcessStreamProperty(
|
||||
return lowerer.mapTypeOf(lowerer.typeOf(expr.expression))?.kind === "procStream";
|
||||
}
|
||||
|
||||
/** The process.versions entries @types/node declares as plain strings
|
||||
* that the runtime's versions object (node and openssl only) lacks. */
|
||||
const ABSENT_DECLARED_VERSIONS: ReadonlySet<string> = new Set([
|
||||
"http_parser",
|
||||
"v8",
|
||||
"ares",
|
||||
"uv",
|
||||
"zlib",
|
||||
"modules",
|
||||
]);
|
||||
|
||||
/** A process.versions member read whose declaration promises a string the
|
||||
* runtime does not have. @types/node declares the entries above as plain
|
||||
* strings, so a checked string read would throw on the absent entry and
|
||||
* equality with undefined would fold to false. These reads keep the
|
||||
* undefined arm, like the optional stream properties. The shipped fallback
|
||||
* types declare other entries `string | undefined` and never reach this.
|
||||
* An element read with a computed key qualifies when the checker types it
|
||||
* as a plain string. */
|
||||
export function isOptionalProcessVersionsRead(lowerer: Lowerer, node: ts.Node): boolean {
|
||||
let key: string | null;
|
||||
if (ts.isPropertyAccessExpression(node)) key = node.name.text;
|
||||
else if (ts.isElementAccessExpression(node))
|
||||
key = ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null;
|
||||
else return false;
|
||||
if (key !== null && !ABSENT_DECLARED_VERSIONS.has(key)) return false;
|
||||
if (!programMentionsVersions(lowerer)) return false;
|
||||
const symbol = lowerer.typeOf(node.expression).getSymbol();
|
||||
return (
|
||||
symbol !== undefined && ts.symbolName(symbol) === "ProcessVersions" &&
|
||||
symbol.declarations.some(
|
||||
(d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
|
||||
) &&
|
||||
lowerer.mapTypeOf(lowerer.typeOf(node))?.kind === "string"
|
||||
);
|
||||
}
|
||||
|
||||
const mentionsVersions = new WeakMap<Lowerer, boolean>();
|
||||
|
||||
/** Whether any implementation source spells `versions`. Programs that
|
||||
* never do skip the per-read checker queries above entirely. */
|
||||
function programMentionsVersions(lowerer: Lowerer): boolean {
|
||||
let known = mentionsVersions.get(lowerer);
|
||||
if (known === undefined) {
|
||||
known = lowerer.program
|
||||
.getImplementationSourceFiles()
|
||||
.some((sf) => !sf.isDeclarationFile && sf.text.includes("versions"));
|
||||
mentionsVersions.set(lowerer, known);
|
||||
}
|
||||
return known;
|
||||
}
|
||||
|
||||
export function lowerProcessStreamProperty(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
@@ -276,7 +328,7 @@ export function lowerProcessStreamProperty(
|
||||
? lowerer.stdlibGlobalMember(expr.expression, "process")
|
||||
: null;
|
||||
if (
|
||||
isJsSourceFile(expr.getSourceFile()) &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
|
||||
(direct === "stdin" ||
|
||||
direct === "stdout" ||
|
||||
direct === "stderr" ||
|
||||
@@ -584,7 +636,7 @@ export function lowerProcessProperty(
|
||||
}
|
||||
// A stored process.nextTick in JavaScript (`cond ? setImmediate :
|
||||
// process.nextTick`): the interned builtin function value.
|
||||
if (member === "nextTick" && isJsSourceFile(expr.getSourceFile())) {
|
||||
if (member === "nextTick" && isJsSourceFile(ts.getSourceFileOfNode(expr))) {
|
||||
return builtinFunctionValue("process.nextTick", loc);
|
||||
}
|
||||
if (member === "getBuiltinModule") {
|
||||
@@ -667,7 +719,7 @@ export function lowerProcessProperty(
|
||||
// OUTER expressions dispatch first.
|
||||
if (
|
||||
member === "stdin" ||
|
||||
((member === "stdout" || member === "stderr") && isJsSourceFile(expr.getSourceFile()))
|
||||
((member === "stdout" || member === "stderr") && isJsSourceFile(ts.getSourceFileOfNode(expr)))
|
||||
) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
@@ -697,7 +749,10 @@ export function lowerProcessMethodCall(
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (call.questionDotToken) return null;
|
||||
if (isJsSourceFile(call.getSourceFile()) && ts.isPropertyAccessExpression(access.expression)) {
|
||||
if (
|
||||
isJsSourceFile(ts.getSourceFileOfNode(call)) &&
|
||||
ts.isPropertyAccessExpression(access.expression)
|
||||
) {
|
||||
const stream = lowerer.stdlibGlobalMember(access.expression, "process");
|
||||
if (stream === "stdin" || stream === "stdout" || stream === "stderr") {
|
||||
if (call.arguments.nodes.some(ts.isSpreadElement))
|
||||
@@ -706,7 +761,7 @@ export function lowerProcessMethodCall(
|
||||
kind: "dynInvoke",
|
||||
recv: lowerer.lowerExpr(access.expression),
|
||||
method: access.name.text,
|
||||
calleeName: access.getText(),
|
||||
calleeName: ts.getTextOfNode(access),
|
||||
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
||||
type: DYN,
|
||||
loc: locOf(call),
|
||||
@@ -883,7 +938,7 @@ export function lowerProcessMethodCall(
|
||||
loc,
|
||||
},
|
||||
receiver,
|
||||
calleeName: access.getText(),
|
||||
calleeName: ts.getTextOfNode(access),
|
||||
args: [
|
||||
lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN),
|
||||
lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN),
|
||||
@@ -1080,7 +1135,7 @@ export function lowerProcessMethodCall(
|
||||
// `writeOut: (str) => process.stdout.write(str)` — str untyped):
|
||||
// a runtime string writes; anything else throws the dynCheck's
|
||||
// honest TypeError at the call.
|
||||
if (data.type.kind === "dyn" && isJsSourceFile(call.getSourceFile())) {
|
||||
if (data.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(call))) {
|
||||
data = { kind: "dynCheck", value: data, type: STRING, loc };
|
||||
}
|
||||
const isBytes = data.type.kind === "bytes" && data.type.elem === "u8";
|
||||
@@ -1095,7 +1150,7 @@ export function lowerProcessMethodCall(
|
||||
let callback: IrExpr | undefined;
|
||||
if (callbackNode) {
|
||||
callback = lowerer.lowerExpr(callbackNode);
|
||||
if (callback.type.kind === "dyn" && isJsSourceFile(call.getSourceFile())) {
|
||||
if (callback.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(call))) {
|
||||
callback = {
|
||||
kind: "dynCheck",
|
||||
value: callback,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { varRef } from "../../../ir/build.js";
|
||||
import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
@@ -187,7 +188,11 @@ function lowerTuplePromiseAll(
|
||||
* type is the checker's combined promise, and each entry's inner type
|
||||
* must be that inner type, one of its union arms, or a sub-union of it
|
||||
* (the backend's interned adapters wrap/re-tag fulfillments; a wider
|
||||
* entry would need machinery that doesn't exist and fences).
|
||||
* entry would need machinery that doesn't exist and fences). A
|
||||
* Promise<never> entry fits any result, and an entry whose payload widens
|
||||
* into the result's converts. When the checker's combined type has no
|
||||
* representation, the result takes the type the entries that can fulfill
|
||||
* share, or 'unknown'.
|
||||
* Promise.all/allSettled/any fence with the sequential-await hint;
|
||||
* resolve/reject and the rest fall to the member fence. Null for
|
||||
* non-Promise receivers. */
|
||||
@@ -214,14 +219,47 @@ export function lowerPromiseStaticCall(
|
||||
"a non-empty array LITERAL of promises is the supported form: Promise.race([p, q])",
|
||||
);
|
||||
}
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (resultT?.kind !== "promise") {
|
||||
lowerer.noLowering(
|
||||
"Promise.race with this combined result type",
|
||||
call,
|
||||
"the entries' value types must combine into a representable union",
|
||||
);
|
||||
// A Promise<never> entry (the deadline idiom: `new Promise<never>((_,
|
||||
// reject) => setTimeout(reject, ms))`) can only reject, so it races
|
||||
// under any result type; its inner type maps to void.
|
||||
const neverEntry = (el: ts.Expression): boolean => {
|
||||
const t = lowerer.typeOf(el);
|
||||
const arg =
|
||||
ts.symbolNameOf(t.getSymbol()) === "Promise"
|
||||
? lowerer.checker.getTypeArguments(t as ts.TypeReference)[0]
|
||||
: undefined;
|
||||
return arg !== undefined && (arg.flags & ts.TypeFlags.Never) !== 0;
|
||||
};
|
||||
const elements = argNode.elements.nodes;
|
||||
let combined = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
let lowered: IrExpr[] | null = null;
|
||||
if (combined?.kind !== "promise") {
|
||||
// The checker's combined type has no representation (an 'any'
|
||||
// element array such as events.once's any[], or 'void | any[]'): the
|
||||
// result takes the one type the entries that can fulfill share, or
|
||||
// else 'unknown' when each of them widens into it.
|
||||
lowered = elements.map((el) => lowerer.lowerExpr(el));
|
||||
const fulfilling = lowered.filter((_, i) => !neverEntry(elements[i]!));
|
||||
const first = fulfilling[0]?.type;
|
||||
const dynResult: IrType & { kind: "promise" } = { kind: "promise", inner: DYN };
|
||||
if (first?.kind === "promise" && fulfilling.every((e) => typeEquals(e.type, first))) {
|
||||
combined = first;
|
||||
} else if (
|
||||
fulfilling.length > 0 &&
|
||||
fulfilling.every(
|
||||
(e) => e.type.kind === "promise" && lowerer.promisePayloadWidens(e.type, dynResult),
|
||||
)
|
||||
) {
|
||||
combined = dynResult;
|
||||
} else {
|
||||
lowerer.noLowering(
|
||||
"Promise.race with this combined result type",
|
||||
call,
|
||||
"the entries' value types must combine into a representable union",
|
||||
);
|
||||
}
|
||||
}
|
||||
const resultT = combined;
|
||||
const resultArms =
|
||||
resultT.inner.kind === "union"
|
||||
? (lowerer.unions.get(resultT.inner.unionId)?.arms ?? [])
|
||||
@@ -238,19 +276,8 @@ export function lowerPromiseStaticCall(
|
||||
}
|
||||
return resultArms.some((a) => typeEquals(a, inner));
|
||||
};
|
||||
// A Promise<never> entry (the deadline idiom: `new Promise<never>((_,
|
||||
// reject) => setTimeout(reject, ms))`) can only reject, so it races
|
||||
// under any result type; its inner type maps to void.
|
||||
const neverEntry = (el: ts.Expression): boolean => {
|
||||
const t = lowerer.typeOf(el);
|
||||
const arg =
|
||||
t.getSymbol()?.name === "Promise"
|
||||
? lowerer.checker.getTypeArguments(t as ts.TypeReference)[0]
|
||||
: undefined;
|
||||
return arg !== undefined && (arg.flags & ts.TypeFlags.Never) !== 0;
|
||||
};
|
||||
const entries = argNode.elements.nodes.map((el) => {
|
||||
const entry = lowerer.lowerExpr(el);
|
||||
const entries = elements.map((el, i) => {
|
||||
const entry = lowered?.[i] ?? lowerer.lowerExpr(el);
|
||||
if (entry.type.kind !== "promise") {
|
||||
lowerer.noLowering(
|
||||
"Promise.race over non-promise entries",
|
||||
@@ -259,6 +286,10 @@ export function lowerPromiseStaticCall(
|
||||
);
|
||||
}
|
||||
if (!compatible(entry.type.inner) && !(entry.type.inner.kind === "void" && neverEntry(el))) {
|
||||
// A payload that widens into the result's (into 'unknown', or a
|
||||
// checked conversion) keeps its promise and settlement turn.
|
||||
if (lowerer.promisePayloadWidens(entry.type, resultT))
|
||||
return lowerer.coerceToExpected(entry, resultT);
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
el,
|
||||
@@ -318,12 +349,12 @@ export function lowerPromiseStaticCall(
|
||||
return { kind: "intrinsic", name: "promise.all", args: [entriesArr], type: resultT, loc };
|
||||
}
|
||||
|
||||
if (!isJsSourceFile(call.getSourceFile())) {
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(call))) {
|
||||
const tuple = lowerTuplePromiseAll(lowerer, call, elems);
|
||||
if (tuple) return tuple;
|
||||
}
|
||||
if (
|
||||
isJsSourceFile(call.getSourceFile()) &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(call)) &&
|
||||
elems.every((entry) => lowerer.dynConvertible(entry.type))
|
||||
) {
|
||||
return checkedPromiseAll(
|
||||
@@ -344,7 +375,7 @@ export function lowerPromiseStaticCall(
|
||||
);
|
||||
}
|
||||
const entries = lowerer.lowerExpr(argNode);
|
||||
if (isJsSourceFile(call.getSourceFile()) && entries.type.kind === "dyn")
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(call)) && entries.type.kind === "dyn")
|
||||
return checkedPromiseAll(lowerer, entries, loc);
|
||||
|
||||
// loadPlugin(p)))` — the loadPlugins shape, where the checker
|
||||
@@ -403,7 +434,7 @@ export function lowerPromiseStaticCall(
|
||||
`one iterable of promises is the supported form: Promise.${member}(ps)`,
|
||||
);
|
||||
}
|
||||
if (isJsSourceFile(call.getSourceFile())) {
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(call))) {
|
||||
// JavaScript entries are checked-dynamic: any iterable, thenables
|
||||
// and plain values included, exactly like Promise.all.
|
||||
if (
|
||||
@@ -670,6 +701,32 @@ export function lowerPromiseStaticCall(
|
||||
lowerer.noLowering("Promise.resolve at a unit-typed promise", call);
|
||||
}
|
||||
const value = lowerer.lowerExprExpecting(argNode, resultT.inner);
|
||||
// In JavaScript, a checked-dynamic argument may itself hold a promise
|
||||
// (a TypeScript async callback's result: `Promise.resolve(fn())`):
|
||||
// Promise.resolve answers THAT promise — the same object, adopted —
|
||||
// and wraps every other value.
|
||||
if (
|
||||
isJsSourceFile(ts.getSourceFileOfNode(call)) &&
|
||||
value.type.kind === "dyn" &&
|
||||
resultT.inner.kind === "dyn"
|
||||
) {
|
||||
const local = lowerer.declareHiddenLocal("%resolveValue", DYN);
|
||||
const ref: IrExpr = { kind: "varRef", localId: local.id, type: DYN, loc };
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }],
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: { kind: "dynTest", test: "promise", value: ref, type: BOOL, loc },
|
||||
then: { kind: "dynCheck", value: ref, type: resultT, loc },
|
||||
else_: { kind: "intrinsic", name: "promise.resolve", args: [ref], type: resultT, loc },
|
||||
type: resultT,
|
||||
loc,
|
||||
},
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return { kind: "intrinsic", name: "promise.resolve", args: [value], type: resultT, loc };
|
||||
}
|
||||
return null; // reject lands on lowerPromiseRejectCall / the member fence
|
||||
|
||||
@@ -290,6 +290,9 @@ function generalChildStdioType(
|
||||
const className = name === "stdin" ? "%Writable" : "%Readable";
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (!mapped) return null;
|
||||
// Checked-dynamic stream slots: the declared stream type maps to dyn
|
||||
// (dyn-streams.ts); the read still produces the bridged stream.
|
||||
if (lowerer.dynNodeStreams && mapped.kind === "dyn") return { kind: "object", className };
|
||||
const arms =
|
||||
mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped];
|
||||
return arms.some((arm) => arm.kind === "object" && arm.className === className)
|
||||
|
||||
@@ -46,10 +46,10 @@ function isReadlineModule(spec: string | null): boolean {
|
||||
export function isReadlineTyped(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
const t = lowerer.checker.getTypeAtLocation(node);
|
||||
const sym = t.getAliasSymbol() ?? t.getSymbol();
|
||||
if (sym?.name !== "Interface") return false;
|
||||
if (sym === undefined || ts.symbolName(sym) !== "Interface") return false;
|
||||
return sym.declarations.some((d) => {
|
||||
if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false;
|
||||
if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
|
||||
if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) return false;
|
||||
return isReadlineModule(ambientModuleOf(d));
|
||||
});
|
||||
}
|
||||
@@ -74,7 +74,7 @@ const RL_EVENT_LIST =
|
||||
* readline/promises question. */
|
||||
function returnsPromise(lowerer: Lowerer, call: ts.CallExpression): boolean {
|
||||
const t = lowerer.checker.getTypeAtLocation(call);
|
||||
return (t.getSymbol()?.name ?? "") === "Promise";
|
||||
return (ts.symbolNameOf(t.getSymbol()) ?? "") === "Promise";
|
||||
}
|
||||
|
||||
/** The `{ signal }` options argument of the promise question: the signal
|
||||
@@ -107,10 +107,10 @@ function questionSignal(lowerer: Lowerer, node: ts.Expression | undefined): ts.E
|
||||
function isReadlineUiTyped(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
const t = lowerer.checker.getTypeAtLocation(node);
|
||||
const sym = t.getAliasSymbol() ?? t.getSymbol();
|
||||
if (sym?.name !== "Readline") return false;
|
||||
if (sym === undefined || ts.symbolName(sym) !== "Readline") return false;
|
||||
return sym.declarations.some((d) => {
|
||||
if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false;
|
||||
if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
|
||||
if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) return false;
|
||||
const spec = ambientModuleOf(d);
|
||||
return spec === "readline/promises" || spec === "node:readline/promises";
|
||||
});
|
||||
|
||||
@@ -10,10 +10,10 @@ export function isBuiltinClassInstance(
|
||||
): boolean {
|
||||
const type = lowerer.checker.getTypeAtLocation(node);
|
||||
const symbol = type.getAliasSymbol() ?? type.getSymbol();
|
||||
if (symbol?.name !== name) return false;
|
||||
if (symbol === undefined || ts.symbolName(symbol) !== name) return false;
|
||||
return symbol.declarations.some(
|
||||
(declaration) =>
|
||||
(ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) &&
|
||||
lowerer.isStdlibFile(declaration.getSourceFile()),
|
||||
lowerer.isStdlibFile(ts.getSourceFileOfNode(declaration)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function runtimeConstructorReceiver(
|
||||
let e = expr;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isPropertyAccessExpression(e) || e.questionDotToken) return null;
|
||||
if (e.name.text !== "constructor" || isJsSourceFile(e.getSourceFile())) return null;
|
||||
if (e.name.text !== "constructor" || isJsSourceFile(ts.getSourceFileOfNode(e))) return null;
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(e.expression));
|
||||
if (mapped?.kind !== "object") return null;
|
||||
const info = lowerer.classes.get(mapped.className);
|
||||
|
||||
@@ -360,13 +360,15 @@ export function storedTextCodecClassOf(
|
||||
expr: ts.Expression,
|
||||
): TextCodecCtor["cls"] | null {
|
||||
const sym = lowerer.typeOf(expr).getSymbol();
|
||||
if (!sym || (sym.name !== "TextEncoder" && sym.name !== "TextDecoder")) return null;
|
||||
if (!sym) return null;
|
||||
const name = ts.symbolName(sym);
|
||||
if (name !== "TextEncoder" && name !== "TextDecoder") return null;
|
||||
return sym.declarations.some(
|
||||
(d) =>
|
||||
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
|
||||
lowerer.isStdlibFile(d.getSourceFile()),
|
||||
lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
|
||||
)
|
||||
? sym.name
|
||||
? name
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -517,8 +519,9 @@ function directTextCodecCtorOf(lowerer: Lowerer, expr: ts.Expression): TextCodec
|
||||
if (callee.text !== "TextDecoder" && callee.text !== "TextEncoder") return null;
|
||||
const sym = lowerer.resolveValueSymbol(callee);
|
||||
if (!sym || !lowerer.isStdlibSymbol(sym)) return null;
|
||||
if (sym.name !== "TextDecoder" && sym.name !== "TextEncoder") return null;
|
||||
return { cls: sym.name, ctor };
|
||||
const name = ts.symbolName(sym);
|
||||
if (name !== "TextDecoder" && name !== "TextEncoder") return null;
|
||||
return { cls: name, ctor };
|
||||
}
|
||||
|
||||
/** Inline codec calls avoid allocating a receiver; stored receivers use
|
||||
|
||||
@@ -154,13 +154,13 @@ function isTimerHandleTyped(
|
||||
): boolean {
|
||||
const t = lowerer.checker.getTypeAtLocation(node);
|
||||
const sym = t.getAliasSymbol() ?? t.getSymbol();
|
||||
if (sym?.name !== name) return false;
|
||||
if (sym === undefined || ts.symbolName(sym) !== name) return false;
|
||||
return sym.declarations.some(
|
||||
(d) =>
|
||||
// An interface in the fallback and @types/node 22+, a class in
|
||||
// @types/node 20 and older.
|
||||
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
|
||||
lowerer.isStdlibFile(d.getSourceFile()),
|
||||
lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
|
||||
);
|
||||
}
|
||||
function isTimeoutTyped(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
|
||||
@@ -842,7 +842,7 @@ export function lowerLegacyUrlCall(
|
||||
if (bi.member === "domainToASCII" || bi.member === "domainToUnicode") {
|
||||
args.push({ kind: "boolLit", value: expr.arguments.nodes.length === 0, type: BOOL, loc });
|
||||
} else {
|
||||
const warn = !insideNodeModules(expr.getSourceFile().fileName);
|
||||
const warn = !insideNodeModules(ts.getSourceFileOfNode(expr).fileName);
|
||||
args.push({ kind: "boolLit", value: warn, type: BOOL, loc });
|
||||
}
|
||||
return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc };
|
||||
|
||||
@@ -103,7 +103,7 @@ export function fenceWebCryptoMember(
|
||||
use: "read" | "call",
|
||||
): IrExpr | null {
|
||||
if (use !== "read" || !ts.isPropertyAccessExpression(access)) return null;
|
||||
if (isJsSourceFile(access.getSourceFile())) return null;
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(access))) return null;
|
||||
if (isSubtleReceiver(lowerer, access)) {
|
||||
lowerer.noLowering("crypto.subtle as a value", access, SUBTLE_HINT);
|
||||
}
|
||||
@@ -123,8 +123,8 @@ export function isWebCryptoMemberType(lowerer: Lowerer, type: ts.Type): boolean
|
||||
const symbol = type.getAliasSymbol() ?? type.getSymbol();
|
||||
return (
|
||||
symbol !== undefined &&
|
||||
symbol.name !== "Crypto" &&
|
||||
WEBCRYPTO_DYN_TYPES.has(symbol.name) &&
|
||||
ts.symbolName(symbol) !== "Crypto" &&
|
||||
WEBCRYPTO_DYN_TYPES.has(ts.symbolName(symbol)) &&
|
||||
lowerer.isStdlibSymbol(symbol)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -174,10 +174,10 @@ function propertyNameText(name: ts.PropertyName): string | undefined {
|
||||
function isWorkerThreadsClass(lowerer: Lowerer, node: ts.Expression, name: string): boolean {
|
||||
const t = lowerer.checker.getTypeAtLocation(node);
|
||||
const sym = t.getSymbol();
|
||||
if (sym?.name !== name) return false;
|
||||
if (sym === undefined || ts.symbolName(sym) !== name) return false;
|
||||
return sym.declarations.some((d) => {
|
||||
if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false;
|
||||
if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
|
||||
if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) return false;
|
||||
let up: ts.Node | undefined = d.parent;
|
||||
while (up) {
|
||||
if (ts.isModuleDeclaration(up) && ts.isStringLiteral(up.name))
|
||||
|
||||
@@ -143,7 +143,7 @@ export function completeArgs(
|
||||
loc: value.loc,
|
||||
};
|
||||
}
|
||||
value = checkedIterableSpread(lowerer, value, arg.expression.getText(), loc);
|
||||
value = checkedIterableSpread(lowerer, value, ts.getTextOfNode(arg.expression), loc);
|
||||
if (spreads === 1 && index === argNodes.length - 1) {
|
||||
stmts.push({
|
||||
kind: "exprStmt",
|
||||
@@ -153,7 +153,7 @@ export function completeArgs(
|
||||
args: [
|
||||
ref(),
|
||||
value,
|
||||
{ kind: "strLit", value: arg.expression.getText(), type: STRING, loc },
|
||||
{ kind: "strLit", value: ts.getTextOfNode(arg.expression), type: STRING, loc },
|
||||
],
|
||||
type: VOID,
|
||||
loc,
|
||||
@@ -605,7 +605,7 @@ export function lowerSpreadArgsCall(
|
||||
// TypeScript reaches this lane only for checked-dynamic variadic callees
|
||||
// (`(...args: unknown[]) => R`), whose ABI is the JS one.
|
||||
if (
|
||||
!isJsSourceFile(expr.getSourceFile()) &&
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
|
||||
!(callee.type.kind === "func" && callee.type.rest === true && callee.type.restAbi === undefined)
|
||||
)
|
||||
return null;
|
||||
@@ -613,7 +613,7 @@ export function lowerSpreadArgsCall(
|
||||
if (callee.type.kind !== "dyn" && callee.type.kind !== "func") return null;
|
||||
const calleeName =
|
||||
ts.isPropertyAccessExpression(expr.expression) || ts.isElementAccessExpression(expr.expression)
|
||||
? expr.expression.getText()
|
||||
? ts.getTextOfNode(expr.expression)
|
||||
: ts.isIdentifier(expr.expression)
|
||||
? expr.expression.text
|
||||
: "value";
|
||||
@@ -640,8 +640,8 @@ export function lowerSpreadArgsCall(
|
||||
if (p.spreadOf !== null) {
|
||||
// The spelling rides along for V8's nullish spread-call
|
||||
// TypeError ("v is not iterable (cannot read property ...)").
|
||||
spreads.push({ arg: args.length, what: p.spreadOf.getText() });
|
||||
args.push(checkedIterableSpread(lowerer, p.v, p.spreadOf.getText(), loc));
|
||||
spreads.push({ arg: args.length, what: ts.getTextOfNode(p.spreadOf) });
|
||||
args.push(checkedIterableSpread(lowerer, p.v, ts.getTextOfNode(p.spreadOf), loc));
|
||||
} else {
|
||||
args.push(lowerer.coerceInto(p.node!, p.v, DYN));
|
||||
}
|
||||
|
||||
@@ -148,7 +148,9 @@ export function documentedParameter(param: ts.ParameterDeclaration): boolean {
|
||||
return (
|
||||
param.type !== undefined ||
|
||||
/@(?:param|type)\b/.test(
|
||||
param.getSourceFile().text.slice(param.parent?.pos ?? param.pos, param.getStart()),
|
||||
ts
|
||||
.getSourceFileOfNode(param)
|
||||
.text.slice(param.parent?.pos ?? param.pos, ts.tokenStartOfNode(param)),
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -158,7 +160,7 @@ export function checkedJsDocumentedParameter(
|
||||
lowerer: Lowerer,
|
||||
param: ts.ParameterDeclaration,
|
||||
): boolean {
|
||||
return lowerer.checkedJsFile(param.getSourceFile()) && documentedParameter(param);
|
||||
return lowerer.checkedJsFile(ts.getSourceFileOfNode(param)) && documentedParameter(param);
|
||||
}
|
||||
|
||||
/** One parameter's ParamShape — the shared signature-shaped collection
|
||||
@@ -180,10 +182,10 @@ export function checkedJsDocumentedParameter(
|
||||
export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): ParamShape {
|
||||
const stage = lowerer.stageSourceType(param.name);
|
||||
if (stage) return { type: stage, mode: "required" };
|
||||
if (isJsSourceFile(param.getSourceFile()) && ts.isSetAccessorDeclaration(param.parent))
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(param)) && ts.isSetAccessorDeclaration(param.parent))
|
||||
return { type: DYN, mode: "required" };
|
||||
if (
|
||||
isJsSourceFile(param.getSourceFile()) &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(param)) &&
|
||||
ts.isConstructorDeclaration(param.parent) &&
|
||||
!param.dotDotDotToken &&
|
||||
!checkedJsDocumentedParameter(lowerer, param)
|
||||
@@ -207,12 +209,14 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa
|
||||
lowerer.isStdlibGlobal(construction.expression, "Promise");
|
||||
if (
|
||||
!nativeExecutor &&
|
||||
isJsSourceFile(param.getSourceFile()) &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(param)) &&
|
||||
!param.type &&
|
||||
!param.dotDotDotToken &&
|
||||
!ts.isSetAccessorDeclaration(param.parent) &&
|
||||
!/@(?:param|type)\b/.test(
|
||||
param.getSourceFile().text.slice(param.parent?.pos ?? param.pos, param.getStart()),
|
||||
ts
|
||||
.getSourceFileOfNode(param)
|
||||
.text.slice(param.parent?.pos ?? param.pos, ts.tokenStartOfNode(param)),
|
||||
)
|
||||
) {
|
||||
return param.initializer
|
||||
@@ -221,10 +225,12 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa
|
||||
}
|
||||
if (
|
||||
param.initializer &&
|
||||
isJsSourceFile(param.getSourceFile()) &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(param)) &&
|
||||
!param.type &&
|
||||
!/@(?:param|type)\b/.test(
|
||||
param.getSourceFile().text.slice(param.parent?.pos ?? param.pos, param.getStart()),
|
||||
ts
|
||||
.getSourceFileOfNode(param)
|
||||
.text.slice(param.parent?.pos ?? param.pos, ts.tokenStartOfNode(param)),
|
||||
)
|
||||
) {
|
||||
return { type: DYN, mode: "omittable", bodyType: DYN };
|
||||
@@ -233,10 +239,12 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa
|
||||
// contract. Consume the actual iterable, including surplus elements.
|
||||
if (
|
||||
ts.isArrayBindingPattern(param.name) &&
|
||||
isJsSourceFile(param.getSourceFile()) &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(param)) &&
|
||||
!param.type &&
|
||||
!/@(?:param|type)\b/.test(
|
||||
param.getSourceFile().text.slice(param.parent?.pos ?? param.pos, param.getStart()),
|
||||
ts
|
||||
.getSourceFileOfNode(param)
|
||||
.text.slice(param.parent?.pos ?? param.pos, ts.tokenStartOfNode(param)),
|
||||
)
|
||||
) {
|
||||
if (param.dotDotDotToken) return { type: DYN, mode: "dynRest" };
|
||||
@@ -298,7 +306,7 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa
|
||||
// ARRAY param the dyn call thunk fills with the call's surplus
|
||||
// arguments; the binding is that array (dynRest — funcType marks
|
||||
// `rest`, and the value only ever calls through the boxed thunk).
|
||||
if (isJsSourceFile(param.getSourceFile())) {
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(param))) {
|
||||
const restMapped = lowerer.mapTypeOf(lowerer.typeOf(param.name));
|
||||
|
||||
// Unmapped JS rest parameters use the native variadic checked-value ABI.
|
||||
@@ -325,7 +333,7 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa
|
||||
// including typed arrays passed through checked callbacks. Extracting
|
||||
// a native array here would copy it and discard element writes.
|
||||
if (
|
||||
isJsSourceFile(param.getSourceFile()) &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(param)) &&
|
||||
!param.type &&
|
||||
(raw.kind === "array" ||
|
||||
(raw.kind === "union" &&
|
||||
@@ -337,7 +345,7 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa
|
||||
}
|
||||
let type = lowerer.runtimeOptionalBindingType(param.name, lowerer.irTypeOf(param.name));
|
||||
const body = param.parent?.body;
|
||||
if (isJsSourceFile(param.getSourceFile()) && body && type.kind !== "dyn") {
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(param)) && body && type.kind !== "dyn") {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
|
||||
let acceptsUndefined = false;
|
||||
ts.walkPreorder(body, (node) => {
|
||||
@@ -397,7 +405,7 @@ export function restTupleShapes(
|
||||
if (
|
||||
param.dotDotDotToken === undefined ||
|
||||
!ts.isIdentifier(param.name) ||
|
||||
isJsSourceFile(param.getSourceFile())
|
||||
isJsSourceFile(ts.getSourceFileOfNode(param))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -435,7 +443,7 @@ export function paramShapes(
|
||||
// rather than as the declaration's unbound type parameter.
|
||||
if (
|
||||
blameOf !== undefined &&
|
||||
!isJsSourceFile(declParam.getSourceFile()) &&
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(declParam)) &&
|
||||
(tsType.flags & ts.TypeFlags.Any) === 0 &&
|
||||
declParam.type !== undefined &&
|
||||
mentionsTypeParameter(lowerer, lowerer.checker.getTypeFromTypeNode(declParam.type)) &&
|
||||
@@ -450,7 +458,8 @@ export function paramShapes(
|
||||
lowerer.mapTypeOf(tsType) ??
|
||||
// A TypeScript instantiation at `any` (a type argument inferred
|
||||
// from an untyped value) takes the checked-dynamic slot.
|
||||
((tsType.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(declParam.getSourceFile())
|
||||
((tsType.flags & ts.TypeFlags.Any) !== 0 &&
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(declParam))
|
||||
? DYN
|
||||
: lowerer.irTypeOf(declParam.name)),
|
||||
),
|
||||
@@ -668,7 +677,7 @@ export function requireExactArityValue(
|
||||
const ownType = lowerer.typeOf(contextual);
|
||||
mappedFn =
|
||||
lowerer.mapTypeOf(ownType) ??
|
||||
(isJsSourceFile(contextual.getSourceFile())
|
||||
(isJsSourceFile(ts.getSourceFileOfNode(contextual))
|
||||
? dynFallbackType(lowerer, contextual, ownType)
|
||||
: null);
|
||||
}
|
||||
@@ -706,7 +715,7 @@ export function requireExactArityValue(
|
||||
target === undefined &&
|
||||
contextual !== null &&
|
||||
(ts.isArrowFunction(contextual) || ts.isFunctionExpression(contextual)) &&
|
||||
npmStaticPackageOfPath(contextual.getSourceFile().fileName) !== null
|
||||
npmStaticPackageOfPath(ts.getSourceFileOfNode(contextual).fileName) !== null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -771,7 +780,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara
|
||||
// routes generic declarations (direct calls, namespace/CJS member
|
||||
// calls, value references) resolves it through genericFnsBySymbol
|
||||
// unchanged.
|
||||
if (implicitMonoFile(decl.getSourceFile()) && !isAsync && !isGenerator) {
|
||||
if (implicitMonoFile(ts.getSourceFileOfNode(decl)) && !isAsync && !isGenerator) {
|
||||
const implicit = implicitAnyParamSymbolsOf(lowerer, decl);
|
||||
if (implicit) {
|
||||
const nameText = decl.name?.text ?? "%default";
|
||||
@@ -780,7 +789,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara
|
||||
lowerer.genericFnsBySymbol.set(symbol, {
|
||||
decl,
|
||||
baseName: nameText,
|
||||
qualifiedName: lowerer.qualify(decl.getSourceFile(), nsPathPrefix(decl) + nameText),
|
||||
qualifiedName: lowerer.qualify(ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + nameText),
|
||||
typeParams: [],
|
||||
instances: new Map(),
|
||||
implicitParams: implicit,
|
||||
@@ -815,7 +824,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara
|
||||
// collide. The anonymous default export takes the synthetic
|
||||
// "%default" spelling ('%' cannot appear in a user identifier).
|
||||
name: lowerer.qualify(
|
||||
decl.getSourceFile(),
|
||||
ts.getSourceFileOfNode(decl),
|
||||
nsPathPrefix(decl) + (decl.name?.text ?? "%default"),
|
||||
),
|
||||
params,
|
||||
@@ -844,7 +853,7 @@ function appendArgumentsParameter(
|
||||
ts.isFunctionDeclaration(node) ||
|
||||
ts.isMethodDeclaration(node)
|
||||
) ||
|
||||
!isJsSourceFile(node.getSourceFile()) ||
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(node)) ||
|
||||
!bodyReadsArguments(node)
|
||||
)
|
||||
return;
|
||||
@@ -854,7 +863,7 @@ function appendArgumentsParameter(
|
||||
(ts.isClassDeclaration(node.parent) || ts.isClassExpression(node.parent));
|
||||
if (
|
||||
!classMethod &&
|
||||
!isNodeEsmFile(node.getSourceFile(), lowerer.program) &&
|
||||
!isNodeEsmFile(ts.getSourceFileOfNode(node), lowerer.program) &&
|
||||
!unmappedArguments(node)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
@@ -925,7 +934,7 @@ export function lambdaSignature(
|
||||
// actual call tuple, including overloads and omitted arguments. Node's
|
||||
// contextual overload signature cannot define this closure's ABI.
|
||||
if (
|
||||
isJsSourceFile(node.getSourceFile()) &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(node)) &&
|
||||
!node.type &&
|
||||
(ts.isFunctionExpression(node) || ts.isArrowFunction(node)) &&
|
||||
node.parameters.nodes.every(
|
||||
@@ -1043,7 +1052,7 @@ export function lambdaSignature(
|
||||
: [];
|
||||
const ctxDecl = ctxSigs.length === 1 ? ctxSigs[0]!.declaration : undefined;
|
||||
const ctxRetRaw =
|
||||
ctxDecl && !ctxDecl.getSourceFile().isDeclarationFile
|
||||
ctxDecl && !ts.getSourceFileOfNode(ctxDecl).isDeclarationFile
|
||||
? lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(ctxSigs[0]!))
|
||||
: null;
|
||||
// An async lambda always produces a promise, so a slot returning a
|
||||
@@ -1065,7 +1074,7 @@ export function lambdaSignature(
|
||||
// must match the slot's ABI exactly.
|
||||
const dynVariadicSlot =
|
||||
ctxRet?.kind === "dyn" &&
|
||||
!isJsSourceFile(node.getSourceFile()) &&
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(node)) &&
|
||||
shapes.some((shape) => shape.mode === "dynRest");
|
||||
if (
|
||||
dynVariadicSlot ||
|
||||
|
||||
@@ -40,12 +40,12 @@ import { DYN, type IrExpr, type IrGlobal } from "../../ir/ir.js";
|
||||
* source declaration) in a CommonJS JS file. */
|
||||
export function isCjsExportsIdentifier(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
if (!ts.isIdentifier(node) || node.text !== "exports") return false;
|
||||
if (!isCjsJsFile(node.getSourceFile(), lowerer.program)) return false;
|
||||
if (!isCjsJsFile(ts.getSourceFileOfNode(node), lowerer.program)) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(node);
|
||||
const decls = symbol ? symbol.declarations : [];
|
||||
return !decls.some(
|
||||
(d) =>
|
||||
!d.getSourceFile().isDeclarationFile &&
|
||||
!ts.getSourceFileOfNode(d).isDeclarationFile &&
|
||||
(ts.isVariableDeclaration(d) ||
|
||||
ts.isParameter(d) ||
|
||||
ts.isBindingElement(d) ||
|
||||
@@ -62,7 +62,7 @@ export function isCjsModuleExports(lowerer: Lowerer, node: ts.Expression): boole
|
||||
return (
|
||||
isModuleExportsAccess(node) &&
|
||||
isCjsModuleGlobal(lowerer, node.expression) &&
|
||||
isCjsJsFile(node.getSourceFile(), lowerer.program)
|
||||
isCjsJsFile(ts.getSourceFileOfNode(node), lowerer.program)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export function cjsInFileExportGlobalOf(
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrGlobal | null {
|
||||
if (access.questionDotToken || !ts.isIdentifier(access.name)) return null;
|
||||
const sf = access.getSourceFile();
|
||||
const sf = ts.getSourceFileOfNode(access);
|
||||
const recv =
|
||||
isModuleExportsAccess(access.expression) ||
|
||||
(ts.isIdentifier(access.expression) &&
|
||||
@@ -186,7 +186,7 @@ function exportsAssignments(
|
||||
* it runs: `exports` unless the file rebinds it; `module.exports` unless the
|
||||
* file rebinds `exports` or replaces `module.exports`. */
|
||||
function namesOriginalExports(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
const assigned = exportsAssignments(lowerer, node.getSourceFile());
|
||||
const assigned = exportsAssignments(lowerer, ts.getSourceFileOfNode(node));
|
||||
if (assigned.exports) return false;
|
||||
return ts.isIdentifier(node) || !assigned.moduleExports;
|
||||
}
|
||||
@@ -252,7 +252,7 @@ export function usesCjsExportsIdentity(lowerer: Lowerer, sf: ts.SourceFile): boo
|
||||
* refusal. Null when `node` is not such a use. */
|
||||
export function lowerCjsExportsObjectValue(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
|
||||
if (!isCjsExportsObjectValue(lowerer, node)) return null;
|
||||
const sf = node.getSourceFile();
|
||||
const sf = ts.getSourceFileOfNode(node);
|
||||
// A module that replaces module.exports answers `module.exports` reads
|
||||
// through the replacement's own machinery (the class-expression whole
|
||||
// export, the replaced-exports reads).
|
||||
@@ -328,7 +328,7 @@ export function cjsExportMemberWriteGlobalOf(
|
||||
lowerer: Lowerer,
|
||||
target: ts.PropertyAccessExpression,
|
||||
): IrGlobal | null {
|
||||
const sf = target.getSourceFile();
|
||||
const sf = ts.getSourceFileOfNode(target);
|
||||
if (!isCjsJsFile(sf, lowerer.program)) return null;
|
||||
const assigned = exportsAssignments(lowerer, sf);
|
||||
if (assigned.exports || assigned.moduleExports) return null;
|
||||
@@ -458,7 +458,7 @@ export function cjsMemberAliasValueOf(lowerer: Lowerer, ref: ts.Identifier): ts.
|
||||
)
|
||||
return null;
|
||||
const spec = importDecl.moduleSpecifier.text;
|
||||
const from = importDecl.getSourceFile();
|
||||
const from = ts.getSourceFileOfNode(importDecl);
|
||||
const dep =
|
||||
resolveImport(lowerer.program, from, spec, "import") ??
|
||||
npmStaticDepSf7(lowerer.program, from, spec, "import");
|
||||
@@ -552,7 +552,7 @@ function rereadableMemberValue(
|
||||
const sym = lowerer.resolveValueSymbol(value);
|
||||
if (sym === null || sym.declarations.length !== 1) return false;
|
||||
const decl = sym.declarations[0]!;
|
||||
if (decl.getSourceFile() !== stmt.getSourceFile()) return false;
|
||||
if (ts.getSourceFileOfNode(decl) !== ts.getSourceFileOfNode(stmt)) return false;
|
||||
// A function declaration is initialized when the module starts; any
|
||||
// other binding must be initialized before the write stores it.
|
||||
if (!ts.isFunctionDeclaration(decl) && decl.end > stmt.pos) return false;
|
||||
|
||||
@@ -56,7 +56,7 @@ export function inlineProgramRequireOf(
|
||||
if (!ts.isCallExpression(e) || !ts.isIdentifier(e.expression)) return null;
|
||||
const spec = requireSpecOf(e);
|
||||
if (spec === null || canonicalBuiltinModule(spec) !== null) return null;
|
||||
const sf = e.getSourceFile();
|
||||
const sf = ts.getSourceFileOfNode(e);
|
||||
if (!isCjsJsFile(sf, lowerer.program)) return null;
|
||||
if (!isCjsRequireGlobal(lowerer, e.expression)) return null;
|
||||
if (createRequireCalleeFileOf(lowerer, e.expression) !== null) return null;
|
||||
@@ -201,7 +201,7 @@ export function lowerUncompiledRequireCall(
|
||||
call: ts.CallExpression,
|
||||
): IrExpr | null {
|
||||
if (!ts.isIdentifier(call.expression) || call.expression.text !== "require") return null;
|
||||
const sf = call.getSourceFile();
|
||||
const sf = ts.getSourceFileOfNode(call);
|
||||
if (!isCjsJsFile(sf, lowerer.program) || !isCjsRequireGlobal(lowerer, call.expression))
|
||||
return null;
|
||||
if (createRequireCalleeFileOf(lowerer, call.expression) !== null) return null;
|
||||
@@ -240,7 +240,7 @@ export function requireNestedPatternsOf(decl: ts.VariableDeclaration): ts.Bindin
|
||||
if (!ts.isObjectBindingPattern(decl.name) || decl.initializer === undefined) return [];
|
||||
const spec = requireSpecOf(decl.initializer);
|
||||
if (spec === null || canonicalBuiltinModule(spec) !== null) return [];
|
||||
if (!isJsSourceFile(decl.getSourceFile())) return [];
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(decl))) return [];
|
||||
return decl.name.elements.nodes.filter(
|
||||
(e) =>
|
||||
e.name !== undefined &&
|
||||
|
||||
@@ -175,7 +175,7 @@ export function tscInteropDefaultValueOf(
|
||||
export function isDeadTscHelperDecl(lowerer: Lowerer, stmt: ts.Statement): boolean {
|
||||
const name = tscHelperDeclName(stmt);
|
||||
if (name === null) return false;
|
||||
const sf = stmt.getSourceFile();
|
||||
const sf = ts.getSourceFileOfNode(stmt);
|
||||
const helperSpans = new Map<string, ts.Statement>();
|
||||
for (const s of sf.statements.nodes) {
|
||||
const n = tscHelperDeclName(s);
|
||||
@@ -192,11 +192,11 @@ export function isDeadTscHelperDecl(lowerer: Lowerer, stmt: ts.Statement): boole
|
||||
const parent = node.parent;
|
||||
if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.name === node)
|
||||
return undefined;
|
||||
const at = node.getStart(sf);
|
||||
if (at >= decl.getStart(sf) && at < decl.getEnd()) return undefined;
|
||||
const at = ts.tokenStartOfNode(node);
|
||||
if (at >= ts.tokenStartOfNode(decl) && at < decl.end) return undefined;
|
||||
for (const [other, otherDecl] of helperSpans) {
|
||||
if (other !== helper && dead.has(other)) {
|
||||
if (at >= otherDecl.getStart(sf) && at < otherDecl.getEnd()) return undefined;
|
||||
if (at >= ts.tokenStartOfNode(otherDecl) && at < otherDecl.end) return undefined;
|
||||
}
|
||||
}
|
||||
const call = parent;
|
||||
|
||||
@@ -39,8 +39,8 @@ export function isClassCallback(lowerer: Lowerer, info: ClassInfo, name: string)
|
||||
!found.sig.abstract &&
|
||||
(lowerer.prototypeMethodAccesses.has(name) ||
|
||||
(info.decl !== null &&
|
||||
isJsSourceFile(info.decl.getSourceFile()) &&
|
||||
!lowerer.checkedJsFile(info.decl.getSourceFile()) &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(info.decl)) &&
|
||||
!lowerer.checkedJsFile(ts.getSourceFileOfNode(info.decl)) &&
|
||||
found.sig.params.length === 0 &&
|
||||
found.sig.ret.kind === "void"))
|
||||
);
|
||||
@@ -171,7 +171,7 @@ export function classCallbackCall(
|
||||
const spreads: { arg: number; what: string }[] = [];
|
||||
const args = argumentNodes.map((argument, index) => {
|
||||
if (!ts.isSpreadElement(argument)) return lowerer.lowerExprExpecting(argument, DYN);
|
||||
const spelling = argument.expression.getText();
|
||||
const spelling = ts.getTextOfNode(argument.expression);
|
||||
spreads.push({ arg: index, what: spelling });
|
||||
return checkedIterableSpread(lowerer, lowerer.lowerExpr(argument.expression), spelling, loc);
|
||||
});
|
||||
|
||||
@@ -1158,7 +1158,7 @@ export class ClassDynamicDispatch {
|
||||
if (this.bagClasses.has(plan.key)) continue;
|
||||
this.bagClasses.add(plan.key);
|
||||
this.ensurePropertyBag(info);
|
||||
if (info.decl && isJsSourceFile(info.decl.getSourceFile())) {
|
||||
if (info.decl && isJsSourceFile(ts.getSourceFileOfNode(info.decl))) {
|
||||
let root = info;
|
||||
while (root.base && !root.base.def.runtime) root = root.base;
|
||||
const track = (current: ClassInfo): void => {
|
||||
@@ -3246,7 +3246,7 @@ export class ClassDynamicDispatch {
|
||||
for (const name of this.boxedConstructors) {
|
||||
if (this.staticBagClasses.has(name)) continue;
|
||||
const info = lowerer.classes.get(name);
|
||||
if (!info?.decl || !isJsSourceFile(info.decl.getSourceFile())) continue;
|
||||
if (!info?.decl || !isJsSourceFile(ts.getSourceFileOfNode(info.decl))) continue;
|
||||
const loc = this.staticPropertyBag.loc;
|
||||
const bag = classStaticData(lowerer, info, loc);
|
||||
if (!bag) continue;
|
||||
|
||||
@@ -152,10 +152,11 @@ export function collectClassMethodMutations(
|
||||
const required = properties.filter(
|
||||
(property) => (property.flags & ts.SymbolFlags.Optional) === 0,
|
||||
);
|
||||
if (required.some((property) => !FUNCTION_MEMBER_NAMES.has(property.name))) return true;
|
||||
if (required.some((property) => !FUNCTION_MEMBER_NAMES.has(ts.symbolName(property))))
|
||||
return true;
|
||||
return (
|
||||
required.length === 0 &&
|
||||
properties.every((property) => !FUNCTION_MEMBER_NAMES.has(property.name))
|
||||
properties.every((property) => !FUNCTION_MEMBER_NAMES.has(ts.symbolName(property)))
|
||||
);
|
||||
};
|
||||
// A string index whose values cannot be functions excludes callable
|
||||
@@ -167,7 +168,7 @@ export function collectClassMethodMutations(
|
||||
.getIndexInfosOfType(lowerer.typeOf(value))
|
||||
.some(
|
||||
(index) =>
|
||||
(index.keyType.flags & ts.TypeFlags.StringLike) !== 0 && functionFree(index.valueType),
|
||||
(index.keyType.flags & ts.TypeFlags.StringLike) !== 0 && functionFree(index.type),
|
||||
);
|
||||
const literalLoopKeys = (key: ts.Expression): (string | number)[] | null => {
|
||||
if (!ts.isIdentifier(key)) return null;
|
||||
|
||||
@@ -92,7 +92,8 @@ export function lowerClassMethodValue(
|
||||
loc,
|
||||
};
|
||||
};
|
||||
const checked = isJsSourceFile(expr.getSourceFile()) && lowerer.dynConvertible(value.type);
|
||||
const checked =
|
||||
isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(value.type);
|
||||
if (checked) value = lowerer.coerceToExpected(value, DYN);
|
||||
// Select the declaration when extracting the value. Calling the value
|
||||
// later must not redispatch the method name on a different receiver.
|
||||
@@ -374,7 +375,7 @@ function reflectedMethodValue(
|
||||
if (!member || (!ts.isMethodDeclaration(member) && !ts.isAccessor(member)) || !member.body)
|
||||
return null;
|
||||
let needsNativeReceiver =
|
||||
!isJsSourceFile(declaration.getSourceFile()) ||
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(declaration)) ||
|
||||
info.localClass !== undefined ||
|
||||
lowerer.errorHierarchyClassOf(info.def.name);
|
||||
ts.walkPreorder(member, (node) => {
|
||||
|
||||
@@ -87,7 +87,7 @@ function codeOwner(node: ts.Node): ts.Node {
|
||||
!ts.isPropertyDeclaration(current)
|
||||
)
|
||||
current = current.parent;
|
||||
return current ?? node.getSourceFile();
|
||||
return current ?? ts.getSourceFileOfNode(node);
|
||||
}
|
||||
|
||||
/** A local variable or parameter that only its own function's code
|
||||
@@ -102,21 +102,21 @@ function ownedBinding(lowerer: Lowerer, id: ts.Identifier): boolean {
|
||||
const decl = symbol.valueDeclaration;
|
||||
let owned =
|
||||
decl !== undefined &&
|
||||
decl.getSourceFile() === id.getSourceFile() &&
|
||||
ts.getSourceFileOfNode(decl) === ts.getSourceFileOfNode(id) &&
|
||||
((ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name)) ||
|
||||
(ts.isParameter(decl) && ts.isIdentifier(decl.name)));
|
||||
if (owned && decl) {
|
||||
const owner = codeOwner(decl);
|
||||
const names = (node: ts.Node): boolean =>
|
||||
ts.isIdentifier(node) &&
|
||||
node.text === symbol.name &&
|
||||
node.text === ts.symbolName(symbol) &&
|
||||
lowerer.resolveValueSymbol(node) === symbol;
|
||||
const written = (target: ts.Node): boolean => {
|
||||
let found = false;
|
||||
const walk = (node: ts.Node): void => {
|
||||
if (found) return;
|
||||
if (names(node)) found = true;
|
||||
else node.forEachChild(walk);
|
||||
else ts.forEachChild(node, walk);
|
||||
};
|
||||
walk(target);
|
||||
return found;
|
||||
@@ -144,9 +144,9 @@ function ownedBinding(lowerer: Lowerer, id: ts.Identifier): boolean {
|
||||
)
|
||||
target = node.initializer;
|
||||
if (target && codeOwner(node) !== owner && written(target)) owned = false;
|
||||
node.forEachChild(visit);
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
owner.forEachChild(visit);
|
||||
ts.forEachChild(owner, visit);
|
||||
}
|
||||
lowerer.protocolOwnedBindings.set(symbol, owned);
|
||||
return owned;
|
||||
@@ -302,7 +302,7 @@ export function lowerProtocolMethodCall(
|
||||
kind: "dynCall",
|
||||
callee: probed ? varRef("p.callee", DYN, loc) : read(),
|
||||
receiver: varRef("p.0", DYN, loc),
|
||||
calleeName: access.getText(),
|
||||
calleeName: ts.getTextOfNode(access),
|
||||
calleeNameValue: varRef("p.calleeName", STRING, loc),
|
||||
args: params.map((type, i) => lowerer.coerceToExpected(varRef(`p.${i + 1}`, type, loc), DYN)),
|
||||
type: DYN,
|
||||
@@ -378,7 +378,7 @@ export function lowerProtocolMethodCall(
|
||||
receiver,
|
||||
...(callee ? [varRef(callee.id, DYN, loc)] : []),
|
||||
...args,
|
||||
{ kind: "strLit", value: access.getText(), type: STRING, loc },
|
||||
{ kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc },
|
||||
],
|
||||
type: result,
|
||||
loc,
|
||||
@@ -487,7 +487,7 @@ export function lowerProtocolPropertyRead(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (isJsSourceFile(access.getSourceFile())) return null;
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(access))) return null;
|
||||
const receiverType = lowerer.typeOf(access.expression);
|
||||
if (!lowerer.classImplementedProtocol(receiverType)) return null;
|
||||
const name = access.name.text;
|
||||
|
||||
@@ -46,9 +46,9 @@ export class ClassProtocols {
|
||||
)
|
||||
)
|
||||
continue;
|
||||
const entries = this.methods.get(property.name) ?? [];
|
||||
const entries = this.methods.get(ts.symbolName(property)) ?? [];
|
||||
entries.push(node);
|
||||
this.methods.set(property.name, entries);
|
||||
this.methods.set(ts.symbolName(property), entries);
|
||||
}
|
||||
}
|
||||
if (
|
||||
@@ -472,7 +472,7 @@ export class ClassProtocols {
|
||||
if (facts.observed.has(symbol) || facts.written.has(symbol)) return true;
|
||||
const target = facts.targets.get(symbol);
|
||||
if (target === undefined) return true;
|
||||
const names = new Set(this.checker.getPropertiesOfType(target).map((p) => p.name));
|
||||
const names = new Set(this.checker.getPropertiesOfType(target).map((p) => ts.symbolName(p)));
|
||||
const origins = facts.origins.get(symbol) ?? new Set<ts.Symbol>();
|
||||
const writesNames = (written: Set<string> | "all"): boolean =>
|
||||
written === "all" || [...written].some((name) => names.has(name));
|
||||
@@ -517,7 +517,7 @@ export class ClassProtocols {
|
||||
.getPropertiesOfType(declared)
|
||||
.some(
|
||||
(property) =>
|
||||
names.has(property.name) &&
|
||||
names.has(ts.symbolName(property)) &&
|
||||
property.declarations.some(
|
||||
(declaration) =>
|
||||
ts.isGetAccessorDeclaration(declaration) || ts.isSetAccessorDeclaration(declaration),
|
||||
@@ -575,7 +575,9 @@ export class ClassProtocols {
|
||||
return (
|
||||
symbol !== undefined &&
|
||||
(symbol.flags & ts.SymbolFlags.Class) !== 0 &&
|
||||
symbol.declarations.some((declaration) => !declaration.getSourceFile().isDeclarationFile) &&
|
||||
symbol.declarations.some(
|
||||
(declaration) => !ts.getSourceFileOfNode(declaration).isDeclarationFile,
|
||||
) &&
|
||||
// `typeof C` (the class value) shares the class symbol.
|
||||
this.checker.getConstructSignatures(type).length === 0
|
||||
);
|
||||
@@ -598,7 +600,8 @@ export class ClassProtocols {
|
||||
this.methods.size > 0 &&
|
||||
symbol.declarations.some(
|
||||
(declaration) =>
|
||||
ts.isInterfaceDeclaration(declaration) && !declaration.getSourceFile().isDeclarationFile,
|
||||
ts.isInterfaceDeclaration(declaration) &&
|
||||
!ts.getSourceFileOfNode(declaration).isDeclarationFile,
|
||||
) &&
|
||||
this.protocol(type)
|
||||
);
|
||||
@@ -617,7 +620,7 @@ export class ClassProtocols {
|
||||
const declarations = symbol.declarations;
|
||||
return (
|
||||
declarations.length > 0 &&
|
||||
declarations.every((declaration) => !declaration.getSourceFile().isDeclarationFile)
|
||||
declarations.every((declaration) => !ts.getSourceFileOfNode(declaration).isDeclarationFile)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -704,14 +707,15 @@ export class ClassProtocols {
|
||||
!symbol ||
|
||||
!symbol.declarations.some(
|
||||
(declaration) =>
|
||||
ts.isInterfaceDeclaration(declaration) && !declaration.getSourceFile().isDeclarationFile,
|
||||
ts.isInterfaceDeclaration(declaration) &&
|
||||
!ts.getSourceFileOfNode(declaration).isDeclarationFile,
|
||||
)
|
||||
)
|
||||
return false;
|
||||
for (const property of properties) {
|
||||
const memberType = this.checker.getTypeOfSymbol(property);
|
||||
if (this.checker.getCallSignatures(memberType).length === 0) continue;
|
||||
for (const declaration of this.methods.get(property.name) ?? []) {
|
||||
for (const declaration of this.methods.get(ts.symbolName(property)) ?? []) {
|
||||
const classSymbol = declaration.name
|
||||
? this.checker.getSymbolAtLocation(declaration.name)
|
||||
: undefined;
|
||||
|
||||
@@ -38,7 +38,7 @@ export function classStaticDataFor(
|
||||
): IrExpr | null {
|
||||
if (
|
||||
!info.decl ||
|
||||
!isJsSourceFile(info.decl.getSourceFile()) ||
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(info.decl)) ||
|
||||
name.startsWith("#") ||
|
||||
constructorMembers.has(name)
|
||||
)
|
||||
|
||||
@@ -126,8 +126,12 @@ function useSiteType(lowerer: Lowerer, expr: ts.Expression): IrType {
|
||||
// elsewhere — the throw always fires first.
|
||||
const type = lowerer.typeOf(expr);
|
||||
const symbol = type.getSymbol();
|
||||
if (symbol?.name === "Function" && lowerer.isStdlibSymbol(symbol)) {
|
||||
return isJsSourceFile(expr.getSourceFile()) ? DYN : F64;
|
||||
if (
|
||||
symbol !== undefined &&
|
||||
ts.symbolName(symbol) === "Function" &&
|
||||
lowerer.isStdlibSymbol(symbol)
|
||||
) {
|
||||
return isJsSourceFile(ts.getSourceFileOfNode(expr)) ? DYN : F64;
|
||||
}
|
||||
const mapped = lowerer.mapTypeOf(type);
|
||||
if (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user