diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbc921b74..bbc9ddb10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 47329cc28..fff1931ae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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' diff --git a/CHANGELOG.md b/CHANGELOG.md index 3de3f334f..c319a1b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,73 @@ All notable changes to scriptc will be documented in this file. +## 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/`), 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 ` 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 ` 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. + + + ## 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.** - - ## 0.3.5 ### Features diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index f8552b003..7ab829bd5 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -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 | diff --git a/benchmarks/runtime/workloads.json b/benchmarks/runtime/workloads.json index 9a3f1bc2f..80beefab4 100644 --- a/benchmarks/runtime/workloads.json +++ b/benchmarks/runtime/workloads.json @@ -231,6 +231,13 @@ "4" ] }, + { + "name": "unicode-lexer", + "entry": "workloads/unicode-lexer.ts", + "args": [ + "1" + ] + }, { "name": "log-summary", "entry": "../builds/log-summary/main.ts", diff --git a/benchmarks/runtime/workloads/unicode-lexer.ts b/benchmarks/runtime/workloads/unicode-lexer.ts new file mode 100644 index 000000000..21638bb86 --- /dev/null +++ b/benchmarks/runtime/workloads/unicode-lexer.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)); diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx index 786cf2c63..df8274258 100644 --- a/docs/content/docs/cli.mdx +++ b/docs/content/docs/cli.mdx @@ -128,7 +128,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
Coverage only. Map an exact bare module specifier to a local declaration file supplied by an embedder. Repeat the option for multiple modules. The declaration supplies checker types so application coverage can continue; runtime imports and values remain explicit SC1010 blockers. Relative paths resolve from the current working directory. Accepted files end in .d.ts, .d.mts, or .d.cts.
--tier <auto|static-only>
-
The tier policy for code that has no static lowering. auto is the default. static-only guarantees that the program contains no dynamic-tier demotions: every such construct stays a compile error in TypeScript and a deferred runtime error in JavaScript, which is how both modes behave today. Use it in CI to keep a program fully static. Library mode is always static-only. See Dynamic tier.
+
The tier policy for code that has no static lowering. auto is the default. static-only guarantees that the program contains no dynamic-tier demotions: every such construct stays a compile error in TypeScript and a deferred runtime error in JavaScript, which is how both modes behave today, and a checked cast whose value does not match its type throws instead of continuing with the value. Use it in CI to keep a program fully static. Library mode is always static-only. See Dynamic tier.
--explain-dynamic[=<name>]
Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as x, Point, or Point.x, show only the matching units. See Dynamic tier.
diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx index 1bb645bd9..b1f6e2c64 100644 --- a/docs/content/docs/coverage.mdx +++ b/docs/content/docs/coverage.mdx @@ -60,7 +60,7 @@ Code that operates on values whose types are known only at runtime, such as `any `--explain-dynamic` lists those functions and the operation that makes each one dynamic. It also lists the units an unsupported construct would move to the dynamic tier: the binding, field, class, function, or module that has no static representation for the construct, with the diagnostic behind each one. These are the constructs the dynamic tier is planned to compile; today they remain blockers in TypeScript and deferred runtime errors in JavaScript. Add a name, such as `--explain-dynamic=Point`, to show only the matching units. With `--print=diagnostics`, the document gains a `tiers` object with the same information. -`--tier=static-only` keeps every unsupported construct a blocker instead of moving it to the dynamic tier; code that already operates on runtime-typed values compiles as before. Today `auto`, the default, behaves the same way. +`--tier=static-only` keeps every unsupported construct a blocker instead of moving it to the dynamic tier; code that already operates on runtime-typed values compiles as before. It also makes a checked cast whose value does not match its type throw, where `auto`, the default, continues with the value in the dynamic tier (see [runtime type checks](/docs/limitations#runtime-type-checks)). Otherwise `auto` behaves the same way today. ## Diagnostic codes diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx index de6c5bd24..f771ec78c 100644 --- a/docs/content/docs/index.mdx +++ b/docs/content/docs/index.mdx @@ -68,25 +68,24 @@ Programs are checked against the `es2025` standard library, scriptc's own declar ## Runtime type checks -Compiled code relies on types, so scriptc checks a value whose type is only known at runtime before using it as a specific type. For example, parsed JSON must match the type you cast it to: +Compiled code relies on types, so scriptc checks a value whose type is only known at runtime before using it as a specific type. Parsed JSON that matches the type you cast it to uses the type's native layout. A value that does not match continues as the value it is, as under Node.js, which does not check the `as Config` assertion: ```ts:cast.ts type Config = { port: number }; -try { - const config = JSON.parse('{"port": "eighty"}') as Config; - console.log(config.port); -} catch (error) { - if (error instanceof Error) console.log(error.message); +for (const text of ['{"port": 8080}', '{"port": "eighty"}']) { + const config = JSON.parse(text) as Config; + console.log(config.port, typeof config.port); } ``` ```console $ scriptc run cast.ts -expected number at $.port, got string +8080 number +eighty string ``` -Under Node.js, the same `as Config` assertion performs no check. See [runtime type checks](/docs/limitations#runtime-type-checks) for details. +See [runtime type checks](/docs/limitations#runtime-type-checks) for where a mismatch still throws. ## Build-time evaluation diff --git a/docs/content/limitations/builtin-apis/eventemitter.mdx b/docs/content/limitations/builtin-apis/eventemitter.mdx index 4f7142094..a9f03b857 100644 --- a/docs/content/limitations/builtin-apis/eventemitter.mdx +++ b/docs/content/limitations/builtin-apis/eventemitter.mdx @@ -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. diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index 73b48c88d..a21f7a617 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -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. diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx index 541c9fb2b..2c08ecc6d 100644 --- a/docs/content/limitations/builtin-function-values.mdx +++ b/docs/content/limitations/builtin-function-values.mdx @@ -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. diff --git a/docs/content/limitations/runtime-type-checks.mdx b/docs/content/limitations/runtime-type-checks.mdx index f6ead482b..bf325b713 100644 --- a/docs/content/limitations/runtime-type-checks.mdx +++ b/docs/content/limitations/runtime-type-checks.mdx @@ -1,5 +1,12 @@ Casts from values whose type is only known at runtime validate the value. -For example, `JSON.parse(s) as Config` throws a catchable error if the data does not match `Config`, identifying the offending path, such as `expected number at $.port, got string`. +A value that matches the type uses the type's native layout. +When the cast initializes a `const` or `let` binding declared in a function or block, and the binding is only read by the code after the cast, a value that does not match continues as the value it is, as under Node.js: with `JSON.parse(s) as Config` and `{"port": "eighty"}`, `config.port` is the string `"eighty"`, missing properties read `undefined`, and extra properties stay visible to `Object.keys`, `JSON.stringify`, and `util.inspect`. +That code then runs on dynamic values, which is slower than native records. +A statement of it that has no dynamic lowering throws a `TypeError` naming the construct and its location when such a value reaches it. + +In the other cases a mismatch throws a catchable `TypeError` identifying the offending path, such as `expected number at $.port, got string`: casts that initialize module-level bindings or appear in other positions, and a mismatched value that leaves the code after its cast through a typed parameter, field, return value, or outer binding, where it is checked against that type. +A value that matches there continues, copied if it is an object. +`--tier=static-only` makes every mismatch throw at the cast. A TypeScript assertion alone does not perform this validation when run under Node.js. Casts between program classes are checked too: casting an instance to a class it does not extend throws a `TypeError`, where Node.js reads the members structurally. diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json deleted file mode 100644 index 167256bca..000000000 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "schemaVersion": 3, - "nodeVersion": "24.22.0", - "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", - "artifactVersion": "ee72820d45a79aa154f0", - "rowCount": 3880 -} diff --git a/internal/compatibility/src/generate.mjs b/internal/compatibility/src/generate.mjs index 8c7f136cf..d2b333bb2 100644 --- a/internal/compatibility/src/generate.mjs +++ b/internal/compatibility/src/generate.mjs @@ -17,13 +17,17 @@ */ import { createHash } from "node:crypto"; -import { readFileSync, writeFileSync } from "node:fs"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; const packageRoot = fileURLToPath(new URL("../", import.meta.url)); const root = fileURLToPath(new URL("../../../", import.meta.url)); const internalOutputPath = `${packageRoot}generated/node-v24-internal.json`; const backlogOutputPath = `${packageRoot}generated/node-v24-backlog.json`; const publicOutputPath = `${root}docs/src/generated/node-v24-compatibility.json`; +// The docs build derives this file's values from the public artifact; a +// merge that keeps a branch's copy would bring back a file every +// compatibility change rewrites. +const retiredOutputPaths = [`${root}docs/src/generated/node-v24-compatibility-meta.json`]; const compilerManifestPath = `${root}packages/compiler/surface-manifest.json`; const compilerCompatPath = `${packageRoot}static-support.json`; const pinPath = `${packageRoot}node-v24.json`; @@ -1009,6 +1013,10 @@ function publicSnapshot(snapshot) { } function checkLocal() { + for (const path of retiredOutputPaths) { + if (existsSync(path)) + throw new Error(`${path.slice(root.length)} is no longer generated; delete it`); + } const snapshot = JSON.parse(readFileSync(internalOutputPath, "utf8")); if (snapshot.nodeVersion !== pin.version || snapshot.nodeCommit !== pin.commit) { throw new Error( diff --git a/packages/cli-darwin-arm64/package.json b/packages/cli-darwin-arm64/package.json index ff55c940e..3c6e73e91 100644 --- a/packages/cli-darwin-arm64/package.json +++ b/packages/cli-darwin-arm64/package.json @@ -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", diff --git a/packages/cli-darwin-x64/package.json b/packages/cli-darwin-x64/package.json index b3b0a58dc..51121824b 100644 --- a/packages/cli-darwin-x64/package.json +++ b/packages/cli-darwin-x64/package.json @@ -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", diff --git a/packages/cli-linux-arm64-gnu/package.json b/packages/cli-linux-arm64-gnu/package.json index ef9fe8bcc..1273cdfb8 100644 --- a/packages/cli-linux-arm64-gnu/package.json +++ b/packages/cli-linux-arm64-gnu/package.json @@ -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", diff --git a/packages/cli-linux-arm64-musl/package.json b/packages/cli-linux-arm64-musl/package.json index a33eb60ab..bdba7d919 100644 --- a/packages/cli-linux-arm64-musl/package.json +++ b/packages/cli-linux-arm64-musl/package.json @@ -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", diff --git a/packages/cli-linux-x64-gnu/package.json b/packages/cli-linux-x64-gnu/package.json index 25cd08204..3de4dcb72 100644 --- a/packages/cli-linux-x64-gnu/package.json +++ b/packages/cli-linux-x64-gnu/package.json @@ -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", diff --git a/packages/cli-linux-x64-musl/package.json b/packages/cli-linux-x64-musl/package.json index b07577400..97a1fae89 100644 --- a/packages/cli-linux-x64-musl/package.json +++ b/packages/cli-linux-x64-musl/package.json @@ -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", diff --git a/packages/cli-win32-x64-msvc/package.json b/packages/cli-win32-x64-msvc/package.json index 23dc94994..9dcd31fa2 100644 --- a/packages/cli-win32-x64-msvc/package.json +++ b/packages/cli-win32-x64-msvc/package.json @@ -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", diff --git a/packages/cli/package.json b/packages/cli/package.json index 434b76fe6..cfe908775 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 411291d94..6dfcc90ef 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -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; diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 4cdb7ccf3..70bf3b27a 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -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" }, diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 38f44e58e..8db629857 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -26,6 +26,9 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { dc: features.dc, dynAsync: features.dynAsync, ...(features.dynTier ? { dynTier: true } : {}), + ...(features.streamDyn ? { streamDyn: true } : {}), + ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), + ...(features.dynBufferNum ? { dynBufferNum: true } : {}), events: features.processEvents, emitter: features.emitter, symbol: features.symbol, @@ -33,6 +36,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, diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts index 42275c5af..cb929e4b0 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -43,6 +43,7 @@ export async function compilePackedLibrary( searchParams: options.searchParams ?? false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index be1c58941..8a2a4659a 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -75,17 +75,25 @@ test("releases split into an inlinable fast path and an out-of-line remainder", const llvm = emitLlvmModule(mod, { pointerBits: 64 }); const body = (name: string): string => new RegExp(`^define internal void @${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(llvm)?.[0] ?? ""; - // A hierarchy rooted in the program: a surviving release decrements and - // takes the possible-root step without loading the vtable. + // A hierarchy rooted in the program: a surviving release decrements the + // count (the low half of the rc word, which keeps the class id in its high + // half) and takes the possible-root step without loading the vtable. const fast = body(mangleClassRelease("Node")); - expect(fast).toContain("%shared = icmp ult i64 %rcm2, -3"); + expect(fast).toContain("%shared = icmp ult i32 %rcm2, -3"); expect(fast).toContain("load i32, ptr %bufp"); expect(fast).toContain(`call void @${mangleClassRelease("Node")}_slow(ptr %o)`); expect(fast).not.toContain("%ScrVt"); // Only the last reference dispatches, from a noinline remainder. const slow = body(`${mangleClassRelease("Node")}_slow`); expect(slow).toMatch(/noinline/); + // The class id indexes the hierarchy's vtable array. + expect(slow).toContain("%vt.id = load i32, ptr %vt.idp"); + expect(slow).toContain("ptr @sc_vta_Node, i64 0, i64 %vt.ix64"); expect(slow).toContain("getelementptr inbounds %ScrVt, ptr %vt, i64 0, i32 2"); + // 32-bit targets keep the vtable word. + const wasm = emitLlvmModule(mod, { pointerBits: 32 }); + expect(wasm).toContain("; class Node (vt at 1)"); + expect(wasm).not.toContain("@sc_vta_Node"); }); test("ProgramLifetime hierarchies are never freed and take no reference counts", () => { @@ -121,9 +129,11 @@ test("ProgramLifetime hierarchies are never freed and take no reference counts", expect(body(mangleClassRetain(cls)), cls).toMatch(/entry:\n {2}ret ptr %o\n}/); expect(body(mangleClassRelease(cls)), cls).toMatch(/entry:\n {2}ret void\n}/); // new: no collector header (no descriptor, no scr_cyc_alloc) and the - // program-lifetime count 2^63, not SIZE_MAX (which means published). + // program-lifetime count 2^31 under the class id (Node's preorder number + // 0 + 1, Leaf's 1 + 1), not SIZE_MAX (which means published). const nw = body(mangleClassNew(cls)); - expect(nw, cls).toContain("store i64 -9223372036854775808, ptr %o"); + const id = cls === "Node" ? 1 : 2; + expect(nw, cls).toContain(`store i64 ${id * 2 ** 32 + 2 ** 31}, ptr %o.vt`); expect(nw, cls).not.toContain("store i64 -1, ptr %o"); expect(nw, cls).not.toContain("scr_cyc_alloc"); expect(llvm, cls).not.toContain(`@${mangleClassTrace(cls)}.cyctype`); @@ -206,13 +216,15 @@ test("published ProgramLifetime instances use the published mark every other obj // a refusal restores the program-lifetime count); untraced, so it never // leaves a collector buffer. expect(llvm).toContain("call void @scr_publish(ptr"); - expect(llvm).toMatch(/@scr_pub_mark\(ptr %ctx, ptr %o, i1 false, i32 4\)/); - // The frozen-object guard of the store tests the count against SIZE_MAX: - // an unpublished instance (2^63) stays writable. + // Kind 4 (an object) | 0x100 (the rc word keeps a class id: the mark + // writes only the count). + expect(llvm).toMatch(/@scr_pub_mark\(ptr %ctx, ptr %o, i1 false, i32 260\)/); + // The frozen-object guard of the store tests the count against UINT32_MAX: + // an unpublished instance (2^31) stays writable. const store = llvm.split(/^define /m).find((fn) => fn.includes("call void @scr_throw_published_field(")) ?? ""; - expect(store).toMatch(/icmp eq i64 %\S+, -1/); + expect(store).toMatch(/icmp eq i32 %\S+, -1/); expect(store).toContain("call void @scr_publish(ptr"); // 32-bit targets use 2^31. const llvm32 = emitLlvmModule(mod, { pointerBits: 32 }); diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index f5a882bbf..5ad29430d 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -26,6 +26,7 @@ import { mangleCtorThunk, mangleFunction, mangleGlobal, + mangleVtArray, mangleVtInstance, mangleVtStruct, } from "../mangle.js"; @@ -81,6 +82,12 @@ export class LlClassMeta { * member's struct carries the i32 slot word before its fields * (classSlotWordIndex). */ threadSlot = false; + /** A member of a program-rooted hierarchy on a 64-bit target: instead of + * a vtable word, the rc word holds the class id (classIdRcWord) in its + * high 32 bits (CLASS_ID_OFFSET) and the reference count in its low 32 + * bits, and the hierarchy's vtables are one array indexed by the id + * (mangleVtArray). */ + idInRc = false; constructor(readonly def: IrClassDef) { this.root = this; @@ -96,6 +103,7 @@ export class LlClassMeta { export function buildClassGraph( mod: IrModule, fnByName: Map, + sizeType: "i32" | "i64" = "i64", ): Map { const metaMap = new Map(); for (const cls of mod.classes ?? []) { @@ -122,6 +130,11 @@ export function buildClassGraph( if (meta.base === null) number(meta, meta); meta.hierarchy = meta.base !== null || meta.children.length > 0; } + // The runtime reads the vtable word of runtime-rooted hierarchies (Error, + // EventEmitter, EventTarget, Event and the streams) and 32-bit targets + // have no spare half in the rc word: those keep the vtable word. + for (const meta of metaMap.values()) + meta.idInRc = sizeType === "i64" && meta.hierarchy && meta.root.def.runtime !== true; const slotRoots = new Set(); for (const key of mod.threadSlotKeys ?? []) { const meta = metaMap.get(key); @@ -336,11 +349,12 @@ function runtimePrefixOf(meta: LlClassMeta): RuntimePrefix | null { } /** The GEP index after a class's header words: rc at 0, the vtable word - * at 1 on hierarchy members, then a runtime prefix (state + display name) + * at 1 on hierarchy members that do not keep their class id in the rc + * word (LlClassMeta.idInRc), then a runtime prefix (state + display name) * on classes rooted at a runtime base, then the stream-state slot on * stream-rooted ones. */ function headerEnd(meta: LlClassMeta): number { - if (!meta.hierarchy) return 1; + if (!meta.hierarchy || meta.idInRc) return 1; if (streamRooted(meta)) return 5; return runtimePrefixOf(meta) ? 4 : 2; } @@ -445,6 +459,69 @@ export function programLifetimeRc(sizeType: "i32" | "i64"): string { return sizeType === "i32" ? "-2147483648" : "-9223372036854775808"; } +/** Lines that load the preorder number of the dynamic class of `obj`, an + * instance of `meta`'s hierarchy whose struct is `struct`, into `pre` (a + * `sizeType`): from the high half of the rc word (LlClassMeta.idInRc), or + * from the vtable its vtable word points at. */ +export function classPreLines( + meta: LlClassMeta, + struct: string, + obj: string, + pre: string, + sizeType: "i32" | "i64", +): string[] { + if (meta.idInRc) + return [ + ` ${pre}.idp = getelementptr inbounds i8, ptr ${obj}, i64 ${CLASS_ID_OFFSET}`, + ` ${pre}.id = load i32, ptr ${pre}.idp`, + ` ${pre}.n = add i32 ${pre}.id, -1 ; the class id is the preorder number + 1`, + ` ${pre} = zext i32 ${pre}.n to ${sizeType}`, + ]; + return [ + ` ${pre}.vtp = getelementptr inbounds %${struct}, ptr ${obj}, i64 0, i32 1`, + ` ${pre}.vt = load ptr, ptr ${pre}.vtp`, + ` ${pre}.prep = getelementptr inbounds %ScrVt, ptr ${pre}.vt, i64 0, i32 0`, + ` ${pre} = load ${sizeType}, ptr ${pre}.prep`, + ]; +} + +/** Lines that load the class id of the LlClassMeta.idInRc instance `obj` + * (`group` adds !invariant.group to the load) and point `vt` at its class's + * element of the hierarchy's vtable array. */ +export function classIdVtLines( + meta: LlClassMeta, + obj: string, + vt: string, + group: string, +): string[] { + const t = `${vt}.`; + const root = meta.root; + const vtt = `%${mangleVtStruct(root.def.name)}`; + const n = root.post - root.pre + 1; + return [ + ` ${t}idp = getelementptr inbounds i8, ptr ${obj}, i64 ${CLASS_ID_OFFSET}`, + ` ${t}id = load i32, ptr ${t}idp${group}`, + ` ${t}ix = sub i32 ${t}id, ${root.pre + 1}`, + ` ${t}ix64 = zext i32 ${t}ix to i64`, + ` ${vt} = getelementptr inbounds [${n} x ${vtt}], ptr @${mangleVtArray(root.def.name)}, i64 0, i64 ${t}ix64`, + ]; +} + +/** The byte offset of the class id in the rc word of an instance that + * keeps it there (LlClassMeta.idInRc): the high half of the little-endian + * 64-bit word. The count is the i32 at offset 0. */ +export const CLASS_ID_OFFSET = 4; + +/** The rc word an LlClassMeta.idInRc instance is created with: the class + * id, its preorder number plus 1, in the high 32 bits, and in the low 32 bits + * a count of 1, or for a program-lifetime instance the midpoint of the 32-bit + * count (as programLifetimeRc is of the 32-bit word). The id is never 0, + * 0x80000000 or 0xffffffff, the high halves of other objects' rc words, so + * the runtime can tell an object with a class id (scr_rc.h scr_rc_class_id). */ +export function classIdRcWord(meta: LlClassMeta, lifetime: boolean): string { + return (BigInt(meta.pre + 1) * 2n ** 32n + (lifetime ? 2n ** 31n : 1n)).toString(); +} + function undefFieldInits( host: ClassHost, meta: LlClassMeta, @@ -524,17 +601,18 @@ export function emitClassShapes( // ScrEmitter* / ScrStream* / ScrEventTargetObj* / ScrEventObj* are the // usual pointer reinterprets. const runtimePrefix = runtimePrefixOf(meta); - const prefix = meta.hierarchy - ? streamRooted(meta) - ? ["ptr", "ptr", "ptr", "ptr"] - : runtimePrefix - ? ["ptr", "ptr", "ptr"] - : ["ptr"] - : []; + const prefix = + meta.hierarchy && !meta.idInRc + ? streamRooted(meta) + ? ["ptr", "ptr", "ptr", "ptr"] + : runtimePrefix + ? ["ptr", "ptr", "ptr"] + : ["ptr"] + : []; const members = [...prefix, ...(meta.threadSlot ? ["i32"] : []), ...fieldTys]; typeDefs.push( `%${mangleClassStruct(cls.name)} = type { ${host.sizeType}${members.length ? ", " + members.join(", ") : ""} } ` + - `; class ${cls.name}${meta.hierarchy ? " (vt at 1)" : ""}${streamRooted(meta) ? " (ScrStream prefix at 2)" : runtimePrefix ? ` (${runtimePrefix.struct} prefix at 2)` : ""} { ${cls.fields.map((f) => llvmCommentText(f.name)).join("; ")} }`, + `; class ${cls.name}${meta.idInRc ? " (class id in rc)" : meta.hierarchy ? " (vt at 1)" : ""}${streamRooted(meta) ? " (ScrStream prefix at 2)" : runtimePrefix ? ` (${runtimePrefix.struct} prefix at 2)` : ""} { ${cls.fields.map((f) => llvmCommentText(f.name)).join("; ")} }`, ); } @@ -559,18 +637,40 @@ export function emitClassShapes( // One constant vtable instance per emitted hierarchy class: interval, // direct release, and the class's dispatch entry for every slot — the // method functions THEMSELVES (no adapters; see the header comment). - for (const cls of emitted) { - const meta = metaMap.get(cls.name)!; - if (!meta.hierarchy) continue; + const vtInit = (meta: LlClassMeta): string => { const entries = vtEntriesFor(meta).map(({ slot, impl }) => impl === null ? `ptr null` // outside the declaring subtree / fully-abstract chain : `ptr @${host.virtualEntry?.(`%${impl.def.name}.${slot.method}`) ?? mangleFunction(`%${impl.def.name}.${slot.method}`)}`, ); - const head = `%ScrVt { ${host.sizeType} ${meta.pre}, ${host.sizeType} ${meta.post}, ptr @${mangleClassReleaseDirect(cls.name)} }`; + const head = `%ScrVt { ${host.sizeType} ${meta.pre}, ${host.sizeType} ${meta.post}, ptr @${mangleClassReleaseDirect(meta.def.name)} }`; + return `{ ${[head, ...entries].join(", ")} }`; + }; + for (const cls of emitted) { + const meta = metaMap.get(cls.name)!; + if (!meta.hierarchy || meta.idInRc) continue; defs.push( `@${mangleVtInstance(cls.name)} = internal constant %${mangleVtStruct(meta.root.def.name)} ` + - `{ ${[head, ...entries].join(", ")} } ; class ${cls.name}`, + `${vtInit(meta)} ; class ${cls.name}`, + ``, + ); + } + // A hierarchy whose instances keep their class id in the rc word has + // one array of its classes' vtables, in preorder: the vtable of an + // instance is the element at its number minus the root's. + for (const name of roots) { + const root = metaMap.get(name)!; + if (!root.idInRc) continue; + const members = [...metaMap.values()] + .filter((m) => m.root === root) + .sort((a, b) => a.pre - b.pre); + const vtt = `%${mangleVtStruct(root.def.name)}`; + defs.push( + `@${mangleVtArray(root.def.name)} = internal constant [${members.length} x ${vtt}] [`, + ...members.map( + (m, i) => ` ${vtt} ${vtInit(m)}${i + 1 < members.length ? "," : ""} ; class ${m.def.name}`, + ), + `]`, ``, ); } @@ -605,6 +705,7 @@ export function emitClassShapes( release: `@${release}`, destroy: `${teardown}_destroy`, traced: host.tracedShapes.has(`object:${cls.name}`), + ...(meta.idInRc ? { count: "i32" as const } : {}), }, ]); } @@ -658,7 +759,13 @@ export function emitClassShapes( // retain: NULL-tolerant, immortal-skip — layout-generic (rc at 0), so // hierarchy members need no dispatch. - defs.push(...retainBody(host, mangleClassRetain(cls.name), `retain ${cls.name}`, lifetime), ``); + // An instance that keeps its class id in the rc word counts in the + // low half (CLASS_ID_OFFSET). + const count = meta.idInRc ? "i32" : host.sizeType; + defs.push( + ...retainBody(host, mangleClassRetain(cls.name), `retain ${cls.name}`, lifetime, count), + ``, + ); // An out-of-line teardown tears typed children down directly // (directChildReleaseLines). It takes the chain length when it has such @@ -714,6 +821,7 @@ export function emitClassShapes( traced, `release ${cls.name}`, lifetime, + count, ), ``, ); @@ -723,12 +831,16 @@ export function emitClassShapes( ` %isnull = icmp eq ptr %o, null`, ` br i1 %isnull, label %done, label %check`, `check:`, - ` %rc = load ${host.sizeType}, ptr %o`, - ` %imm = icmp eq ${host.sizeType} %rc, -1`, + ` %rc = load ${count}, ptr %o`, + ` %imm = icmp eq ${count} %rc, -1`, ` br i1 %imm, label %done, label %disp`, `disp:`, - ` %vtp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 1`, - ` %vt = load ptr, ptr %vtp`, + ...(meta.idInRc + ? classIdVtLines(meta, "%o", "%vt", "") + : [ + ` %vtp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 1`, + ` %vt = load ptr, ptr %vtp`, + ]), ` %relp = getelementptr inbounds %ScrVt, ptr %vt, i64 0, i32 2`, ` %rel = load ptr, ptr %relp`, ` call void %rel(ptr %o) ; the DYNAMIC class's teardown`, @@ -743,10 +855,10 @@ export function emitClassShapes( const reld: string[] = [ `define internal void @${mangleClassReleaseDirect(cls.name)}(ptr %o) ${FN_ATTRS} { ; direct release ${cls.name}`, `entry:`, - ` %rc = load ${host.sizeType}, ptr %o`, - ` %n = sub ${host.sizeType} %rc, 1`, - ` store ${host.sizeType} %n, ptr %o`, - ` %dead = icmp eq ${host.sizeType} %n, 0`, + ` %rc = load ${count}, ptr %o`, + ` %n = sub ${count} %rc, 1`, + ` store ${count} %n, ptr %o`, + ` %dead = icmp eq ${count} %n, 0`, ` br i1 %dead, label %free, label %${traced ? "root" : "done"}`, `free:`, ]; @@ -815,10 +927,22 @@ export function emitClassShapes( host, sizeOf, traced && !lifetime ? { type: cycType } : null, - meta.hierarchy ? 16 : 8, + meta.hierarchy && !meta.idInRc ? 16 : 8, ), ); - nw.push(` store ${host.sizeType} ${lifetime ? programLifetimeRc(host.sizeType) : 1}, ptr %o`); + if (meta.idInRc) { + // The class id in the high half of the rc word, the count in the low + // half (classIdRcWord). It never changes while the object lives: + // retains, releases and the runtime write only the low half. + host.declare(`declare ptr @llvm.launder.invariant.group.p0(ptr)`); + nw.push( + ` %o.vt = call ptr @llvm.launder.invariant.group.p0(ptr %o)`, + ` store i64 ${classIdRcWord(meta, lifetime)}, ptr %o.vt ; class ${llvmCommentText(cls.name)} is ${meta.pre}`, + ); + } else + nw.push( + ` store ${host.sizeType} ${lifetime ? programLifetimeRc(host.sizeType) : 1}, ptr %o`, + ); if (lifetime && audit) { // --sanitize builds: the RC audit skips its leak report when the // program keeps program-lifetime objects (scr_console.c), and @@ -826,7 +950,7 @@ export function emitClassShapes( host.declare(`declare void @scr_obj_lifetime_note(ptr)`); nw.push(` call void @scr_obj_lifetime_note(ptr %o)`); } - if (meta.hierarchy) { + if (meta.hierarchy && !meta.idInRc) { // The vtable word is written once, here, and never changes while the // object lives: its store and the virtual-call loads carry // !invariant.group. A recycled block may have held an object of diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index 7e414daee..4c7d649e0 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -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 = { + "%Readable": 2, + "%Writable": 3, + "%Duplex": 4, + "%Transform": 5, + "%PassThrough": 6, + }; + return kinds[className] ?? null; +} + export class LlDyn { private readonly dynMatchers = new Map(); private readonly dynBuilders = new Map(); @@ -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()); @@ -1392,22 +1448,29 @@ export class LlDyn { * differently), runs the checked-dynamic route — scr_json_parse plus the * dynCheck builder — which reports the exact error. Returns +1, or null * with the pending flag set. */ - jsonParseHelper(t: IrType): string | null { + jsonParseHelper(t: IrType, fallback = false): string | null { if (t.kind !== "record" && t.kind !== "array") return null; const key = typeKey(t); - const existing = this.jsonParsers.get(key); + const cacheKey = fallback ? `fallback:${key}` : key; + const existing = this.jsonParsers.get(cacheKey); if (existing) return existing; if (!this.jsonSchemaSupported(t, new Set())) return null; const host = this.host; const name = `sc_jp_${this.jsonParsers.size}`; - this.jsonParsers.set(key, name); + this.jsonParsers.set(cacheKey, name); const schema = this.jsonSchema(t); const check = this.dynCheckHelper(t); host.declare(`declare ptr @scr_json_parse_schema(ptr, ptr)`); host.declare(`declare ptr @scr_json_parse(ptr)`); host.declare(`declare void @scr_dyn_release(ptr)`); + // A fallback site (dynCheck.fallback): a value that fails validation + // parks as the checked value (scr_dyn_check_recover) and the helper + // returns the dummy with no exception pending. Syntax errors still + // throw. + if (fallback) host.declare(`declare zeroext i1 @scr_dyn_check_recover(ptr)`); + const recover = fallback ? [` call zeroext i1 @scr_dyn_check_recover(ptr %d)`] : []; this.defs.push( - `define internal ptr @${name}(ptr %text) ${FN_ATTRS} { ; JSON.parse as ${llvmCommentText(key)}`, + `define internal ptr @${name}(ptr %text) ${FN_ATTRS} { ; JSON.parse as ${llvmCommentText(key)}${fallback ? " (fallback)" : ""}`, `entry:`, ` %fast = call ptr @scr_json_parse_schema(ptr %text, ptr @${schema})`, ` %hit = icmp ne ptr %fast, null`, @@ -1422,6 +1485,7 @@ export class LlDyn { ` ret ptr null`, `check:`, ` %v = call ptr @${check}(ptr %d, ptr null)`, + ...recover, ` call void @scr_dyn_release(ptr %d)`, ` ret ptr %v`, `}`, @@ -1491,6 +1555,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 +2340,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 +2409,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; diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 4e4f11828..b373613e0 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -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), ``); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index b72a6e226..00b4c59a8 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -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"; @@ -262,6 +266,7 @@ import { buildClassGraph, classMembershipIntervals, classFieldIndex, + classPreLines, classStructSym, programLifetimeClass, type LlClassMeta, @@ -902,6 +907,11 @@ export class LlEmitter { /** Identity-preserving static→dyn capsules for program classes and Web * APIs whose values remain directly observable. */ readonly liveDynRefAdapters = new Map(); + 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(); readonly dynPromiseAdapters = new Map(); @@ -1405,6 +1415,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", @@ -1461,7 +1472,7 @@ export class LlEmitter { // The class graph: base/children links, hierarchy membership, the // whole-program preorder numbering for instanceof over runtime and // compiled error objects, and the per-hierarchy virtual slot lists. - this.classMeta = buildClassGraph(mod, this.fnByName); + this.classMeta = buildClassGraph(mod, this.fnByName, this.sizeType); if (mod.lifetimeRegions === true) { this.regions = new LifetimeRegions( this.shapeHost, @@ -2812,10 +2823,16 @@ export class LlEmitter { } else if (t.kind === "string") { this.declare(`declare void @scr_throw_str(ptr)`); B.line(`call void @scr_throw_str(ptr ${v.name})`); - } else if (t.kind === "object" && this.classMeta.get(t.className)?.hierarchy === true) { + } else if ( + t.kind === "object" && + this.classMeta.get(t.className)?.hierarchy === true && + this.classMeta.get(t.className)?.idInRc !== true + ) { // Hierarchy instances carry a vtable word: the OBJ kind keeps the // dynamic class inspectable (catch-binding instanceof, the uncaught - // printer's "name: message" for Error instances). + // printer's "name: message" for Error instances). Instances that keep + // their class id in the rc word are never Errors and are thrown as + // REF payloads (narrow-guards.ts emitCaughtClassIdInstanceof). const rc = vAdapters(this.shapeHost, t); this.declare(`declare void @scr_throw_obj(ptr, ptr, ptr, ptr)`); B.line( @@ -3148,20 +3165,20 @@ export class LlEmitter { return { ptr: p, type, inline: this.inlineUnions.classField(className, field) }; } - /** `o->vt->pre` — the dynamic class's preorder number (borrowed object; - * the static class names which struct spelling carries the vt word). */ + /** The dynamic class's preorder number (borrowed object; the static class + * names the struct spelling and where the number is, classPreLines). */ loadVtPre(objName: string, staticClassName: string): string { const B = this.B; - const vtp = B.tmp(); - const vt = B.tmp(); - const prep = B.tmp(); const pre = B.tmp(); - B.line( - `${vtp} = getelementptr inbounds %${classStructSym(staticClassName)}, ptr ${objName}, i64 0, i32 1`, - ); - B.line(`${vt} = load ptr, ptr ${vtp}`); - B.line(`${prep} = getelementptr inbounds %ScrVt, ptr ${vt}, i64 0, i32 0`); - B.line(`${pre} = load ${this.sizeType}, ptr ${prep}`); + const meta = this.classMetaOf(staticClassName); + for (const line of classPreLines( + meta, + classStructSym(staticClassName), + objName, + pre, + this.sizeType, + )) + B.line(line.trimStart()); return pre; } @@ -5093,6 +5110,7 @@ export class LlEmitter { pre: meta.pre, post: meta.post, runtimeRooted: meta.root.def.runtime === true, + idInRc: meta.idInRc, // Inline scalar-union fields are plain bits: nothing to push. fields: meta.def.fields.flatMap((f) => this.inlineUnions.classField(className, f.name) @@ -5131,15 +5149,17 @@ export class LlEmitter { return this.own({ name: value.name, type: e.type }); } - /** Branches on "`obj` is published" (rc == SIZE_MAX); leaves the builder - * in the cold block and answers the continuation label. */ - private emitPublishedTest(obj: string): string { + /** Branches on "`obj` is published" (rc == SIZE_MAX, or a count of + * UINT32_MAX in the low half for `count` i32: classes.ts + * LlClassMeta.idInRc); leaves the builder in the cold block and answers + * the continuation label. */ + private emitPublishedTest(obj: string, count: "i32" | "i64" = this.sizeType): string { const B = this.B; const rc = B.tmp(), frozen = B.tmp(), expected = B.tmp(); - B.line(`${rc} = load ${this.sizeType}, ptr ${obj}`); - B.line(`${frozen} = icmp eq ${this.sizeType} ${rc}, -1`); + B.line(`${rc} = load ${count}, ptr ${obj}`); + B.line(`${frozen} = icmp eq ${count} ${rc}, -1`); this.declare(`declare i1 @llvm.expect.i1(i1, i1)`); B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${frozen}, i1 false)`); const cold = B.newLabel("pub.frozen"), @@ -5179,22 +5199,17 @@ export class LlEmitter { if (published === null) return; if (className !== null && !published.classGuarded(className)) return; const B = this.B; - const ok = this.emitPublishedTest(obj); - let owner: string; const meta = className === null ? undefined : this.classMeta.get(className); + const ok = this.emitPublishedTest(obj, meta?.idInRc === true ? "i32" : this.sizeType); + let owner: string; if (meta === undefined) owner = this.cstr("Object"); else if (!meta.hierarchy) owner = this.cstr(meta.def.jsName ?? meta.def.name); else { // The DYNAMIC class names the object, as in Node. this.publishedNamesSym ??= "@sc_pub_class_names"; - const vtp = B.tmp(), - vt = B.tmp(), - pre = B.tmp(), + const pre = this.loadVtPre(obj, meta.def.name), slot = B.tmp(), name = B.tmp(); - B.line(`${vtp} = getelementptr inbounds ptr, ptr ${obj}, i64 1`); - B.line(`${vt} = load ptr, ptr ${vtp}`); - B.line(`${pre} = load ${this.sizeType}, ptr ${vt}`); B.line( `${slot} = getelementptr inbounds ptr, ptr ${this.publishedNamesSym}, ${this.sizeType} ${pre}`, ); @@ -5599,8 +5614,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 { diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index 6da63977d..901a8749e 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -374,7 +374,7 @@ export function emitSerializationExpr( // falls back to exactly this route whenever the fast path declines. const parsed = e.value; if (!e.preserveRefs && parsed.kind === "libCall" && parsed.fn === "json.parse") { - const fused = host.dyn.jsonParseHelper(e.type); + const fused = host.dyn.jsonParseHelper(e.type, e.fallback === true); if (fused) { const text = emitBorrowedInput(host, parsed.args[0]!); const r = B.tmp(); @@ -400,6 +400,12 @@ export function emitSerializationExpr( B.line( `${t} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr ${dynV.name}, ptr null)`, ); + // A fallback site: a value that fails validation parks as the + // checked value instead of throwing (dynCheck.fallback). + if (e.fallback) { + host.declare(`declare zeroext i1 @scr_dyn_check_recover(ptr)`); + B.line(`call zeroext i1 @scr_dyn_check_recover(ptr ${dynV.name})`); + } const out = host.own({ name: t, type: e.type }); host.emitPendingCheck(); return out; diff --git a/packages/compiler/src/backend/llvm/expr-callbacks.ts b/packages/compiler/src/backend/llvm/expr-callbacks.ts index 28bd15759..b91cdc543 100644 --- a/packages/compiler/src/backend/llvm/expr-callbacks.ts +++ b/packages/compiler/src/backend/llvm/expr-callbacks.ts @@ -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`, `}`, ``, diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index b49c8895e..43dfce459 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -34,6 +34,7 @@ import { } from "../mangle.js"; import { classEnvironmentIndex, + classIdVtLines, classStructSym, vtEntriesFor, type LlClassMeta, @@ -1193,15 +1194,20 @@ export function emitCallExpr( if (!borrowed.has(index)) host.toFrame(a); }); const recv = args[0]!.name; - const vtp = B.tmp(); const vt = B.tmp(); const fnp = B.tmp(); const fn = B.tmp(); - B.line( - `${vtp} = getelementptr inbounds %${classStructSym(e.className)}, ptr ${recv}, i64 0, i32 1`, - ); // Set once at allocation (classes.ts launders each new object). - B.line(`${vt} = load ptr, ptr ${vtp}, !invariant.group !{}`); + if (meta.idInRc) + for (const line of classIdVtLines(meta, recv, vt, ", !invariant.group !{}")) + B.line(line.trimStart()); + else { + const vtp = B.tmp(); + B.line( + `${vtp} = getelementptr inbounds %${classStructSym(e.className)}, ptr ${recv}, i64 0, i32 1`, + ); + B.line(`${vt} = load ptr, ptr ${vtp}, !invariant.group !{}`); + } B.line( `${fnp} = getelementptr inbounds %${mangleVtStruct(meta.root.def.name)}, ptr ${vt}, i64 0, i32 ${slotIdx + 1}`, ); diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index bc4f1128a..da3e1bfa4 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -28,6 +28,7 @@ import { elemAccess, vAdapters } from "./shapes.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js"; import { emitUnionWiden } from "./expr-records.js"; +import { DYN_BUFFER_NUM_METHODS } from "../../ir/runtime-features.js"; export function emitDynamicExpr( host: LlvmEmitterContext, @@ -226,9 +227,12 @@ export function emitDynamicExpr( }); argsPtr = arr; } - host.declare( - `declare ptr @scr_dyn_invoke_prepared(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`, - ); + // Fixed-width Buffer method names take scr_bytes_dyn.c's entry, which + // runs them on Buffer receivers and dispatches any other receiver. + const invoke = DYN_BUFFER_NUM_METHODS.has(e.method) + ? "scr_dyn_invoke_buffer_num" + : "scr_dyn_invoke_prepared"; + host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`); let name = host.cstr(e.calleeName); if (e.calleeNameValue !== undefined) { const value = emitBorrowedInput(host, e.calleeNameValue); @@ -237,7 +241,7 @@ export function emitDynamicExpr( } const t = B.tmp(); B.line( - `${t} = call ptr @scr_dyn_invoke_prepared(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`, + `${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`, ); const out = host.own({ name: t, type: e.type }); host.emitPendingCheck(); @@ -1058,6 +1062,42 @@ export function emitDynamicExpr( host.declare( `declare ptr @scr_caught_check_obj(ptr, ${host.sizeType}, ${host.sizeType}, ptr)`, ); + if (target.idInRc) { + // A REF payload (emitCaughtInstanceof): a match retains it through + // its adapter; anything else gets the runtime's TypeError from the + // check against an empty interval. + const ok = emitCaughtInstanceof(host, c.name, e.className); + const yes = B.newLabel("caught.cast"), + no = B.newLabel("caught.cast.fail"), + join = B.newLabel("caught.cast.done"); + B.condBr(ok, yes, no); + B.startBlock(yes); + const payloadSlot = B.tmp(), + payload = B.tmp(), + retainSlot = B.tmp(), + retain = B.tmp(), + retained = B.tmp(); + B.line(`${payloadSlot} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 4`); + B.line(`${payload} = load ptr, ptr ${payloadSlot}`); + B.line(`${retainSlot} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 5`); + B.line(`${retain} = load ptr, ptr ${retainSlot}`); + B.line(`${retained} = call ptr ${retain}(ptr ${payload})`); + const fromYes = B.currentLabel(); + B.br(join); + B.startBlock(no); + const failed = B.tmp(); + B.line( + `${failed} = call ptr @scr_caught_check_obj(ptr ${c.name}, ${host.sizeType} 1, ${host.sizeType} 0, ptr ${host.cstr(display)})`, + ); + const fromNo = B.currentLabel(); + B.br(join); + B.startBlock(join); + const t = B.tmp(); + B.line(`${t} = phi ptr [ ${retained}, %${fromYes} ], [ ${failed}, %${fromNo} ]`); + const out = host.own({ name: t, type: e.type }); + host.emitPendingCheck(); + return out; + } let pre = String(target.pre); let post = String(target.post); const intervals = classMembershipIntervals(host.classMeta, target.def.name); diff --git a/packages/compiler/src/backend/llvm/expr-primitives.test.ts b/packages/compiler/src/backend/llvm/expr-primitives.test.ts index 1790854ef..eb6a88d0a 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.test.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.test.ts @@ -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"); +}); diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index 847ed1e20..0952ea993 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -20,7 +20,7 @@ import { } from "../../ir/ir.js"; import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js"; import { BlockBuilder } from "./blocks.js"; -import { type LlClassMeta, classFieldIndex, classStructSym } from "./classes.js"; +import { type LlClassMeta, classFieldIndex, classPreLines, classStructSym } from "./classes.js"; import { SCR_ARR_FIELD, llvmCommentText } from "./common.js"; import { FN_ATTRS, @@ -232,10 +232,13 @@ export function streamTypedRefCommitAdapter( ]; if (meta.hierarchy && meta.children.length) { lines.push( - ` %derived_vt_slot = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 1`, - ` %derived_vt = load ptr, ptr %derived_vt_slot`, - ` %derived_pre_slot = getelementptr inbounds %ScrVt, ptr %derived_vt, i64 0, i32 0`, - ` %derived_pre = load ${host.sizeType}, ptr %derived_pre_slot`, + ...classPreLines( + meta, + classStructSym(t.className), + "%target", + "%derived_pre", + host.sizeType, + ), ); const descendants = (current: typeof meta): (typeof meta)[] => current.children.flatMap((child) => [child, ...descendants(child)]); @@ -728,16 +731,9 @@ export function streamTypedRefMaterializeAdapter( } if (adapter.classOps !== undefined) liveClassOps(host, snapshot, meta); if (meta.hierarchy && meta.children.length) { - const vtSlot = B.tmp(), - vt = B.tmp(), - preSlot = B.tmp(), - pre = B.tmp(); - B.line( - `${vtSlot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 1`, - ); - B.line(`${vt} = load ptr, ptr ${vtSlot}`); - B.line(`${preSlot} = getelementptr inbounds %ScrVt, ptr ${vt}, i64 0, i32 0`); - B.line(`${pre} = load ${host.sizeType}, ptr ${preSlot}`); + const pre = B.tmp(); + for (const line of classPreLines(meta, classStructSym(t.className), "%p", pre, host.sizeType)) + B.line(line.trimStart()); const descendants = (current: typeof meta): (typeof meta)[] => current.children.flatMap((child) => [child, ...descendants(child)]); for (const child of descendants(meta).reverse()) { @@ -766,8 +762,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) { @@ -1359,16 +1360,9 @@ function liveClassOps(host: LlvmEmitterContext, snapshot: string, meta: LlClassM if (subclasses.length > 0) { exact = `@${snapshot}_cexact`; const B = new BlockBuilder(); - const vtSlot = B.tmp(), - vt = B.tmp(), - preSlot = B.tmp(), - pre = B.tmp(); - B.line( - `${vtSlot} = getelementptr inbounds %${classStructSym(meta.def.name)}, ptr %p, i64 0, i32 1`, - ); - B.line(`${vt} = load ptr, ptr ${vtSlot}`); - B.line(`${preSlot} = getelementptr inbounds %ScrVt, ptr ${vt}, i64 0, i32 0`); - B.line(`${pre} = load ${host.sizeType}, ptr ${preSlot}`); + const pre = B.tmp(); + for (const line of classPreLines(meta, classStructSym(meta.def.name), "%p", pre, host.sizeType)) + B.line(line.trimStart()); for (const child of subclasses.reverse()) { const childAdapter = host.liveDynRefAdapter({ kind: "object", className: child.def.name }); const match = B.tmp(), diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index 6cec9d403..283c5b38d 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -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": @@ -859,6 +865,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { case "blob": case "wasm": case "record": + case "checkedCast": case "disposable": case "punycode": case "constants": diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts index 4a7be1a35..097fcb4de 100644 --- a/packages/compiler/src/backend/llvm/lib-process.ts +++ b/packages/compiler/src/backend/llvm/lib-process.ts @@ -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(); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 91242b45c..386c592c6 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -444,6 +444,7 @@ export const LIB_FN_SYMS: Record = { "buffer.poolFromBytes": "scr_buffer_from_bytes", "buffer.poolFromArray": "scr_buffer_from_arr", "buffer.poolFromDyn": "scr_buffer_from_dyn_pooled", + "buffer.poolFromDynRef": "scr_buffer_from_dyn_ref_pooled", "buffer.poolSize": "scr_buffer_pool_size", "buffer.setPoolSize": "scr_buffer_set_pool_size", "buffer.byteLenDyn": "scr_buffer_byte_length_dyn", @@ -643,6 +644,9 @@ export const LIB_FN_SYMS: Record = { "insp.circular": "scr_insp_circular", // Undeclared source properties kept beside a checked record (scr_json.c). "record.sourceHas": "scr_record_source_has", + // Checked-cast fallback reads (scr_json.c); take answers +1. + "checkedCast.fallbackTaken": "scr_dyn_check_fallback_taken", + "checkedCast.fallbackTake": "scr_dyn_check_fallback_take", // WHATWG URL + URLSearchParams (scr_url.c / scr_url_params.c): // constructions +1; url.new, the fileURLToPath pair, the win32 // pathToFileURL flavor, and sp.fromPairs throw catchably (may-throw). @@ -742,6 +746,8 @@ export const LIB_FN_SYMS: Record = { "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 +1465,7 @@ export const LIB_FN_SYMS: Record = { "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 +1544,8 @@ export function mockTimerSym( export const USES_TIMERS_LIB_FNS = new Set([ "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. diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index 0ded59a1a..a319d0257 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -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 diff --git a/packages/compiler/src/backend/llvm/narrow-guards.ts b/packages/compiler/src/backend/llvm/narrow-guards.ts index 910af6f79..4744bed7c 100644 --- a/packages/compiler/src/backend/llvm/narrow-guards.ts +++ b/packages/compiler/src/backend/llvm/narrow-guards.ts @@ -14,7 +14,7 @@ * throw. The guards are cheap: one load and compare per site, which LLVM * folds where an identical test already dominates. */ import { InternalCompilerError } from "../../errors.js"; -import { classMembershipIntervals } from "./classes.js"; +import { classMembershipIntervals, type LlClassMeta } from "./classes.js"; import type { LlvmEmitterContext } from "./expr-context.js"; import { vAdapters } from "./shapes.js"; @@ -124,6 +124,7 @@ export function emitCaughtInstanceof( const B = host.B; const target = host.classMetaOf(className); const intervals = classMembershipIntervals(host.classMeta, target.def.name); + if (target.idInRc) return emitCaughtClassIdInstanceof(host, caught, target, intervals); if (!target.hierarchy) { const adapterSlot = B.tmp(); const adapter = B.tmp(); @@ -169,6 +170,80 @@ export function emitCaughtInstanceof( return result; } +/** `e instanceof C` for a class whose instances keep their class id in + * the rc word (classes.ts LlClassMeta.idInRc). They are thrown as REF + * payloads, like standalone class instances: the retain adapter names the + * hierarchy (the adapter of whichever static class threw), and the payload's + * class id is tested against the intervals. */ +function emitCaughtClassIdInstanceof( + host: LlvmEmitterContext, + caught: string, + target: LlClassMeta, + intervals: { pre: number; post: number }[], +): string { + const B = host.B; + const kp = B.tmp(), + kind = B.tmp(), + isRef = B.tmp(), + adapterSlot = B.tmp(), + adapter = B.tmp(); + B.line(`${kp} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 1`); + B.line(`${kind} = load i32, ptr ${kp}`); + B.line(`${isRef} = icmp eq i32 ${kind}, ${CAUGHT_REF_KIND}`); + B.line(`${adapterSlot} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 5`); + B.line(`${adapter} = load ptr, ptr ${adapterSlot}`); + let anyAdapter: string | undefined; + for (const m of host.classMeta.values()) { + if (m.root !== target.root) continue; + const rc = vAdapters(host.shapeHost, { kind: "object", className: m.def.name }); + const matches = B.tmp(); + B.line(`${matches} = icmp eq ptr ${adapter}, ${rc.retain}`); + if (anyAdapter === undefined) anyAdapter = matches; + else { + const joined = B.tmp(); + B.line(`${joined} = or i1 ${anyAdapter}, ${matches}`); + anyAdapter = joined; + } + } + const inHierarchy = B.tmp(); + B.line(`${inHierarchy} = and i1 ${isRef}, ${anyAdapter!}`); + const test = B.newLabel("caught.class"), + join = B.newLabel("caught.class.done"), + from = B.currentLabel(); + B.condBr(inHierarchy, test, join); + B.startBlock(test); + const payloadSlot = B.tmp(), + payload = B.tmp(); + B.line(`${payloadSlot} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 4`); + B.line(`${payload} = load ptr, ptr ${payloadSlot}`); + const pre = host.loadVtPre(payload, target.def.name); + let result: string | undefined; + for (const interval of intervals) { + const ge = B.tmp(), + le = B.tmp(), + t = B.tmp(); + B.line(`${ge} = icmp uge ${host.sizeType} ${pre}, ${interval.pre}`); + B.line(`${le} = icmp ule ${host.sizeType} ${pre}, ${interval.post}`); + B.line(`${t} = and i1 ${ge}, ${le}`); + if (result === undefined) result = t; + else { + const combined = B.tmp(); + B.line(`${combined} = or i1 ${result}, ${t}`); + result = combined; + } + } + if (result === undefined) throw new InternalCompilerError("empty class membership"); + const tested = B.currentLabel(); + B.br(join); + B.startBlock(join); + const out = B.tmp(); + B.line(`${out} = phi i1 [ false, %${from} ], [ ${result}, %${tested} ]`); + return out; +} + +/** SCR_EXC_REF (scr_exception.h). */ +const CAUGHT_REF_KIND = 4; + /** SCR_EXC_F64 = 1, SCR_EXC_BOOL = 2, SCR_EXC_STR = 3 (scr_exception.h). */ export const CAUGHT_SCALAR_KINDS = { f64: 1, bool: 2, string: 3 } as const; diff --git a/packages/compiler/src/backend/llvm/publish.ts b/packages/compiler/src/backend/llvm/publish.ts index 4f807424b..15221bbc3 100644 --- a/packages/compiler/src/backend/llvm/publish.ts +++ b/packages/compiler/src/backend/llvm/publish.ts @@ -63,6 +63,9 @@ export interface PublishClassInfo { post: number; /** A runtime-provided root class (errors, emitters, streams). */ runtimeRooted: boolean; + /** Instances keep their class id in the rc word (classes.ts + * LlClassMeta.idInRc). */ + idInRc: boolean; /** The class's own and inherited fields: GEP index and storage type. */ fields: { name: string; index: number; type: IrType }[]; } @@ -70,6 +73,12 @@ export interface PublishClassInfo { /** scr_pub_mark's object kinds (SCR_PUB_KIND_* in scr_runtime.h), for the RC audit. */ const PUB_KIND_OBJECT = 4; const PUB_KIND_UNION = 5; +/** SCR_PUB_CLASS_ID, or'ed into a kind: the object keeps its class id + * in the high half of its rc word (classes.ts LlClassMeta.idInRc). */ +const PUB_CLASS_ID = 0x100; +/** SCR_PUB_LOG_COUNT (scr_publish.c), in a log entry's `buf`: `rc` is the + * count of the low half. */ +const PUB_LOG_COUNT = "-2147483648"; const RUNTIME_WALKERS = { string: "@scr_pub_str_fn", @@ -307,9 +316,18 @@ export class PublishWalkers { const lines = [ head, `entry:`, - ` %vtp = getelementptr inbounds ptr, ptr %o, i64 1`, - ` %vt = load ptr, ptr %vtp`, - ` %pre = load ${this.host.sizeType}, ptr %vt`, + ...(info.idInRc + ? [ + ` %idp = getelementptr inbounds i8, ptr %o, i64 4 ; the class id (classes.ts classIdRcWord)`, + ` %id = load i32, ptr %idp`, + ` %idpre = add i32 %id, -1`, + ` %pre = zext i32 %idpre to ${this.host.sizeType}`, + ] + : [ + ` %vtp = getelementptr inbounds ptr, ptr %o, i64 1`, + ` %vt = load ptr, ptr %vtp`, + ` %pre = load ${this.host.sizeType}, ptr %vt`, + ]), ` switch ${this.host.sizeType} %pre, label %other [ ${cases.map((c) => `${this.host.sizeType} ${c.pre}, label %c${c.pre}`).join(" ")} ]`, ]; for (const c of cases) @@ -344,6 +362,7 @@ export class PublishWalkers { info.fields, path, loc, + info.idInRc, ); } @@ -355,20 +374,23 @@ export class PublishWalkers { * targets), and an object with a collector header leaves its candidate * buffer through scr_cyc_pub_take. Anything else calls scr_pub_mark, as * does every mark of an RC-audit build. */ - private markLines(cyc: boolean, kind: number): string[] { + private markLines(cyc: boolean, kind: number, classId = false): string[] { const call = (name: string): string[] => [ - ` %${name} = call zeroext i1 @scr_pub_mark(ptr %ctx, ptr %o, i1 ${cyc ? "true" : "false"}, i32 ${kind})`, + ` %${name} = call zeroext i1 @scr_pub_mark(ptr %ctx, ptr %o, i1 ${cyc ? "true" : "false"}, i32 ${classId ? kind | PUB_CLASS_ID : kind})`, ` br i1 %${name}, label %walk, label %done`, ]; if (this.host.rcAudit === true) return call("m"); const S = this.host.sizeType; + // An object that keeps its class id in the rc word is marked and + // logged through its count, the low half. + const C = classId ? "i32" : S; const field = (name: string, slot: number): string[] => [ ` %${name}p = getelementptr inbounds ptr, ptr %ctx, i32 ${slot}`, ` %${name} = load ${S === "i64" ? "i64" : "i32"}, ptr %${name}p`, ]; const lines = [ - ` %mrc = load ${S}, ptr %o`, - ` %mimm = icmp eq ${S} %mrc, -1`, + ` %mrc = load ${C}, ptr %o`, + ` %mimm = icmp eq ${C} %mrc, -1`, ` br i1 %mimm, label %done, label %mtest`, `mtest:`, ` %mrefp = getelementptr inbounds ptr, ptr %ctx, i32 6`, @@ -380,13 +402,13 @@ export class PublishWalkers { ` %mok1 = and i1 %mok0, %mroom`, ]; let ok = "%mok1"; - if (S === "i64") { + if (C === "i64") { // 0xfffffffe and up are the log's escapes (SCR_PUB_RC_LIFETIME, _BIG). lines.push(` %msmall = icmp ult i64 %mrc, 4294967294`, ` %mok2 = and i1 %mok1, %msmall`); ok = "%mok2"; } lines.push(` br i1 ${ok}, label %mfast, label %mslow`, `mslow:`, ...call("ms"), `mfast:`); - let buf = "0"; + let buf = classId ? String(PUB_LOG_COUNT) : "0"; if (cyc) { // ScrCycHdr.buf, the word before the object: its top bit while the // object is a buffered candidate. @@ -402,9 +424,10 @@ export class PublishWalkers { ...field("mtaken", 7), ` %mtaken1 = add ${S} %mtaken, 1`, ` store ${S} %mtaken1, ptr %mtakenp`, + ...(classId ? [` %mix2 = or i32 %mix1, ${PUB_LOG_COUNT}`] : []), ` br label %mput`, `mput:`, - ` %mbuf = phi i32 [ 0, %mfast ], [ %mix1, %mtake ]`, + ` %mbuf = phi i32 [ ${buf}, %mfast ], [ %mix${classId ? 2 : 1}, %mtake ]`, ); buf = "%mbuf"; } @@ -414,13 +437,13 @@ export class PublishWalkers { ` %me = getelementptr inbounds { ptr, i32, i32 }, ptr %mlog, ${S} %mlogged`, ` store ptr %o, ptr %me`, ` %merc = getelementptr inbounds { ptr, i32, i32 }, ptr %me, i32 0, i32 1`, - S === "i64" ? ` %mrc32 = trunc i64 %mrc to i32` : ` %mrc32 = add i32 %mrc, 0`, + C === "i64" ? ` %mrc32 = trunc i64 %mrc to i32` : ` %mrc32 = add i32 %mrc, 0`, ` store i32 %mrc32, ptr %merc`, ` %mebuf = getelementptr inbounds { ptr, i32, i32 }, ptr %me, i32 0, i32 2`, ` store i32 ${buf}, ptr %mebuf`, ` %mlogged1 = add ${S} %mlogged, 1`, ` store ${S} %mlogged1, ptr %mloggedp`, - ` store ${S} -1, ptr %o`, + ` store ${C} -1, ptr %o`, ` br label %walk`, ); return lines; @@ -431,13 +454,14 @@ export class PublishWalkers { * before) are the ones scr_pub_push skips: they are tested here, without * the call. The lines end in a fresh block labelled `pushed${id}`. */ private push(v: string, fn: string, id: string): string[] { - const size = this.host.sizeType; return [ ` %null${id} = icmp eq ptr ${v}, null`, ` br i1 %null${id}, label %pushed${id}, label %live${id}`, `live${id}:`, - ` %rc${id} = load ${size}, ptr ${v}`, - ` %immortal${id} = icmp eq ${size} %rc${id}, -1`, + // The count (the low half of a 64-bit rc word) is UINT32_MAX for an + // immortal object of every kind (scr_rc.h SCR_RC_COUNT). + ` %rc${id} = load i32, ptr ${v}`, + ` %immortal${id} = icmp eq i32 %rc${id}, -1`, ` br i1 %immortal${id}, label %pushed${id}, label %push${id}`, `push${id}:`, ...this.pushLines(v, fn, id), @@ -480,8 +504,9 @@ export class PublishWalkers { members: { name: string; index: number; type: IrType }[], path: string, loc: SrcLoc, + classId = false, ): string[] { - const lines = [head, `entry:`, ...this.markLines(traced, PUB_KIND_OBJECT), `walk:`]; + const lines = [head, `entry:`, ...this.markLines(traced, PUB_KIND_OBJECT, classId), `walk:`]; let n = 0; for (const member of members) { const fn = this.walker(member.type, `${path}.${member.name}`, loc); diff --git a/packages/compiler/src/backend/llvm/regions.test.ts b/packages/compiler/src/backend/llvm/regions.test.ts index 51ceb21c8..f2fed6b8e 100644 --- a/packages/compiler/src/backend/llvm/regions.test.ts +++ b/packages/compiler/src/backend/llvm/regions.test.ts @@ -117,7 +117,13 @@ test("ProgramLifetime instances live in the current region and count in their ch ); // The first reference is implied: allocation leaves the chunk count alone. expect(nw, cls).not.toContain("2097151"); - expect(nw, cls).toContain("store i64 -9223372036854775808, ptr %o"); + // The rc word: the class id in the high half (the hierarchy keeps it + // there instead of a vtable word) over the program-lifetime count, the + // midpoint of the 32-bit count in the low half. + const word = /store i64 (\d+), ptr %o\.vt/.exec(nw)?.[1]; + expect(word, cls).toBeDefined(); + expect(BigInt(word!) % 2n ** 32n, cls).toBe(2n ** 31n); + expect(BigInt(word!) / 2n ** 32n > 0n, cls).toBe(true); expect(nw, cls).not.toContain("scr_rt_calloc"); // retain/release: the chunk count, NULL-tolerant, once a region // exists, and only in chunks that have an owner (freeable regions). diff --git a/packages/compiler/src/backend/llvm/regions.ts b/packages/compiler/src/backend/llvm/regions.ts index 1ec46a5b1..e7ad39f7c 100644 --- a/packages/compiler/src/backend/llvm/regions.ts +++ b/packages/compiler/src/backend/llvm/regions.ts @@ -36,7 +36,7 @@ import { mangleRecordStruct, } from "../mangle.js"; import { threadHotAddressLines } from "./alloc.js"; -import { classFieldBase, type LlClassMeta } from "./classes.js"; +import { classFieldBase, classPreLines, type LlClassMeta } from "./classes.js"; import { llvmCommentText } from "./common.js"; import type { NullableRefFields } from "./nullable-fields.js"; import { FN_ATTRS, rcMembers, releaseSym, vAdapters, type ShapeHost } from "./shapes.js"; @@ -389,9 +389,7 @@ export class LifetimeRegions { out.push( `define internal void ${walk}(ptr %o, ptr %w) ${FN_ATTRS} { ; region walk ${llvmCommentText(cls.name)} and its subclasses`, `entry:`, - ` %vtp = getelementptr inbounds ptr, ptr %o, i64 1`, - ` %vt = load ptr, ptr %vtp`, - ` %pre = load ${S}, ptr %vt`, + ...classPreLines(meta, mangleClassStruct(cls.name), "%o", "%pre", S), ` switch ${S} %pre, label %done [ ${members.map((m) => `${S} ${m.pre}, label %c${m.pre}`).join(" ")} ]`, ...members.flatMap((m) => [ `c${m.pre}: ; ${llvmCommentText(m.def.name)}`, diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 9cceaa443..40803a5ff 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -1249,6 +1249,11 @@ export function rcMembers( ]; } +/** The integer type a release or retain loads and stores the count as: + * the rc word, or its low half (an i32 at offset 0) for class instances that + * keep their class number in the high half (classes.ts LlClassMeta.idInRc). */ +export type RcCount = "i32" | "i64"; + /** The NULL- and immortal-skipping retain body shared by every shape. A * retain never touches the cycle header: a buffered candidate that is * retained again stays a candidate (scr_cycle.c, markRoots). */ @@ -1257,8 +1262,9 @@ export function retainBody( fnName: string, comment = "", immortal = false, + count: RcCount = host.sizeType, ): string[] { - const S = host.sizeType; + const S = count; if (immortal) return immortalRcBody(host, fnName, "ptr", comment); return [ `define internal ptr @${fnName}(ptr %o) ${FN_ATTRS} {${comment ? ` ; ${comment}` : ""}`, @@ -1325,6 +1331,8 @@ export interface DirectDestroy { readonly destroy: string; /** The child carries a collector header. */ readonly traced: boolean; + /** The child's count type, when it is not the rc word (RcCount). */ + readonly count?: RcCount; } /** The release of child `value` (a loaded field, possibly NULL) inside a @@ -1339,19 +1347,20 @@ export function directChildReleaseLines( comment: string, ): string[] { const S = host.sizeType; + const C = target.count ?? S; host.declare(`declare void @scr_rc_destroy(ptr, ptr)`); const lines = [ ` %${tag}.nn = icmp ne ptr ${value}, null`, ` br i1 %${tag}.nn, label %${tag}.chk, label %${tag}.next`, `${tag}.chk:`, - ` %${tag}.rc = load ${S}, ptr ${value}`, - ` %${tag}.last = icmp eq ${S} %${tag}.rc, 1`, + ` %${tag}.rc = load ${C}, ptr ${value}`, + ` %${tag}.last = icmp eq ${C} %${tag}.rc, 1`, ` br i1 %${tag}.last, label %${tag}.dead, label %${tag}.shared`, `${tag}.shared:`, ` call void ${target.release}(ptr ${value}) ; ${comment}`, ` br label %${tag}.next`, `${tag}.dead: ; ${comment}: the last reference`, - ` store ${S} 0, ptr ${value}`, + ` store ${C} 0, ptr ${value}`, ]; if (target.traced) { host.declare(`declare void @scr_cyc_on_dead(ptr)`); @@ -1514,8 +1523,9 @@ export function releaseFastPath( traced: boolean, comment = "", immortal = false, + count: RcCount = host.sizeType, ): string[] { - const S = host.sizeType; + const S = count; if (immortal) return immortalRcBody(host, fnName, "void", comment); return [ `define internal void @${fnName}(ptr %o) ${FN_ATTRS} {${comment ? ` ; ${comment} (fast path)` : ""}`, diff --git a/packages/compiler/src/backend/llvm/string-reads.ts b/packages/compiler/src/backend/llvm/string-reads.ts index ef0702361..d7bbed272 100644 --- a/packages/compiler/src/backend/llvm/string-reads.ts +++ b/packages/compiler/src/backend/llvm/string-reads.ts @@ -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 diff --git a/packages/compiler/src/backend/llvm/thread-slots.test.ts b/packages/compiler/src/backend/llvm/thread-slots.test.ts index dba4453b3..b8d8dccf9 100644 --- a/packages/compiler/src/backend/llvm/thread-slots.test.ts +++ b/packages/compiler/src/backend/llvm/thread-slots.test.ts @@ -85,12 +85,11 @@ test("slots.get reads the slot word, the page and the entry inline", () => { ]; expect(validateModule(module(calls))).toEqual([]); const llvm = emitLlvmModule(module(calls), { pointerBits: 64 }); - // The slot word sits after the header words of every hierarchy member. + // The slot word sits after the rc word of every hierarchy member, which + // keeps its class id in the rc word instead of a vtable word. + expect(llvm).toMatch(new RegExp(`%${mangleClassStruct("Node")} = type \\{ i64, i32, ptr \\}`)); expect(llvm).toMatch( - new RegExp(`%${mangleClassStruct("Node")} = type \\{ i64, ptr, i32, ptr \\}`), - ); - expect(llvm).toMatch( - new RegExp(`%${mangleClassStruct("Leaf")} = type \\{ i64, ptr, i32, ptr, (i32|double) \\}`), + new RegExp(`%${mangleClassStruct("Leaf")} = type \\{ i64, i32, ptr, (i32|double) \\}`), ); expect(llvm).toMatch(new RegExp(`%${mangleClassStruct("Plain")} = type \\{ i64, ptr \\}`)); // One block per family name. @@ -102,7 +101,7 @@ test("slots.get reads the slot word, the page and the entry inline", () => { ); const main = body(llvm, "sc_f_main"); expect(main).toMatch( - /getelementptr inbounds %\S+, ptr %\S+, i64 0, i32 2\n {2}(%\S+) = load atomic i32, ptr %\S+ monotonic, align 4\n {2}%\S+ = lshr i32 \1, 10/, + /getelementptr inbounds %\S+, ptr %\S+, i64 0, i32 1\n {2}(%\S+) = load atomic i32, ptr %\S+ monotonic, align 4\n {2}%\S+ = lshr i32 \1, 10/, ); expect(main).not.toMatch(/call ptr @scr_slots_get/); expect(main).toMatch( diff --git a/packages/compiler/src/backend/mangle.ts b/packages/compiler/src/backend/mangle.ts index 43c9cea71..da6cbe0f5 100644 --- a/packages/compiler/src/backend/mangle.ts +++ b/packages/compiler/src/backend/mangle.ts @@ -109,6 +109,13 @@ export function mangleVtStruct(rootClassName: string): string { export function mangleVtInstance(className: string): string { return `sc_vtable_${sanitize(className)}`; } +/** The per-HIERARCHY array of the vtables of its classes in preorder, for + * hierarchies whose instances keep their class id in the rc word + * (classes.ts LlClassMeta.idInRc). `sc_vta_` differs from `sc_vtable_` and + * `sc_vtt_` before any user-controlled text. */ +export function mangleVtArray(rootClassName: string): string { + return `sc_vta_${sanitize(rootClassName)}`; +} /** A virtual slot's member name inside the vtable struct. Sibling * branches of one hierarchy can each declare a slot for the SAME method * name (root-most in their own subtree, overridden below — mixin layers diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 031f99ddd..5aa5307a0 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -43,6 +43,15 @@ 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; + /** Fixed-width Buffer methods called on checked-dynamic values link + * scr_bytes_dyn.c; absent (false) for programs without them. */ + dynBufferNum?: boolean; events: boolean; emitter: boolean; symbol: boolean; @@ -50,6 +59,7 @@ export interface NativeLinkFeatures { searchParams: boolean; qs: boolean; dynBuiltinClass: boolean; + dynBuiltinStream: boolean; parseArgs: boolean; stream: boolean; net: boolean; diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 8abdc6c38..47cb91d96 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -113,6 +113,15 @@ 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; + /** Fixed-width Buffer methods called on checked-dynamic values: + * compiles scr_bytes_dyn.c. */ + dynBufferNum?: 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 +166,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; diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 33c126de8..e135b9ee2 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -179,6 +179,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.assert || u.regex || u.opts.symbol, "scr_assert.c"), unit((u) => u.opts.inspect, "scr_inspect.c", "scr_console_native.c"), unit((u) => u.opts.dynInvoke || u.opts.workers || u.nativeFetch, "scr_dyn_invoke.c"), + // Fixed-width Buffer methods called on checked-dynamic values. + unit((u) => u.opts.dynBufferNum === true && u.opts.dynInvoke, "scr_bytes_dyn.c"), unit((u) => u.opts.dc, "scr_dc.c"), unit((u) => u.opts.dynTier, "scr_js_value.c"), unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"), @@ -220,6 +222,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 +252,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, diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts index be94b33b9..75cfaa577 100644 --- a/packages/compiler/src/backend/runtime-pack-core.ts +++ b/packages/compiler/src/backend/runtime-pack-core.ts @@ -84,6 +84,9 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures { eventTarget: boolean; wasm: boolean; dynTier: boolean; + streamDyn: boolean; + eventsOnceSignal: boolean; + dynBufferNum: boolean; warningReplace: boolean; stdioExtended: boolean; nativeFetch: boolean; @@ -275,6 +278,9 @@ export function effectiveRuntimeFeatures( eventTarget: features.eventTarget === true, wasm: features.wasm === true, dynTier: features.dynTier === true, + streamDyn: features.streamDyn === true, + eventsOnceSignal: features.eventsOnceSignal === true, + dynBufferNum: features.dynBufferNum === true, warningReplace: features.warningReplace === true, stdioExtended: features.stdioExtended === true, nativeFetch, diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index a39ce78e2..96503e2db 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -108,6 +108,7 @@ const features = { searchParams: false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 1681626b7..cd342f85b 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -45,6 +45,7 @@ const BASE: NativeLinkFeatures = { searchParams: false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/backend/stack-observations.test.ts b/packages/compiler/src/backend/stack-observations.test.ts index 0e1178ce9..d381c5307 100644 --- a/packages/compiler/src/backend/stack-observations.test.ts +++ b/packages/compiler/src/backend/stack-observations.test.ts @@ -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); }); diff --git a/packages/compiler/src/backend/stack-observations.ts b/packages/compiler/src/backend/stack-observations.ts index cc6d109e4..fa67c071f 100644 --- a/packages/compiler/src/backend/stack-observations.ts +++ b/packages/compiler/src/backend/stack-observations.ts @@ -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 = 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 = 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(); + private readonly named: Set; + private readonly inits = new Map>(); + /** 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(); + + 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(); + 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): 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): 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 `%.` convention of class + * methods, accessors and constructors for one of `classNames`. */ +function isClassMember(name: string, classNames: ReadonlySet): 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(); 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); diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts index 9a505d517..389fa19e9 100644 --- a/packages/compiler/src/backend/warm-cache.ts +++ b/packages/compiler/src/backend/warm-cache.ts @@ -51,6 +51,7 @@ export async function warmNativeCaches( searchParams: false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/cli/usage.ts b/packages/compiler/src/cli/usage.ts index 26b439167..69d30b76e 100644 --- a/packages/compiler/src/cli/usage.ts +++ b/packages/compiler/src/cli/usage.ts @@ -73,8 +73,9 @@ Options: --tier tier policy (default: auto). static-only keeps every construct without a static lowering a compile error, so - the program has no dynamic-tier demotions. Both modes - currently compile identically + the program has no dynamic-tier demotions, and makes a + checked cast whose value does not fit its type throw + instead of continuing with the value --explain-dynamic[=] coverage only: list the functions that operate on dynamic values and the bindings, fields, functions, classes and diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index 5b0982dc1..33da0610f 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -46,6 +46,7 @@ const native: EarlyExecutableNativeFeatures = { searchParams: false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index fd4a34ec2..45655c06f 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -66,6 +66,9 @@ export interface EarlyExecutableNativeFeatures { dc: boolean; dynAsync: boolean; dynTier?: boolean; + streamDyn?: boolean; + eventsOnceSignal?: boolean; + dynBufferNum?: boolean; events: boolean; emitter: boolean; symbol: boolean; @@ -73,6 +76,7 @@ export interface EarlyExecutableNativeFeatures { searchParams: boolean; qs: boolean; dynBuiltinClass: boolean; + dynBuiltinStream: boolean; parseArgs: boolean; stream: boolean; net: boolean; @@ -193,6 +197,7 @@ const BOOLEAN_NATIVE_KEYS = [ "searchParams", "qs", "dynBuiltinClass", + "dynBuiltinStream", "parseArgs", "stream", "net", diff --git a/packages/compiler/src/frontend/cjs-syntax.ts b/packages/compiler/src/frontend/cjs-syntax.ts index 41191432a..99195d994 100644 --- a/packages/compiler/src/frontend/cjs-syntax.ts +++ b/packages/compiler/src/frontend/cjs-syntax.ts @@ -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)) { diff --git a/packages/compiler/src/frontend/contract-types.ts b/packages/compiler/src/frontend/contract-types.ts index c30788fa1..4ded2a1a0 100644 --- a/packages/compiler/src/frontend/contract-types.ts +++ b/packages/compiler/src/frontend/contract-types.ts @@ -130,7 +130,7 @@ function markerNamed( function nameText(node: ts.Node | undefined): string { if (node === undefined) return ""; if (ts.isIdentifier(node) || ts.isPrivateIdentifier(node)) return node.text; - return node.getText(); + return ts.getTextOfNode(node); } function contractDiag( diff --git a/packages/compiler/src/frontend/experimental-import.ts b/packages/compiler/src/frontend/experimental-import.ts index 8fc2238bf..3b35a196a 100644 --- a/packages/compiler/src/frontend/experimental-import.ts +++ b/packages/compiler/src/frontend/experimental-import.ts @@ -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; diff --git a/packages/compiler/src/frontend/fork-target.test.ts b/packages/compiler/src/frontend/fork-target.test.ts index 7a3e23c9b..5e56cea5d 100644 --- a/packages/compiler/src/frontend/fork-target.test.ts +++ b/packages/compiler/src/frontend/fork-target.test.ts @@ -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", diff --git a/packages/compiler/src/frontend/fork-target.ts b/packages/compiler/src/frontend/fork-target.ts index 592bbb2d6..38c8f5f07 100644 --- a/packages/compiler/src/frontend/fork-target.ts +++ b/packages/compiler/src/frontend/fork-target.ts @@ -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) { diff --git a/packages/compiler/src/frontend/inferred-js-diagnostics.ts b/packages/compiler/src/frontend/inferred-js-diagnostics.ts index 7095cea5d..b21d19cb0 100644 --- a/packages/compiler/src/frontend/inferred-js-diagnostics.ts +++ b/packages/compiler/src/frontend/inferred-js-diagnostics.ts @@ -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]!) ) { diff --git a/packages/compiler/src/frontend/lib-contract.ts b/packages/compiler/src/frontend/lib-contract.ts index 0432d06e2..28532ecd8 100644 --- a/packages/compiler/src/frontend/lib-contract.ts +++ b/packages/compiler/src/frontend/lib-contract.ts @@ -115,7 +115,7 @@ const CONTRACT_GLOBAL_TYPES = new Set(["Array", "ReadonlyArray", "Uint8Array"]); type TypeBindings = Map>; 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 diff --git a/packages/compiler/src/frontend/lib-exports.ts b/packages/compiler/src/frontend/lib-exports.ts index 738dd2f19..6a42074bd 100644 --- a/packages/compiler/src/frontend/lib-exports.ts +++ b/packages/compiler/src/frontend/lib-exports.ts @@ -35,7 +35,7 @@ export function entryFunctionExports(entry: ts.SourceFile): Map 0, async: mods?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true, generator: stmt.asteriskToken !== undefined, diff --git a/packages/compiler/src/frontend/lowering/assert-instances.ts b/packages/compiler/src/frontend/lowering/assert-instances.ts index ee8a34ea2..1166c6afa 100644 --- a/packages/compiler/src/frontend/lowering/assert-instances.ts +++ b/packages/compiler/src/frontend/lowering/assert-instances.ts @@ -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; } diff --git a/packages/compiler/src/frontend/lowering/assertion-members.ts b/packages/compiler/src/frontend/lowering/assertion-members.ts index d61c379d6..484963fa8 100644 --- a/packages/compiler/src/frontend/lowering/assertion-members.ts +++ b/packages/compiler/src/frontend/lowering/assertion-members.ts @@ -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)), diff --git a/packages/compiler/src/frontend/lowering/binding-analysis.ts b/packages/compiler/src/frontend/lowering/binding-analysis.ts index 88b0f91dc..83b9aa6f9 100644 --- a/packages/compiler/src/frontend/lowering/binding-analysis.ts +++ b/packages/compiler/src/frontend/lowering/binding-analysis.ts @@ -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; diff --git a/packages/compiler/src/frontend/lowering/builtin-function-fences.ts b/packages/compiler/src/frontend/lowering/builtin-function-fences.ts index e6221384b..312596c1f 100644 --- a/packages/compiler/src/frontend/lowering/builtin-function-fences.ts +++ b/packages/compiler/src/frontend/lowering/builtin-function-fences.ts @@ -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: [], diff --git a/packages/compiler/src/frontend/lowering/builtins/async-context.ts b/packages/compiler/src/frontend/lowering/builtins/async-context.ts index e6de99b09..18bd93a86 100644 --- a/packages/compiler/src/frontend/lowering/builtins/async-context.ts +++ b/packages/compiler/src/frontend/lowering/builtins/async-context.ts @@ -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", diff --git a/packages/compiler/src/frontend/lowering/builtins/blob.ts b/packages/compiler/src/frontend/lowering/builtins/blob.ts index 3fc4ec173..94c8c7ffe 100644 --- a/packages/compiler/src/frontend/lowering/builtins/blob.ts +++ b/packages/compiler/src/frontend/lowering/builtins/blob.ts @@ -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) { diff --git a/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts b/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts index ac73924ad..eaa08c9dd 100644 --- a/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts +++ b/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts @@ -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, diff --git a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts index afb221d04..661fec528 100644 --- a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts +++ b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts @@ -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, diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts index e1a25d169..af7270ea9 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts @@ -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) => { diff --git a/packages/compiler/src/frontend/lowering/builtins/class-values.ts b/packages/compiler/src/frontend/lowering/builtins/class-values.ts index 416217716..4d073c264 100644 --- a/packages/compiler/src/frontend/lowering/builtins/class-values.ts +++ b/packages/compiler/src/frontend/lowering/builtins/class-values.ts @@ -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; diff --git a/packages/compiler/src/frontend/lowering/builtins/compression.ts b/packages/compiler/src/frontend/lowering/builtins/compression.ts index e69011522..717e09933 100644 --- a/packages/compiler/src/frontend/lowering/builtins/compression.ts +++ b/packages/compiler/src/frontend/lowering/builtins/compression.ts @@ -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)) { diff --git a/packages/compiler/src/frontend/lowering/builtins/constants.ts b/packages/compiler/src/frontend/lowering/builtins/constants.ts index 611805d94..35f0fc095 100644 --- a/packages/compiler/src/frontend/lowering/builtins/constants.ts +++ b/packages/compiler/src/frontend/lowering/builtins/constants.ts @@ -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 }; } diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index cd490bc87..e6834abd4 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -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, }; diff --git a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts index fb1c92257..1b9a9c03a 100644 --- a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts +++ b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts @@ -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; } diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts index 439c9fca3..d95442829 100644 --- a/packages/compiler/src/frontend/lowering/builtins/errors.ts +++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts @@ -27,9 +27,10 @@ const ASSERTION_ERROR_PROPERTIES: ReadonlySet = 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)), ) ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts index 3fbea04aa..49875e004 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts @@ -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); diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts index 22e524690..498f9b6d4 100644 --- a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts +++ b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index b48030ba5..54bcae4b5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -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))) ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/key-object.ts b/packages/compiler/src/frontend/lowering/builtins/key-object.ts index ff6c0b38b..7e680fa20 100644 --- a/packages/compiler/src/frontend/lowering/builtins/key-object.ts +++ b/packages/compiler/src/frontend/lowering/builtins/key-object.ts @@ -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), diff --git a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts index 8a5365d18..5465aeafa 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts @@ -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; } diff --git a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts index 895c1a9d6..c85f2c777 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts @@ -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); } diff --git a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts index afa9dc1ea..6b2e9e589 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts @@ -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; } diff --git a/packages/compiler/src/frontend/lowering/builtins/navigator.ts b/packages/compiler/src/frontend/lowering/builtins/navigator.ts index 637edaf3c..f90448c1e 100644 --- a/packages/compiler/src/frontend/lowering/builtins/navigator.ts +++ b/packages/compiler/src/frontend/lowering/builtins/navigator.ts @@ -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 ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/net-address.ts b/packages/compiler/src/frontend/lowering/builtins/net-address.ts index d99d63fd3..9e795bca6 100644 --- a/packages/compiler/src/frontend/lowering/builtins/net-address.ts +++ b/packages/compiler/src/frontend/lowering/builtins/net-address.ts @@ -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, diff --git a/packages/compiler/src/frontend/lowering/builtins/performance.ts b/packages/compiler/src/frontend/lowering/builtins/performance.ts index 7aac3b80c..72304e6a9 100644 --- a/packages/compiler/src/frontend/lowering/builtins/performance.ts +++ b/packages/compiler/src/frontend/lowering/builtins/performance.ts @@ -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. */ diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 9ac1fc059..2db62d675 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -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,59 @@ 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 = 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(); + +/** 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 +329,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 +637,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 +720,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 +750,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 +762,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 +939,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 +1136,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 +1151,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, diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 55290f720..668ef01f8 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -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 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 entry (the deadline idiom: `new Promise((_, + // 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 entry (the deadline idiom: `new Promise((_, - // 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 diff --git a/packages/compiler/src/frontend/lowering/builtins/properties.ts b/packages/compiler/src/frontend/lowering/builtins/properties.ts index e60a14c0c..70512fed9 100644 --- a/packages/compiler/src/frontend/lowering/builtins/properties.ts +++ b/packages/compiler/src/frontend/lowering/builtins/properties.ts @@ -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) diff --git a/packages/compiler/src/frontend/lowering/builtins/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts index bcf77a39b..ede65d788 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -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"; }); diff --git a/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts b/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts index fc507e2bd..230441d31 100644 --- a/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts +++ b/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts @@ -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)), ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/runtime-constructor.ts b/packages/compiler/src/frontend/lowering/builtins/runtime-constructor.ts index 4b61f98c1..7db3f8dcc 100644 --- a/packages/compiler/src/frontend/lowering/builtins/runtime-constructor.ts +++ b/packages/compiler/src/frontend/lowering/builtins/runtime-constructor.ts @@ -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); diff --git a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts index e89b1f07e..7229d5b84 100644 --- a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts +++ b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts index 8c6606782..2d531663f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/timers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts @@ -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 { diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts index 3de973335..dca8a7cc0 100644 --- a/packages/compiler/src/frontend/lowering/builtins/url.ts +++ b/packages/compiler/src/frontend/lowering/builtins/url.ts @@ -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 }; diff --git a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts index 2a9ff9eef..93cba84c5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts @@ -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) ); } diff --git a/packages/compiler/src/frontend/lowering/builtins/workers.ts b/packages/compiler/src/frontend/lowering/builtins/workers.ts index 335fdb969..1426041a2 100644 --- a/packages/compiler/src/frontend/lowering/builtins/workers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/workers.ts @@ -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)) diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index db7691a1c..49f7c8ec0 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -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)); } diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 954122a16..47cdd834f 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -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 || diff --git a/packages/compiler/src/frontend/lowering/checked-fallback.ts b/packages/compiler/src/frontend/lowering/checked-fallback.ts new file mode 100644 index 000000000..b1d746c1d --- /dev/null +++ b/packages/compiler/src/frontend/lowering/checked-fallback.ts @@ -0,0 +1,861 @@ +/* Checked-cast fallback: a value that fails a checked cast keeps running. + * + * `JSON.parse(text) as T` (and `const x: T = anyValue`, `await res.json() + * as T`) is a checked conversion: the static code after it reads `T`'s + * native layout, so a value that does not fit `T` used to throw a + * TypeError at the cast. Node never checks: the program continues with the + * value it has. This module makes the failing case continue too, without + * changing the code that runs when the value fits. + * + * The unit is a FALLBACK SITE: a function-local binding whose only write + * is the checked cast (the ROOT), declared with `const`/`let` in a function + * body. The code that can observe the root is the statements after the + * cast in each enclosing statement list up to the root's declaration (the + * TAILS; every other reference disqualifies the site). Each tail is lowered + * twice: + * + * - the static lowering, exactly as before, used while the cast held; + * - the dynamic lowering, used after a failure: the root reads its checked + * dynamic value (a shadow local), and every expression derived from it is + * typed `any` (fallbackTainted), so the region lowers through scriptc's + * ordinary `any` path, which has Node's semantics: missing members read + * undefined, values keep their real types, extra members stay visible, + * and objects keep their identity. + * + * `if (flag) dynamic else static` selects the lowering after each tail's + * first statement; the cast sets the flag. A derived value that flows into + * a typed slot outside the region (a typed parameter, a field, an outer + * binding) passes the same checked conversion as any `any` value does + * there: it continues when it fits and throws that conversion's TypeError + * at that use when it does not. A statement of the dynamic lowering that + * has no `any` lowering compiles to a runtime fence that throws at that + * statement only when a failed value reaches it. + * + * Code without a fallback site is unchanged; code with one runs its static + * tail unchanged behind one flag test per tail, and the cast reads one + * thread-local word after the check (checkedCast.fallbackTaken). + * `--tier=static-only` (and library mode) opens no sites: casts throw. */ +import * as ts from "../ts7/adapter.js"; +import { + BOOL, + DYN, + type IrExpr, + type IrLocal, + type IrStmt, + type IrType, + type SrcLoc, +} from "../../ir/ir.js"; +import { isJsSourceFile, locOf } from "../program.js"; +import { PoisonError, type Lowerer } from "./lowerer.js"; + +export interface FallbackSite { + root: ts.Symbol; + /** The root's declared name (a module-level root is a module binding). */ + name: ts.Identifier; + /** The statement holding the checked cast (the declaration itself, or + * the `root = value as T` expression statement). */ + boundaryStmt: ts.Statement; + /** Statements after which a tail starts, innermost first; removed as + * their lists split. */ + anchors: Set; + flag: IrLocal; + shadow: IrLocal; + armed: boolean; + /** An initializer-less root's flag and shadow declarations, and the + * statement list they were emitted into. */ + decls: { stmts: IrStmt[]; out: IrStmt[] } | null; +} + +export interface FallbackTaint { + symbols: Set; + memo: Map; +} + +/** Tails larger than this keep the throwing cast: every node is lowered + * twice. */ +const MAX_TAIL_NODES = 20000; + +/** A site arms only while fewer than this many splits enclose its tail + * or share its split point: each one lowers the tail again, so a tail is + * lowered at most 2^n times. */ +const MAX_NESTED_SPLITS = 2; + +type StatementList = { statements: { nodes: readonly ts.Statement[] } }; + +function isStatementList(node: ts.Node | undefined): boolean { + return ( + node !== undefined && (ts.isBlock(node) || ts.isModuleBlock(node) || ts.isSourceFile(node)) + ); +} + +function listOf(node: ts.Node): readonly ts.Statement[] { + return (node as unknown as StatementList).statements.nodes; +} + +function strip(e: ts.Expression): ts.Expression { + let v = e; + while (ts.isParenthesizedExpression(v)) v = v.expression; + return v; +} + +function enclosingFunction(node: ts.Node): ts.Node | undefined { + let n: ts.Node | undefined = node.parent; + while (n !== undefined && !ts.isFunctionLike(n) && !ts.isSourceFile(n)) n = n.parent; + return n; +} + +/** `value as T` / `value` over an `any` or `unknown` value, or a + * dynamic value flowing into an annotated declaration. */ +function isCheckedCast(lowerer: Lowerer, e: ts.Expression, declared: ts.Type | null): boolean { + const dynamicFlags = ts.TypeFlags.Any | ts.TypeFlags.Unknown; + const dynamic = (x: ts.Expression): boolean => + (lowerer.checker.getTypeAtLocation(x).flags & dynamicFlags) !== 0; + const v = strip(e); + if (ts.isAsExpression(v) || ts.isTypeAssertion(v)) { + const target = lowerer.checker.getTypeFromTypeNode(v.type); + if (target.flags & dynamicFlags) return false; + return dynamic(strip(v.expression)); + } + return declared !== null && (declared.flags & dynamicFlags) === 0 && dynamic(v); +} + +function writesSymbol(lowerer: Lowerer, n: ts.Node, sym: ts.Symbol): ts.Node | null { + const names = (e: ts.Node): boolean => + ts.isIdentifier(e) && lowerer.resolveValueSymbol(e) === sym; + const targetNames = (t: ts.Expression): boolean => { + const e = strip(t); + if (names(e)) return true; + if (ts.isArrayLiteralExpression(e) || ts.isObjectLiteralExpression(e)) { + let found = false; + const walk = (x: ts.Node): void => { + if (names(x)) found = true; + else ts.forEachChild(x, walk); + }; + walk(e); + return found; + } + return false; + }; + if (ts.isBinaryExpression(n)) { + const k = n.operatorToken.kind; + if ( + k >= ts.SyntaxKind.FirstAssignment && + k <= ts.SyntaxKind.LastAssignment && + targetNames(n.left) + ) + return n; + } else if ( + (ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) && + (n.operator === ts.SyntaxKind.PlusPlusToken || n.operator === ts.SyntaxKind.MinusMinusToken) && + targetNames(n.operand as ts.Expression) + ) { + return n; + } else if ( + (ts.isForOfStatement(n) || ts.isForInStatement(n)) && + !ts.isVariableDeclarationList(n.initializer) && + targetNames(n.initializer as ts.Expression) + ) { + return n; + } + return null; +} + +/** Hoisted or registered-once declarations, and `super()` calls (which + * initialize the instance once), that a second lowering would duplicate. */ +function tailIsRelowerable(stmts: readonly ts.Statement[]): boolean { + let ok = true; + let count = 0; + const visit = (n: ts.Node): void => { + if (!ok) return; + if (++count > MAX_TAIL_NODES) { + ok = false; + return; + } + if ( + ts.isFunctionDeclaration(n) || + ts.isClassDeclaration(n) || + ts.isClassExpression(n) || + ts.isModuleDeclaration(n) || + ts.isEnumDeclaration(n) || + (ts.isCallExpression(n) && n.expression.kind === ts.SyntaxKind.SuperKeyword) || + (ts.isVariableDeclarationList(n) && + (n.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const | ts.NodeFlags.Using)) === 0) + ) { + ok = false; + return; + } + ts.forEachChild(n, visit); + }; + for (const s of stmts) visit(s); + return ok; +} + +interface Candidate { + root: ts.Symbol; + name: ts.Identifier; + boundaryStmt: ts.Statement; + anchors: ts.Statement[]; +} + +/** The fallback site a declaration statement starts, or null. */ +function fallbackCandidate(lowerer: Lowerer, stmt: ts.VariableStatement): Candidate | null { + const list = stmt.declarationList; + if ((list.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) === 0) return null; + if ((list.flags & ts.NodeFlags.Using) !== 0) return null; + if (list.declarations.nodes.length !== 1) return null; + if (ts.canHaveModifiers(stmt) && (ts.getModifiers(stmt)?.length ?? 0) > 0) return null; + const decl = list.declarations.nodes[0]!; + if (!ts.isIdentifier(decl.name)) return null; + if (isJsSourceFile(ts.getSourceFileOfNode(stmt))) return null; + // Cheap gates before any scan: a checked-cast initializer, or a typed + // declaration without one. + const declared = decl.type !== undefined ? lowerer.checker.getTypeFromTypeNode(decl.type) : null; + if ( + decl.initializer !== undefined + ? !isCheckedCast(lowerer, decl.initializer, declared) + : declared === null + ) + return null; + const rootList = stmt.parent; + // Module-level bindings are module state (and module statements lower + // one at a time): roots live in blocks. + if (rootList === undefined || !ts.isBlock(rootList)) return null; + const fn = enclosingFunction(stmt); + if (fn === undefined) return null; + const root = lowerer.checker.getSymbolAtLocation(decl.name); + if (root === undefined) return null; + // A root in a module-level block is scanned across its file. + const body = ts.isSourceFile(fn) ? fn : (fn as ts.FunctionLikeDeclaration).body; + if (body === undefined) return null; + + // The only write: the initializer, or one `root = value as T` statement. + let boundaryStmt: ts.Statement | null = null; + let boundaryWrite: ts.Node | null = null; + const writes: ts.Node[] = []; + const findWrites = (n: ts.Node): void => { + const w = writesSymbol(lowerer, n, root); + if (w) writes.push(w); + ts.forEachChild(n, findWrites); + }; + findWrites(body); + if (decl.initializer !== undefined) { + if (writes.length > 0) return null; + boundaryStmt = stmt; + } else { + if (writes.length !== 1) return null; + const w = writes[0]!; + if ( + !ts.isBinaryExpression(w) || + w.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + !ts.isIdentifier(strip(w.left)) || + !ts.isExpressionStatement(w.parent) || + !isCheckedCast(lowerer, w.right, declared) + ) + return null; + if (enclosingFunction(w) !== fn) return null; + boundaryStmt = w.parent; + boundaryWrite = w; + } + + // The split points: the statement holding the cast in each enclosing + // statement list, up to the root's list. + const anchors: ts.Statement[] = []; + let cur: ts.Node = boundaryStmt; + for (;;) { + const parent: ts.Node | undefined = cur.parent; + if (parent === undefined || ts.isFunctionLike(parent)) return null; + if (ts.isCaseClause(parent) || ts.isDefaultClause(parent)) return null; + if (isStatementList(parent)) { + anchors.push(cur as ts.Statement); + if (parent === rootList) break; + if (ts.isSourceFile(parent)) return null; + } + cur = parent; + } + const anchorIndex = new Map(); + for (const a of anchors) + anchorIndex.set(a, { list: a.parent!, index: listOf(a.parent!).indexOf(a) }); + const tails: ts.Statement[] = []; + for (const a of anchors) { + const { list, index } = anchorIndex.get(a)!; + tails.push(...listOf(list).slice(index + 1)); + } + if (!tailIsRelowerable(tails)) return null; + + // Every other reference sits in a tail. + const inTail = (ref: ts.Node): boolean => { + let n: ts.Node = ref; + while (n.parent !== undefined && n !== body) { + const parent = n.parent; + if (isStatementList(parent)) { + for (const a of anchors) { + if (a.parent !== parent) continue; + const at = anchorIndex.get(a)!.index; + const mine = listOf(parent).indexOf(n as ts.Statement); + if (mine > at) return true; + if (mine < at) return false; + } + } + n = parent; + } + return false; + }; + let referencesOk = true; + const scanRefs = (n: ts.Node): void => { + if (!referencesOk) return; + if (ts.isIdentifier(n) && n !== decl.name && lowerer.resolveValueSymbol(n) === root) { + const isBoundaryTarget = + boundaryWrite !== null && + ts.isBinaryExpression(boundaryWrite) && + strip(boundaryWrite.left) === n; + if (!isBoundaryTarget && !inTail(n)) referencesOk = false; + return; + } + ts.forEachChild(n, scanRefs); + }; + scanRefs(body); + if (!referencesOk) return null; + return { root, name: decl.name, boundaryStmt, anchors }; +} + +function declareSiteLocals( + lowerer: Lowerer, + candidate: Candidate, + loc: SrcLoc, +): { site: FallbackSite; decls: IrStmt[] } { + const name = ts.symbolName(candidate.root); + const flag = lowerer.declareHiddenLocal(`%fb.${name}`, BOOL); + flag.mutable = true; + const shadow = lowerer.declareHiddenLocal(`%fbv.${name}`, DYN); + shadow.mutable = true; + const decls: IrStmt[] = [ + { + kind: "varDecl", + localId: flag.id, + init: { kind: "boolLit", value: false, type: BOOL, loc }, + loc, + }, + { kind: "varDecl", localId: shadow.id, init: null, loc }, + ]; + const site: FallbackSite = { + root: candidate.root, + name: candidate.name, + boundaryStmt: candidate.boundaryStmt, + anchors: new Set(candidate.anchors), + flag, + shadow, + armed: false, + decls: null, + }; + return { site, decls }; +} + +/** Before an initializer-less typed declaration lowers: open the fallback + * site its later `root = value as T` statement starts, declaring the flag + * (false) and the shadow that holds a failed value where the root is + * declared. Declarations with an initializer open when they arm. */ +export function openFallbackSite(lowerer: Lowerer, stmt: ts.Statement, out: IrStmt[]): void { + if (!ts.isVariableStatement(stmt) || lowerer.fnStack.length === 0) return; + if (lowerer.tierMode === "static-only") return; + const decls = stmt.declarationList.declarations.nodes; + if (decls.length !== 1 || decls[0]!.initializer !== undefined || decls[0]!.type === undefined) + return; + const candidate = fallbackCandidate(lowerer, stmt); + if (candidate === null) return; + const opened = declareSiteLocals(lowerer, candidate, locOf(stmt)); + out.push(...opened.decls); + opened.site.decls = { stmts: opened.decls, out }; + lowerer.fallbackSites.set(candidate.root, opened.site); +} + +/** A site that will not arm: drop its declarations again, so the program + * lowers exactly as it did without the site. */ +function closeUnarmed(lowerer: Lowerer, site: FallbackSite): void { + const decls = site.decls; + if (decls !== null) { + for (const d of decls.stmts) { + const at = decls.out.indexOf(d); + if (at >= 0) decls.out.splice(at, 1); + } + } + const locals = lowerer.ctx.locals; + for (const local of [site.flag, site.shadow]) { + const at = locals.indexOf(local); + if (at >= 0) locals.splice(at, 1); + } +} + +const FALLBACK_TARGETS = new Set(["record", "array", "f64", "string", "bool"]); + +/** A plain check of a record, array or scalar target that holds no class + * instance: casts to program classes keep their checked semantics (a + * documented scriptc boundary: a class cast checks the instance's class). */ +function fallbackCheck(lowerer: Lowerer, check: IrExpr & { kind: "dynCheck" }): boolean { + if (check.preserveRefs || check.exact || !FALLBACK_TARGETS.has(check.type.kind)) return false; + return !holdsClassInstance(lowerer, check.type, new Set()); +} + +function holdsClassInstance(lowerer: Lowerer, t: IrType, seen: Set): boolean { + switch (t.kind) { + case "object": + case "classval": + return true; + case "array": + return holdsClassInstance(lowerer, t.elem, seen); + case "record": { + if (seen.has(t.shapeId)) return false; + seen.add(t.shapeId); + const shape = lowerer.shapes.get(t.shapeId); + if (shape === undefined) return true; + if (shape.indexValue !== undefined && holdsClassInstance(lowerer, shape.indexValue, seen)) + return true; + return shape.fields.some((f) => holdsClassInstance(lowerer, f.type, seen)); + } + case "union": { + const def = lowerer.unions.get(t.unionId); + return def === undefined || def.arms.some((arm) => holdsClassInstance(lowerer, arm, seen)); + } + default: + return false; + } +} + +/** After a statement lowers: when it holds a fallback site's cast, mark the + * check as a fallback check and read its outcome into the flag and the + * shadow. A cast whose lowering is not a plain check keeps throwing. */ +export function armFallbackSite(lowerer: Lowerer, stmt: ts.Statement, lowered: IrStmt[]): void { + // `const root = value as T`: the check is the declaration's initializer. + if ( + ts.isVariableStatement(stmt) && + lowerer.fnStack.length > 0 && + lowerer.tierMode !== "static-only" && + lowered.some((s) => s.kind === "varDecl" && s.init?.kind === "dynCheck") + ) { + const decls = stmt.declarationList.declarations.nodes; + const decl = decls.length === 1 ? decls[0]! : undefined; + if (decl?.initializer !== undefined && ts.isIdentifier(decl.name)) { + const symbol = lowerer.checker.getSymbolAtLocation(decl.name); + const local = symbol ? lowerer.bindingIn(lowerer.ctx, symbol) : null; + const at = + local === null + ? -1 + : lowered.findIndex( + (s) => s.kind === "varDecl" && s.localId === local.id && s.init?.kind === "dynCheck", + ); + const write = at >= 0 ? lowered[at]! : null; + const init = write?.kind === "varDecl" ? write.init : null; + if (init?.kind === "dynCheck" && fallbackCheck(lowerer, init)) { + const candidate = fallbackCandidate(lowerer, stmt); + if (candidate !== null) { + const opened = declareSiteLocals(lowerer, candidate, locOf(stmt)); + if (armSite(lowerer, opened.site, lowered, local!.id)) + lowered.splice(at, 0, ...opened.decls); + else closeUnarmed(lowerer, opened.site); + } + } + } + } + if (lowerer.fallbackSites.size === 0) return; + for (const site of [...lowerer.fallbackSites.values()]) { + if (site.boundaryStmt !== stmt || site.armed) continue; + lowerer.fallbackSites.delete(site.root); + const id = lowerer.bindingIn(lowerer.ctx, site.root)?.id; + if (id === undefined || !armSite(lowerer, site, lowered, id)) closeUnarmed(lowerer, site); + } +} + +function armSite(lowerer: Lowerer, site: FallbackSite, lowered: IrStmt[], id: string): boolean { + const at = lowered.findIndex( + (s) => + (s.kind === "varDecl" && s.localId === id && s.init?.kind === "dynCheck") || + (s.kind === "assign" && s.localId === id && s.value.kind === "dynCheck"), + ); + if (at < 0) return false; + const write = lowered[at]!; + let check: IrExpr | null = null; + if (write.kind === "varDecl") check = write.init; + else if (write.kind === "assign") check = write.value; + if (check === null || check.kind !== "dynCheck" || !fallbackCheck(lowerer, check)) return false; + // Every tail must split: each split point's list is lowering whole. + const whole = [...site.anchors].every((anchor) => { + const real = listOf(anchor.parent!); + const index = real.indexOf(anchor); + return lowerer.activeStmtLists.some( + (entry) => + entry.stmts[entry.index] === anchor && + entry.stmts.length - entry.index === real.length - index, + ); + }); + if (!whole) return false; + // Splits around this site (sequential casts each split the code after + // them) and sites sharing a split point multiply the lowerings. + let nested = lowerer.fallbackSplitDepth; + for (const other of lowerer.fallbackArmed) { + for (const a of other.anchors) if (site.anchors.has(a)) nested++; + } + if (nested >= MAX_NESTED_SPLITS) return false; + check.fallback = true; + const loc = check.loc; + lowered.splice( + at + 1, + 0, + { + kind: "assign", + localId: site.flag.id, + value: { kind: "libCall", fn: "checkedCast.fallbackTaken", args: [], type: BOOL, loc }, + loc, + }, + { + kind: "if", + cond: { kind: "varRef", localId: site.flag.id, type: BOOL, loc }, + then: [ + { + kind: "assign", + localId: site.shadow.id, + value: { kind: "libCall", fn: "checkedCast.fallbackTake", args: [], type: DYN, loc }, + loc, + }, + ], + else_: null, + loc, + }, + ); + site.armed = true; + lowerer.fallbackArmed.push(site); + return true; +} + +/** Armed sites whose tail starts after `stmt`, consumed. */ +export function fallbackSplitsAt(lowerer: Lowerer, stmt: ts.Statement): FallbackSite[] { + if (lowerer.fallbackArmed.length === 0) return []; + const here: FallbackSite[] = []; + for (const site of lowerer.fallbackArmed) { + if (site.anchors.delete(stmt)) here.push(site); + } + if (here.length > 0) + lowerer.fallbackArmed = lowerer.fallbackArmed.filter((s) => s.anchors.size > 0); + return here; +} + +/** The tail after a split point: `if (flag) else ` per + * site. The static lowering runs first and is exactly the lowering the + * tail had without a site. */ +export function lowerFallbackTail( + lowerer: Lowerer, + rest: readonly ts.Statement[], + sites: readonly FallbackSite[], +): IrStmt[] { + if (sites.length === 0) return lowerer.lowerStmts(rest); + // An empty tail (the cast ends its block) needs no split. + if (rest.length === 0) return []; + const [site, ...others] = sites as [FallbackSite, ...FallbackSite[]]; + const at = locOf(rest.length > 0 ? rest[0]! : site.boundaryStmt); + let statics: IrStmt[]; + let dynamic: IrStmt[]; + lowerer.fallbackSplitDepth++; + try { + statics = scoped(lowerer, () => lowerFallbackTail(lowerer, rest, others)); + dynamic = lowerDynamicTail(lowerer, rest, site, others); + } finally { + lowerer.fallbackSplitDepth--; + } + return [ + { + kind: "if", + cond: { kind: "varRef", localId: site.flag.id, type: BOOL, loc: at }, + then: dynamic, + else_: statics, + loc: at, + }, + ]; +} + +function scoped(lowerer: Lowerer, lower: () => T, frame = new Map()): T { + lowerer.scopes.push(frame); + try { + return lower(); + } finally { + lowerer.scopes.pop(); + } +} + +function lowerDynamicTail( + lowerer: Lowerer, + rest: readonly ts.Statement[], + site: FallbackSite, + others: readonly FallbackSite[], +): IrStmt[] { + const saved = { + taint: lowerer.fallbackTaint, + sink: lowerer.diagSink, + stats: lowerer.suppressStats, + sites: new Map(lowerer.fallbackSites), + armed: lowerer.fallbackArmed, + scopes: lowerer.scopes.length, + }; + lowerer.fallbackTaint = fallbackTaintOf(lowerer, saved.taint, site.root, rest); + lowerer.diagSink = []; + lowerer.suppressStats = true; + lowerer.fallbackArmed = []; + try { + return scoped( + lowerer, + () => lowerFallbackTail(lowerer, rest, others), + new Map([[site.root, site.shadow]]), + ); + } catch (error) { + if (!(error instanceof PoisonError) && process.env.SCRIPTC_FALLBACK_DEBUG) throw error; + while (lowerer.scopes.length > saved.scopes) lowerer.scopes.pop(); + const loc = rest.length > 0 ? locOf(rest[0]!) : locOf(site.boundaryStmt); + return [ + { + kind: "runtimeFence", + code: "SC1090", + message: `a value that failed its checked cast reached code with no dynamic lowering`, + loc, + }, + ]; + } finally { + lowerer.fallbackTaint = saved.taint; + lowerer.diagSink = saved.sink; + lowerer.suppressStats = saved.stats; + lowerer.fallbackSites = saved.sites; + lowerer.fallbackArmed = saved.armed; + } +} + +/* ── taint: which expressions of a dynamic tail read a failed value ── */ + +/** True when `node`'s value derives from a failed checked value in the + * dynamic lowering being built: it is typed `any` there. */ +export function fallbackTainted(lowerer: Lowerer, node: ts.Node): boolean { + const taint = lowerer.fallbackTaint; + if (taint === null) return false; + const known = taint.memo.get(node); + if (known !== undefined) return known; + taint.memo.set(node, false); + const result = computeTainted(lowerer, taint, node); + taint.memo.set(node, result); + return result; +} + +function computeTainted(lowerer: Lowerer, taint: FallbackTaint, node: ts.Node): boolean { + const t = (n: ts.Node): boolean => fallbackTainted(lowerer, n); + if (ts.isIdentifier(node)) { + const sym = lowerer.resolveValueSymbol(node); + return sym !== null && taint.symbols.has(sym); + } + if ( + ts.isParenthesizedExpression(node) || + ts.isNonNullExpression(node) || + ts.isAsExpression(node) || + ts.isTypeAssertion(node) || + ts.isSatisfiesExpression(node) || + ts.isAwaitExpression(node) + ) + return t(node.expression); + if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) + return t(node.expression); + if (ts.isCallExpression(node)) return t(node.expression); + if (ts.isConditionalExpression(node)) return t(node.whenTrue) || t(node.whenFalse); + if (ts.isBinaryExpression(node)) { + const k = node.operatorToken.kind; + switch (k) { + case ts.SyntaxKind.QuestionQuestionToken: + case ts.SyntaxKind.BarBarToken: + case ts.SyntaxKind.AmpersandAmpersandToken: + case ts.SyntaxKind.PlusToken: + return t(node.left) || t(node.right); + case ts.SyntaxKind.CommaToken: + case ts.SyntaxKind.EqualsToken: + return t(node.right); + case ts.SyntaxKind.PlusEqualsToken: + case ts.SyntaxKind.QuestionQuestionEqualsToken: + case ts.SyntaxKind.BarBarEqualsToken: + case ts.SyntaxKind.AmpersandAmpersandEqualsToken: + return t(node.left) || t(node.right); + default: + return false; + } + } + // A literal beside a failed value (`value ?? {}`, `ok ? value : []`) + // is built dynamically too: its contextual type is the static one. + if (ts.isObjectLiteralExpression(node) || ts.isArrayLiteralExpression(node)) { + // A literal spreading a failed value is built dynamically, and so is + // one holding a failed value that initializes a binding (the binding + // turns dynamic) or sits in such a literal. A literal flowing straight + // into a typed slot keeps its layout: each failed member is checked + // into its field, so members that fit continue. + let holds = false; + if (ts.isObjectLiteralExpression(node)) { + for (const p of node.properties.nodes) { + if (ts.isSpreadAssignment(p) && t(p.expression)) return true; + if ( + (ts.isPropertyAssignment(p) && t(p.initializer)) || + (ts.isShorthandPropertyAssignment(p) && t(p.name)) + ) + holds = true; + } + } else { + for (const e of node.elements.nodes) + if (t(ts.isSpreadElement(e) ? e.expression : e)) holds = true; + } + if (holds) { + // The outermost literal of a nest decides for all of it. + let top: ts.Node = node; + for (;;) { + const host: ts.Node | undefined = top.parent; + if (host !== undefined && ts.isPropertyAssignment(host) && host.parent !== undefined) { + top = host.parent; + continue; + } + if (host !== undefined && ts.isArrayLiteralExpression(host)) { + top = host; + continue; + } + break; + } + // An untyped destination (`console.log`, `JSON.stringify`) takes it + // dynamically. + const contextual = lowerer.checker.getContextualType(top); + if ( + contextual !== undefined && + (contextual.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 + ) + return true; + const host = top.parent; + if (host !== undefined && ts.isVariableDeclaration(host) && host.initializer === top) + return true; + } + let child: ts.Node = node; + let parent = node.parent; + while (parent !== undefined && ts.isParenthesizedExpression(parent)) { + child = parent; + parent = parent.parent; + } + if (parent === undefined) return false; + if (ts.isBinaryExpression(parent)) { + const k = parent.operatorToken.kind; + if ( + k === ts.SyntaxKind.QuestionQuestionToken || + k === ts.SyntaxKind.BarBarToken || + k === ts.SyntaxKind.AmpersandAmpersandToken + ) + return t(parent.left === child ? parent.right : parent.left); + } + if (ts.isConditionalExpression(parent) && parent.condition !== child) + return t(parent.whenTrue === child ? parent.whenFalse : parent.whenTrue); + return false; + } + if (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) { + if ( + node.operator === ts.SyntaxKind.PlusPlusToken || + node.operator === ts.SyntaxKind.MinusMinusToken + ) + return t(node.operand); + return false; + } + return false; +} + +/** The taint of a dynamic tail: the root, and every binding declared in the + * tail that a tainted value is written to (a fixpoint over the tail, + * nested functions included). Parameters of callbacks passed to a tainted + * callee are tainted too: the dynamic call passes them dynamic values. */ +function fallbackTaintOf( + lowerer: Lowerer, + outer: FallbackTaint | null, + root: ts.Symbol, + region: readonly ts.Statement[], +): FallbackTaint { + const symbols = new Set(outer?.symbols ?? []); + symbols.add(root); + const taint: FallbackTaint = { symbols, memo: new Map() }; + const saved = lowerer.fallbackTaint; + lowerer.fallbackTaint = taint; + try { + growTaint(lowerer, taint, region); + } finally { + lowerer.fallbackTaint = saved; + } + return taint; +} + +function growTaint(lowerer: Lowerer, taint: FallbackTaint, region: readonly ts.Statement[]): void { + const symbols = taint.symbols; + const declared = new Set(); + const collectDeclared = (n: ts.Node): void => { + if ( + (ts.isVariableDeclaration(n) || ts.isParameter(n) || ts.isBindingElement(n)) && + ts.isIdentifier(n.name) + ) { + const s = lowerer.checker.getSymbolAtLocation(n.name); + if (s) declared.add(s); + } + ts.forEachChild(n, collectDeclared); + }; + for (const s of region) collectDeclared(s); + const bindNames = (name: ts.BindingName): boolean => { + let changed = false; + const visit = (n: ts.Node): void => { + if (ts.isIdentifier(n)) { + const s = lowerer.checker.getSymbolAtLocation(n); + if (s && !symbols.has(s)) { + symbols.add(s); + changed = true; + } + return; + } + if (ts.isBindingElement(n)) { + if (n.name !== undefined) visit(n.name); + return; + } + ts.forEachChild(n, visit); + }; + visit(name); + return changed; + }; + for (let round = 0; round < 32; round++) { + let changed = false; + taint.memo.clear(); + const visit = (n: ts.Node): void => { + if ( + ts.isVariableDeclaration(n) && + n.initializer !== undefined && + fallbackTainted(lowerer, n.initializer) + ) { + if (bindNames(n.name)) changed = true; + } else if (ts.isBinaryExpression(n)) { + const k = n.operatorToken.kind; + if ( + k >= ts.SyntaxKind.FirstAssignment && + k <= ts.SyntaxKind.LastAssignment && + ts.isIdentifier(strip(n.left)) + ) { + const s = lowerer.resolveValueSymbol(strip(n.left) as ts.Identifier); + if (s && declared.has(s) && !symbols.has(s) && fallbackTainted(lowerer, n.right)) { + symbols.add(s); + changed = true; + } + } + } else if ( + ts.isForOfStatement(n) && + ts.isVariableDeclarationList(n.initializer) && + fallbackTainted(lowerer, n.expression) + ) { + for (const d of n.initializer.declarations.nodes) if (bindNames(d.name)) changed = true; + } else if (ts.isCallExpression(n) && fallbackTainted(lowerer, n.expression)) { + for (const arg of n.arguments.nodes) { + const a = strip(arg); + if (ts.isArrowFunction(a) || ts.isFunctionExpression(a)) { + for (const p of a.parameters.nodes) if (bindNames(p.name)) changed = true; + } + } + } + ts.forEachChild(n, visit); + }; + for (const s of region) visit(s); + if (!changed) break; + } + taint.memo.clear(); +} diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts index 5e72e4bf7..ae8cce4ca 100644 --- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -41,12 +41,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) || @@ -63,7 +63,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) ); } @@ -76,7 +76,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) && @@ -187,7 +187,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; } @@ -253,7 +253,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). @@ -329,7 +329,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; @@ -390,7 +390,7 @@ function esModuleExportIgnoresThis( return decls.every( (d) => (ts.isFunctionDeclaration(d) && !functionUsesThis(d)) || - (d.getSourceFile().isDeclarationFile && ts.isFunctionDeclaration(d)), + (ts.getSourceFileOfNode(d).isDeclarationFile && ts.isFunctionDeclaration(d)), ); } for (const stmt of sf.statements.nodes) { @@ -504,7 +504,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"); @@ -598,7 +598,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; diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts index 174f9dc7c..2b28c6533 100644 --- a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts +++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts @@ -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 && diff --git a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts index cd83c8709..aae221b30 100644 --- a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts +++ b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts @@ -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(); 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; diff --git a/packages/compiler/src/frontend/lowering/class-callbacks.ts b/packages/compiler/src/frontend/lowering/class-callbacks.ts index 859938845..6ca685684 100644 --- a/packages/compiler/src/frontend/lowering/class-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/class-callbacks.ts @@ -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); }); diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index 5fc2a3cec..cb5c28444 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -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; @@ -5245,7 +5245,15 @@ export class ClassDynamicDispatch { if (methodInfo) { owner = methodInfo.declarer; params = methodInfo.sig.params; - result = methodInfo.sig.ret; + // An untyped async method's `dyn` result is its promise's payload; the + // call answers the promise. An async generator method answers its + // generator object, which its signature already returns. + result = + methodInfo.sig.async === true && + !methodInfo.sig.gen && + methodInfo.sig.ret.kind !== "promise" + ? { kind: "promise", inner: methodInfo.sig.ret } + : methodInfo.sig.ret; callee = `%${owner.def.name}.${method}`; virtual = lowerer.overrideBelow(owner, method) || methodInfo.sig.abstract === true; } else if ( diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index 981cd0c1a..cc8700469 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -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; diff --git a/packages/compiler/src/frontend/lowering/class-method-values.ts b/packages/compiler/src/frontend/lowering/class-method-values.ts index 795307c93..1197d9bdd 100644 --- a/packages/compiler/src/frontend/lowering/class-method-values.ts +++ b/packages/compiler/src/frontend/lowering/class-method-values.ts @@ -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) => { diff --git a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts index 298ff45dc..33515bf2c 100644 --- a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts @@ -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; diff --git a/packages/compiler/src/frontend/lowering/class-protocols.ts b/packages/compiler/src/frontend/lowering/class-protocols.ts index 5a27f0815..4de7f03ad 100644 --- a/packages/compiler/src/frontend/lowering/class-protocols.ts +++ b/packages/compiler/src/frontend/lowering/class-protocols.ts @@ -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(); const writesNames = (written: Set | "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; diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 6c6c7811d..7b4b58ae3 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -189,12 +189,16 @@ export function classPrototypeData( continue; const compiled = classMethodValue(lowerer, access, info, method, locOf(access)); if (!compiled) continue; + // A method value without a checked-dynamic form (an async method's + // promise-returning signature) gets no data slot. + const boxedValue = lowerer.coerceToExpected(compiled, DYN); + if (boxedValue.type.kind !== "dyn") continue; const descriptor: IrExpr = { kind: "dynObjLit", fields: [ { key: { kind: "strLit", value: "value", type: STRING, loc }, - value: lowerer.coerceToExpected(compiled, DYN), + value: boxedValue, }, { key: { kind: "strLit", value: "writable", type: STRING, loc }, diff --git a/packages/compiler/src/frontend/lowering/class-static-data.ts b/packages/compiler/src/frontend/lowering/class-static-data.ts index af5425727..dd6927d18 100644 --- a/packages/compiler/src/frontend/lowering/class-static-data.ts +++ b/packages/compiler/src/frontend/lowering/class-static-data.ts @@ -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) ) diff --git a/packages/compiler/src/frontend/lowering/code-generation.ts b/packages/compiler/src/frontend/lowering/code-generation.ts index b3bd9243a..c3a5c0ea7 100644 --- a/packages/compiler/src/frontend/lowering/code-generation.ts +++ b/packages/compiler/src/frontend/lowering/code-generation.ts @@ -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 ( diff --git a/packages/compiler/src/frontend/lowering/collection-ingestion.ts b/packages/compiler/src/frontend/lowering/collection-ingestion.ts index 5449b0943..5d14d5492 100644 --- a/packages/compiler/src/frontend/lowering/collection-ingestion.ts +++ b/packages/compiler/src/frontend/lowering/collection-ingestion.ts @@ -91,7 +91,7 @@ export function collectionInput( const symbol = type.getSymbol(); if ( !symbol || - (symbol.name !== "MapIterator" && symbol.name !== "SetIterator") || + (ts.symbolName(symbol) !== "MapIterator" && ts.symbolName(symbol) !== "SetIterator") || !lowerer.isStdlibSymbol(symbol) ) return null; diff --git a/packages/compiler/src/frontend/lowering/collection-narrowing.ts b/packages/compiler/src/frontend/lowering/collection-narrowing.ts index 29b358e99..6c1ca80f4 100644 --- a/packages/compiler/src/frontend/lowering/collection-narrowing.ts +++ b/packages/compiler/src/frontend/lowering/collection-narrowing.ts @@ -11,9 +11,12 @@ export function concreteCollectionNarrow( ): ts.Type | null { if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return null; const symbol = narrowed.getSymbol(); - const name = symbol?.name; + const name = ts.symbolNameOf(symbol); if (name !== "Map" && name !== "Set") return null; - if (!symbol || !symbol.declarations.some((decl) => lowerer.isStdlibFile(decl.getSourceFile()))) + if ( + !symbol || + !symbol.declarations.some((decl) => lowerer.isStdlibFile(ts.getSourceFileOfNode(decl))) + ) return null; const args = lowerer.checker.getTypeArguments(narrowed as ts.TypeReference); if (args.length === 0 || !args.every((arg) => (arg.flags & ts.TypeFlags.Any) !== 0)) return null; diff --git a/packages/compiler/src/frontend/lowering/construction-escapes.test.ts b/packages/compiler/src/frontend/lowering/construction-escapes.test.ts index 77f72dc15..6807946d0 100644 --- a/packages/compiler/src/frontend/lowering/construction-escapes.test.ts +++ b/packages/compiler/src/frontend/lowering/construction-escapes.test.ts @@ -16,7 +16,7 @@ function analyze(source: string, options: { derived?: boolean; baseEscapes?: boo const ctor = cls.members.nodes.find(ts.isConstructorDeclaration) ?? null; const fields = cls.members.nodes .filter(ts.isPropertyDeclaration) - .map((member) => ({ name: member.name.getText(), initializer: member.initializer })); + .map((member) => ({ name: ts.getTextOfNode(member.name), initializer: member.initializer })); const fieldNames = new Set(fields.map((field) => field.name)); return constructionEscape({ ctor, @@ -30,7 +30,7 @@ function analyze(source: string, options: { derived?: boolean; baseEscapes?: boo const name = call.expression.name.text; const method = cls.members.nodes.find( (member): member is ts.MethodDeclaration => - ts.isMethodDeclaration(member) && member.name.getText() === name, + ts.isMethodDeclaration(member) && ts.getTextOfNode(member.name) === name, ); const isPrivate = method !== undefined && diff --git a/packages/compiler/src/frontend/lowering/construction-escapes.ts b/packages/compiler/src/frontend/lowering/construction-escapes.ts index b9c8d7e07..3fdae4ab4 100644 --- a/packages/compiler/src/frontend/lowering/construction-escapes.ts +++ b/packages/compiler/src/frontend/lowering/construction-escapes.ts @@ -23,7 +23,7 @@ export function mentionsReceiver(node: ts.Node): boolean { found = true; return; } - child.forEachChild(visit); + ts.forEachChild(child, visit); }; visit(node); return found; @@ -68,9 +68,9 @@ function containsReturn(node: ts.Node): boolean { found = true; return; } - child.forEachChild(visit); + ts.forEachChild(child, visit); }; - node.forEachChild(visit); + ts.forEachChild(node, visit); return found; } @@ -100,7 +100,7 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca return !(ts.isIdentifier(node.name) && assigned.has(name) && inputs.isField(name)); } let found = false; - node.forEachChild((child) => { + ts.forEachChild(node, (child) => { if (!found && mentionsReceiver(child)) found = exposes(child, depth); }); return found; diff --git a/packages/compiler/src/frontend/lowering/constructor-returns.ts b/packages/compiler/src/frontend/lowering/constructor-returns.ts index bee2e7567..0e600e700 100644 --- a/packages/compiler/src/frontend/lowering/constructor-returns.ts +++ b/packages/compiler/src/frontend/lowering/constructor-returns.ts @@ -86,9 +86,9 @@ function ownValueReturns(ctor: ts.ConstructorDeclaration): ts.ReturnStatement[] const visit = (node: ts.Node): void => { if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return; if (ts.isReturnStatement(node) && node.expression !== undefined) out.push(node); - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - ctor.body?.forEachChild(visit); + if (ctor.body !== undefined) ts.forEachChild(ctor.body, visit); return out; } @@ -188,9 +188,9 @@ function ownReturns(body: ts.Node): ts.ReturnStatement[] { const visit = (node: ts.Node): void => { if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return; if (ts.isReturnStatement(node)) out.push(node); - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - body.forEachChild(visit); + ts.forEachChild(body, visit); return out; } @@ -225,7 +225,7 @@ export function constructorReturnPlan( // the class too (`if (C.instance) return C.instance;`). const runtimeInstance = info.localClass === undefined && - isJsSourceFile(ctor.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(ctor)) && [...returns].some(([stmt, kind]) => kind === "mixed" && !insideFinallyTry(stmt, ctor)); if ([...returns.values()].includes("instance") || runtimeInstance) { slot = { @@ -277,7 +277,7 @@ export function lowerConstructorReturn(lowerer: Lowerer, stmt: ts.ReturnStatemen }; } const feature = foreignObjectFeature(info); - if (kind === "object" || !isJsSourceFile(stmt.getSourceFile())) + if (kind === "object" || !isJsSourceFile(ts.getSourceFileOfNode(stmt))) lowerer.unsupported("SC1090", stmt, feature); // A JavaScript value that is an object only sometimes: a primitive // still answers the constructed instance, an object refuses. @@ -296,7 +296,7 @@ export function lowerConstructorReturn(lowerer: Lowerer, stmt: ts.ReturnStatemen }); const diag = unsupportedDiag("SC1090", loc, feature); lowerer.runtimeFences.push(diag); - const sf = stmt.getSourceFile(); + const sf = ts.getSourceFileOfNode(stmt); const line = ts.getLineAndCharacterOfPosition(sf, loc.start).line + 1; return { kind: "block", @@ -460,7 +460,7 @@ export function superIntoResultSlot( const slot = plan.slot; const display = base.def.name.replace(/^%|^%m\d+\./, ""); const feature = `extending a class whose constructor can return an existing instance ('super()' would rebind 'this' to the instance '${display}' returns)`; - if (!isJsSourceFile(blame.getSourceFile())) { + if (!isJsSourceFile(ts.getSourceFileOfNode(blame))) { lowerer.unsupported("SC1090", blame, feature); return call; } @@ -470,7 +470,8 @@ export function superIntoResultSlot( if (!lowerer.liftedFns.some((fn) => fn.name === name)) { const diag = unsupportedDiag("SC1090", loc, feature); lowerer.runtimeFences.push(diag); - const line = ts.getLineAndCharacterOfPosition(blame.getSourceFile(), loc.start).line + 1; + const line = + ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(blame), loc.start).line + 1; const instance = { id: "instance.0", name: "instance", type, mutable: false }; const returned = { id: "returned.0", name: "returned", type: slot.type, mutable: false }; lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index 735361f54..5406c3ec1 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -304,7 +304,7 @@ function lowerFastCallback( fastLoweringDepth > 0 || slow.arity <= elemIndex || !ts.isArrowFunction(argNode) || - isJsSourceFile(argNode.getSourceFile()) || + isJsSourceFile(ts.getSourceFileOfNode(argNode)) || valueT?.kind !== "union" || typeEquals(valueT, elem) || elem.kind === "union" || @@ -1489,7 +1489,7 @@ export function lowerArrayFlatMapCall( kind: "dynInvoke", recv: { kind: "dynFrom", value: receiver, liveRef: true, type: DYN, loc }, method: "flatMap", - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: [{ kind: "dynFrom", value: fnArg, type: DYN, loc }], type: DYN, loc, @@ -2085,7 +2085,7 @@ export function lowerArraySortCall( // Arithmetic on inferred JS values retains a checked result because // it can produce BigInt. The sort callback still requires a number. const result = - fnArg.type.ret.kind === "dyn" && isJsSourceFile(argNode.getSourceFile()) + fnArg.type.ret.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(argNode)) ? F64 : fnArg.type.ret; const expected = funcOf( @@ -2223,7 +2223,7 @@ export function lowerDynArrayFilterCall( recv, fnArg, { kind: "strLit", value: access.name.text, type: STRING, loc }, - { kind: "strLit", value: access.getText(), type: STRING, loc }, + { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc }, ], type: arrayOf(elemT), loc, @@ -2264,7 +2264,7 @@ export function lowerDynArrayFlatMapCall( kind: "dynInvoke", recv, method: "flatMap", - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: [boxed], type: DYN, loc, @@ -2286,7 +2286,7 @@ export function lowerDynArrayFlatMapCall( recv, fnArg, { kind: "strLit", value: access.name.text, type: STRING, loc }, - { kind: "strLit", value: access.getText(), type: STRING, loc }, + { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc }, ], type: arrayOf(ret.elem), loc, diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 110af2b56..53d46d00d 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -296,7 +296,7 @@ export function lowerArrayConstructor( // reference. Known element forms retain native arrays, including their // existing sparse storage and indexed property contracts. if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && (result?.kind !== "array" || result.elem.kind === "dyn") ) { return { @@ -321,7 +321,7 @@ export function lowerArrayConstructor( : null; if (elem !== null && isSupportedArrayElem(elem)) result = arrayOf(elem); } - if (isJsSourceFile(expr.getSourceFile()) && (result === null || result.kind === "dyn")) + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && (result === null || result.kind === "dyn")) result = arrayOf(DYN); if (result?.kind !== "array" || !isSupportedArrayElem(result.elem)) { lowerer.badType(expr, lowerer.typeOf(expr)); @@ -823,7 +823,7 @@ export function lowerArrayFromCall( const loc = locOf(call); const args = call.arguments.nodes; if ( - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && args.length >= 1 && args.length <= 3 && !args.some(ts.isSpreadElement) diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index 7ef96457c..12078720e 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -353,7 +353,7 @@ export function lowerArrayMethodCall( // machinery answered string[]): same rule, any-typed spelling. ((lowerer.typeOf(access.expression).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 && - isJsSourceFile(access.getSourceFile()))) + isJsSourceFile(ts.getSourceFileOfNode(access)))) ) { const probe = lowerer.lowerExpr(access.expression); if (probe.type.kind === "array") { @@ -1039,7 +1039,7 @@ export function lowerArrayMethodCall( kind: "dynInvoke", recv: receiver, method: "join", - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args, type: DYN, loc, @@ -2096,7 +2096,7 @@ export function lowerTupleReadMethodCall( kind: "dynInvoke", recv: { kind: "dynFrom", value: receiver, type: DYN, loc }, method: "flatMap", - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: [{ kind: "dynFrom", value: fnArg, type: DYN, loc }], type: DYN, loc, diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index c5a41cf32..49292c260 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -24,6 +24,7 @@ import { typeEquals, } from "../../../ir/ir.js"; import { isJsSourceFile, locOf } from "../../program.js"; +import { stdlibGlobalNameOf } from "../surfaces.js"; import type { Lowerer } from "../lowerer.js"; import { newFnCtx, own } from "../lowerer.js"; import { buildBytesSortFn, closureKeyOrder } from "../lower-array-sort.js"; @@ -120,7 +121,7 @@ function lowerBytesSortCall( fnArg.type.params.every((param) => lowerer.coercibleValue(F64, param)) ) { const result = - fnArg.type.ret.kind === "dyn" && isJsSourceFile(argNode.getSourceFile()) + fnArg.type.ret.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(argNode)) ? F64 : fnArg.type.ret; const expected = funcOf( @@ -187,7 +188,7 @@ export function lowerBytesNew( ): IrExpr | null { if ( symbol != null && - (symbol.name === "ArrayBuffer" || symbol.name === "SharedArrayBuffer") && + (ts.symbolName(symbol) === "ArrayBuffer" || ts.symbolName(symbol) === "SharedArrayBuffer") && lowerer.isStdlibSymbol(symbol) ) { const args = expr.arguments?.nodes ?? []; @@ -203,18 +204,19 @@ export function lowerBytesNew( : dynUndefinedExpr(locOf(expr)); return { kind: "libCall", - fn: symbol.name === "SharedArrayBuffer" ? "sharedArrayBuffer.new" : "arrayBuffer.new", + fn: + ts.symbolName(symbol) === "SharedArrayBuffer" ? "sharedArrayBuffer.new" : "arrayBuffer.new", args: [length], type: DYN, loc: locOf(expr), }; } - if (symbol && symbol.name === "DataView" && lowerer.isStdlibSymbol(symbol)) { + if (symbol && ts.symbolName(symbol) === "DataView" && lowerer.isStdlibSymbol(symbol)) { return lowerDataViewNew(lowerer, expr); } - const elem = symbol ? own(BYTES_CTORS, symbol.name) : undefined; + const elem = symbol ? own(BYTES_CTORS, ts.symbolName(symbol)) : undefined; if (!elem || !symbol || !lowerer.isStdlibSymbol(symbol)) return null; - const name = symbol.name; + const name = ts.symbolName(symbol); const type = bytesOf(elem); const loc = locOf(expr); const args = expr.arguments?.nodes ?? []; @@ -291,7 +293,7 @@ export function lowerBytesStaticCall( } if (access.name.text !== "from" || !ts.isIdentifier(access.expression)) return null; const symbol = lowerer.resolveValueSymbol(access.expression); - const elem = symbol ? own(BYTES_CTORS, symbol.name) : undefined; + const elem = symbol ? own(BYTES_CTORS, ts.symbolName(symbol)) : undefined; if (!elem || !lowerer.isStdlibSymbol(symbol ?? undefined)) return null; if ( call.arguments.nodes.length < 1 || @@ -442,7 +444,7 @@ export function lowerBytesMethodCall( const nArgs = call.arguments.nodes.length; // Encoded stream callbacks can receive strings even when JavaScript's // contextual inference still describes their parameter as a Buffer. - if (name === "toString" && nArgs <= 1 && isJsSourceFile(call.getSourceFile())) { + if (name === "toString" && nArgs <= 1 && isJsSourceFile(ts.getSourceFileOfNode(call))) { const lowered = lowerer.lowerExpr(access.expression); const receiver = lowered.kind === "dynCheck" && lowered.value.type.kind === "dyn" ? lowered.value : lowered; @@ -455,7 +457,7 @@ export function lowerBytesMethodCall( call.arguments.nodes[0] ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(loc), - { kind: "strLit", value: access.getText(), type: STRING, loc }, + { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc }, ], type: STRING, loc, @@ -1305,6 +1307,185 @@ function knownBufferProducer( return type?.kind === "cryptoHash" || type?.kind === "cryptoHmac"; } +/** Buffer.from over a checked-dynamic source and encoding, both already in + * repeatable locals: Node's fromString takes utf8 for an absent, non-string + * or empty encoding and throws ERR_UNKNOWN_ENCODING for an unknown name; + * other sources ignore the encoding. */ +function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrExpr { + const utf8: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; + const isString = (x: IrExpr): IrExpr => ({ + kind: "dynTest", + test: "string", + value: x, + type: BOOL, + loc, + }); + const name: IrExpr = { + kind: "ternary", + cond: { + kind: "logical", + op: "&&", + left: isString(encoding), + right: { + kind: "strEq", + negated: true, + left: { kind: "dynCheck", value: encoding, type: STRING, loc }, + right: { kind: "strLit", value: "", type: STRING, loc }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + then: { kind: "dynCheck", value: encoding, type: STRING, loc }, + else_: utf8, + type: STRING, + loc, + }; + return { + kind: "ternary", + cond: isString(source), + then: { + kind: "libCall", + fn: "buffer.poolFromStrChecked", + args: [{ kind: "dynCheck", value: source, type: STRING, loc }, name], + type: BYTES_U8, + loc, + }, + else_: { + kind: "libCall", + fn: "buffer.poolFromDynRef", + args: [source, utf8], + type: BYTES_U8, + loc, + }, + type: BYTES_U8, + loc, + }; +} + +/** Lowers `source` and `encNode` (or undefined) into hidden dyn locals, in + * source order, and builds the result from their references. */ +function withDynBufferArguments( + lowerer: Lowerer, + source: IrExpr, + encNode: ts.Expression | undefined, + loc: SrcLoc, + build: (source: IrExpr, encoding: IrExpr) => IrExpr, +): IrExpr { + const v = lowerer.declareHiddenLocal("%bufferValue", DYN); + const e = lowerer.declareHiddenLocal("%bufferEncoding", DYN); + const result = build(varRef(v.id, DYN, loc), varRef(e.id, DYN, loc)); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: v.id, init: lowerer.coerceToExpected(source, DYN), loc }, + { + kind: "varDecl", + localId: e.id, + init: + encNode === undefined + ? dynUndefinedExpr(loc) + : lowerer.coerceToExpected(lowerer.lowerExpr(encNode), DYN), + loc, + }, + ], + result, + type: result.type, + loc, + }; +} + +/** Buffer.from(value, encoding) over a checked-dynamic value whose encoding + * is not a literal: yazl forwards its `encoding` parameter, often undefined. + * Null when the encoding is absent or a literal. */ +function dynBufferFromRuntimeEncoding( + lowerer: Lowerer, + value: IrExpr, + encNode: ts.Expression | undefined, + loc: SrcLoc, +): IrExpr | null { + if (encNode === undefined || lowerer.typeOf(encNode).isStringLiteralType()) return null; + return withDynBufferArguments(lowerer, value, encNode, loc, (source, encoding) => + dynBufferFromParts(source, encoding, loc), + ); +} + +/** The deprecated `new Buffer(arg[, encoding])` in a dependency's + * JavaScript, through the global or an alias of it (buffer-crc32 and yazl + * keep it as the fallback for runtimes without Buffer.alloc/Buffer.from). + * Node never emits DEP0005 for calls from node_modules, so the construction + * is exactly Node's: a number allocates zero-filled bytes (and throws + * ERR_INVALID_ARG_TYPE beside a string second argument), anything else + * converts as Buffer.from does. Null elsewhere: other callers owe the + * deprecation warning. */ +export function lowerNodeModulesBufferConstruct( + lowerer: Lowerer, + expr: ts.NewExpression, +): IrExpr | null { + const sf = expr.getSourceFile(); + if (!isJsSourceFile(sf) || !/[\\/]node_modules[\\/]/.test(sf.fileName)) return null; + const args = expr.arguments?.nodes ?? []; + if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) return null; + if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Buffer") return null; + const loc = locOf(expr); + const value = withDynBufferArguments( + lowerer, + lowerer.lowerExpr(args[0]!), + args[1], + loc, + (source, encoding) => ({ + kind: "ternary", + cond: { kind: "dynTest", test: "number", value: source, type: BOOL, loc }, + then: { + kind: "ternary", + cond: { kind: "dynTest", test: "string", value: encoding, type: BOOL, loc }, + then: { kind: "libCall", fn: "buffer.newStringFail", args: [source], type: BYTES_U8, loc }, + else_: { + kind: "libCall", + fn: "buffer.alloc", + args: [{ kind: "dynCheck", value: source, type: F64, loc }], + type: BYTES_U8, + loc, + }, + type: BYTES_U8, + loc, + }, + else_: dynBufferFromParts(source, encoding, loc), + type: BYTES_U8, + loc, + }), + ); + return { kind: "libCall", fn: "buffer.brand", args: [value], type: BYTES_U8, loc }; +} + +/** Buffer.from(array or bytes, extra): Node copies an array-like source and + * ignores the encoding/offset argument (yazl passes its `encoding` through + * unchanged), but JavaScript still evaluates it after the source. */ +function withIgnoredBufferArgument( + lowerer: Lowerer, + extraNode: ts.Expression | undefined, + source: IrExpr, + build: (source: IrExpr) => IrExpr, +): IrExpr { + if (extraNode === undefined) return build(source); + const extra = lowerer.lowerExpr(extraNode); + if (isSafeToDiscard(extra)) return build(source); + const loc = source.loc; + const tmp = lowerer.declareHiddenLocal("%bufferSource", source.type); + const result = build(varRef(tmp.id, source.type, loc)); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: tmp.id, init: source, loc }, + { kind: "exprStmt", expr: extra, loc }, + ], + result, + type: result.type, + loc, + }; +} + export function lowerBufferStaticCall( lowerer: Lowerer, call: ts.CallExpression, @@ -1334,7 +1515,8 @@ function lowerBufferStaticValue( args.length <= 3 && !args.some(ts.isSpreadElement) && ((symbol) => - symbol != null && (symbol.name === "ArrayBuffer" || symbol.name === "SharedArrayBuffer"))( + symbol != null && + (ts.symbolName(symbol) === "ArrayBuffer" || ts.symbolName(symbol) === "SharedArrayBuffer"))( lowerer.typeOf(args[0]!).getSymbol(), ) ) { @@ -1376,17 +1558,23 @@ function lowerBufferStaticValue( ) { // A number[] literal (contextually typed by the lib's readonly // number[] slot — build it element-wise, the Set-seed pattern). - if (args.length === 1) { - const elems = argNode.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, F64)); - const seed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc }; - return { kind: "libCall", fn: "buffer.poolFromArray", args: [seed], type: BYTES_U8, loc }; - } + const elems = argNode.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, F64)); + const seed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc }; + return withIgnoredBufferArgument(lowerer, args[1], seed, (array) => ({ + kind: "libCall", + fn: "buffer.poolFromArray", + args: [array], + type: BYTES_U8, + loc, + })); } else { const srcIr = lowerer.mapTypeOf(lowerer.typeOf(argNode)); const value = lowerer.lowerExpr(argNode); // JS callback storage can be native DYN even when the checker // reports any (or a narrowed Buffer). Dispatch on its actual ABI. if (value.type.kind === "dyn") { + const runtimeEncoding = dynBufferFromRuntimeEncoding(lowerer, value, args[1], loc); + if (runtimeEncoding) return runtimeEncoding; const encName = args[1] ? bufEncoding(lowerer, "Buffer.from", args[1]) : "utf8"; const enc: IrExpr = { kind: "strLit", value: encName, type: STRING, loc }; return { @@ -1430,26 +1618,25 @@ function lowerBufferStaticValue( // { toText: true } ? string : Uint8Array` result) whose lowered // value is a u8 view copies like the typed form. if ( - args.length === 1 && - ((srcIr?.kind === "bytes" && srcIr.elem === "u8") || - (srcIr === null && value.type.kind === "bytes" && value.type.elem === "u8")) + (srcIr?.kind === "bytes" && srcIr.elem === "u8") || + (srcIr === null && value.type.kind === "bytes" && value.type.elem === "u8") ) { - return { + return withIgnoredBufferArgument(lowerer, args[1], value, (bytes) => ({ kind: "libCall", fn: "buffer.poolFromBytes", - args: [value], + args: [bytes], type: BYTES_U8, loc, - }; + })); } - if (args.length === 1 && srcIr?.kind === "array" && srcIr.elem.kind === "f64") { - return { + if (srcIr?.kind === "array" && srcIr.elem.kind === "f64") { + return withIgnoredBufferArgument(lowerer, args[1], value, (array) => ({ kind: "libCall", fn: "buffer.poolFromArray", - args: [value], + args: [array], type: BYTES_U8, loc, - }; + })); } // A union of strings, u8 views and undefined/null (a JS-inferred // npm result such as pako's deflate): the checked form dispatches @@ -1650,7 +1837,9 @@ function lowerBufferStaticValue( // flavor. Plain bytes are Uint8Arrays at this boundary; inspecting // only the bytes tag would incorrectly identify them as Buffers. const checked = - isJsSourceFile(call.getSourceFile()) && v.kind === "dynCheck" && v.value.type.kind === "dyn" + isJsSourceFile(ts.getSourceFileOfNode(call)) && + v.kind === "dynCheck" && + v.value.type.kind === "dyn" ? v.value : v; if (checked.type.kind === "dyn") { diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index dcd9934bd..18754f088 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -59,7 +59,7 @@ export function lowerArrayCallback( if ( ts.isIdentifier(argNode) && argNode.text === "Boolean" && - !isJsSourceFile(argNode.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(argNode)) && lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(argNode) ?? undefined) ) { const valueT = lead[0]; diff --git a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts index 7c4ddf220..0bf126830 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts @@ -67,7 +67,7 @@ export function lowerGroupByStaticCall( const fNode = call.arguments.nodes[1]!; // An unannotated JavaScript callback parameter takes the element. const f = - isJsSourceFile(fNode.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(fNode)) && (ts.isArrowFunction(fNode) || ts.isFunctionExpression(fNode)) ? lowerArrayCallback(lowerer, fNode, [elem], groupT, true).fnArg : lowerer.lowerExpr(fNode); diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index 6a5290c0f..6aa6d6faf 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -60,7 +60,7 @@ export function lowerMapMethodCall( if (!MAP_METHODS.has(name)) return null; let receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); let probedUntyped = false; - if (receiverIr?.kind !== "map" && isJsSourceFile(access.getSourceFile())) { + if (receiverIr?.kind !== "map" && isJsSourceFile(ts.getSourceFileOfNode(access))) { const probed = tryLowerExpression(lowerer, access.expression); if (probed?.type.kind === "map") { receiverIr = probed.type; @@ -427,7 +427,7 @@ export function lowerCollectionSpread( loc, }, then: entries, - else_: nodeThrowExpr(1, "", `${node.getText()} is not iterable`, entries.type, loc), + else_: nodeThrowExpr(1, "", `${ts.getTextOfNode(node)} is not iterable`, entries.type, loc), type: entries.type, loc, }, @@ -544,7 +544,7 @@ export function lowerSetMethodCall( // Set, but the VALUE lowered as a real Set of // identity tokens (the new-Set probe) — the lowered receiver's type // is the honest dispatch key. - if (receiverIr?.kind !== "set" && isJsSourceFile(access.getSourceFile())) { + if (receiverIr?.kind !== "set" && isJsSourceFile(ts.getSourceFileOfNode(access))) { const probed = tryLowerExpression(lowerer, access.expression); if (probed?.type.kind === "set") { receiverIr = probed.type; @@ -973,7 +973,7 @@ export function lowerSetSeedNew( { kind: "libCall", fn: "dyn.iterPack", - args: [source, strLit(node.getText(), loc)], + args: [source, strLit(ts.getTextOfNode(node), loc)], type: DYN, loc, }, @@ -1165,7 +1165,7 @@ export function lowerMapSeedNew( } if (ts.isArrayLiteralExpression(argNode)) { if ( - isJsSourceFile(argNode.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(argNode)) && argNode.elements.nodes.some((entry) => !ts.isArrayLiteralExpression(entry)) ) { return mapFromSeedValue(lowerer, lowerer.lowerExprExpecting(argNode, DYN), mapT); diff --git a/packages/compiler/src/frontend/lowering/containers/dynamic-receivers.ts b/packages/compiler/src/frontend/lowering/containers/dynamic-receivers.ts index 523fda46e..32f90bfe7 100644 --- a/packages/compiler/src/frontend/lowering/containers/dynamic-receivers.ts +++ b/packages/compiler/src/frontend/lowering/containers/dynamic-receivers.ts @@ -142,7 +142,7 @@ export function dynStringReceiver( args: [ recv, { kind: "strLit", value: access.name.text, type: STRING, loc }, - { kind: "strLit", value: access.getText(), type: STRING, loc }, + { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc }, ], type: STRING, loc, diff --git a/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts b/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts index f2967c298..397bf6ec8 100644 --- a/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts +++ b/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts @@ -29,7 +29,7 @@ export function execResultCaptureMask(lowerer: Lowerer, call: ts.CallExpression) const symbol = lowerer.resolveValueSymbol(decl.name); if (!symbol) return null; const read = new Set(); - for (const use of identifiersNamed(lowerer, decl.getSourceFile(), decl.name.text)) { + for (const use of identifiersNamed(lowerer, ts.getSourceFileOfNode(decl), decl.name.text)) { if (use === decl.name || lowerer.resolveValueSymbol(use) !== symbol) continue; const ref = outerValue(use); const parent = ref.parent; diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index 4017335ae..c23e6931c 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -684,7 +684,7 @@ function isHelperReceiver(lowerer: Lowerer, expr: ts.Expression): boolean { return ( mapped?.kind === "dyn" && symbol !== undefined && - ADAPTED_ITERATOR_TYPES.has(symbol.name) && + ADAPTED_ITERATOR_TYPES.has(ts.symbolName(symbol)) && lowerer.isStdlibSymbol(symbol) ); } diff --git a/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts b/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts index f936e27cb..794bd27c2 100644 --- a/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts +++ b/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts @@ -242,7 +242,7 @@ function constBindingReadsByElements( } function usesOf(lowerer: Lowerer, name: ts.Identifier, symbol: ts.Symbol): ts.Identifier[] { - return identifiersNamed(lowerer, name.getSourceFile(), name.text).filter( + return identifiersNamed(lowerer, ts.getSourceFileOfNode(name), name.text).filter( (use) => use !== name && lowerer.resolveValueSymbol(use) === symbol, ); } diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts index e3ca2270d..b80eb7ac0 100644 --- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts +++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts @@ -62,8 +62,9 @@ export class DeferredModuleInitializers { const info = exactClassOfReceiver(lowerer, owner); if ( info && - info.decl?.getSourceFile() === statement.getSourceFile() && - info.decl.getStart() < statement.getStart() && + info.decl !== null && + ts.getSourceFileOfNode(info.decl) === ts.getSourceFileOfNode(statement) && + ts.tokenStartOfNode(info.decl) < ts.tokenStartOfNode(statement) && !info.localClass && !info.classDecorators && !info.generic && @@ -101,7 +102,7 @@ export class DeferredModuleInitializers { loc: locOf(statement), }; const context = lowerer.ctx; - const moduleIndex = [...lowerer.initNameOf.keys()].indexOf(statement.getSourceFile()); + const moduleIndex = [...lowerer.initNameOf.keys()].indexOf(ts.getSourceFileOfNode(statement)); const initializers = this.pending.get(key) ?? []; initializers.push({ loc: block.loc, @@ -269,14 +270,14 @@ function registryInitializer( return false; expression = expression.arguments.nodes[0]!; } - const packageSource = npmStaticPackageOfPath(statement.getSourceFile().fileName) !== null; + const packageSource = npmStaticPackageOfPath(ts.getSourceFileOfNode(statement).fileName) !== null; const initialized = (declaration: ts.Node): boolean => { - const source = declaration.getSourceFile(); - return source === statement.getSourceFile() - ? declaration.getStart() < statement.getStart() + const source = ts.getSourceFileOfNode(declaration); + return source === ts.getSourceFileOfNode(statement) + ? ts.tokenStartOfNode(declaration) < ts.tokenStartOfNode(statement) : [...lowerer.initNameOf.keys()].indexOf(source) >= 0 && [...lowerer.initNameOf.keys()].indexOf(source) < - [...lowerer.initNameOf.keys()].indexOf(statement.getSourceFile()); + [...lowerer.initNameOf.keys()].indexOf(ts.getSourceFileOfNode(statement)); }; const callable = (callee: ts.Expression): boolean => { if (!ts.isIdentifier(callee)) return false; @@ -324,7 +325,7 @@ function registryInitializer( // bundlers. Arguments still need to be safe to omit: an annotation // never licenses dropping argument effects or a throwing property read. if (packageSource && (ts.isCallExpression(node) || ts.isNewExpression(node))) { - const trivia = node.getSourceFile().text.slice(node.pos, node.getStart()); + const trivia = ts.getSourceFileOfNode(node).text.slice(node.pos, ts.tokenStartOfNode(node)); if (!/[@#]__PURE__/.test(trivia)) return false; if (!callable(node.expression)) return false; return (node.arguments?.nodes ?? []).every((argument) => value(argument, depth + 1)); @@ -337,8 +338,8 @@ function registryInitializer( const info = exactClassOfReceiver(lowerer, node.expression.expression); return ( !!info?.decl && - info.decl.getSourceFile() === statement.getSourceFile() && - info.decl.getStart() < statement.getStart() && + ts.getSourceFileOfNode(info.decl) === ts.getSourceFileOfNode(statement) && + ts.tokenStartOfNode(info.decl) < ts.tokenStartOfNode(statement) && classWriteIsUnobserved(lowerer, info, statement) && (findMethodOn(lowerer, info, node.name.text) !== null || findGenericMethodOn(lowerer, info, node.name.text) !== null) @@ -398,7 +399,7 @@ function registryInitializer( if ( functionDeclaration && ts.isFunctionDeclaration(functionDeclaration) && - functionDeclaration.getSourceFile() === statement.getSourceFile() + ts.getSourceFileOfNode(functionDeclaration) === ts.getSourceFileOfNode(statement) ) return true; // Reading an earlier initialized binding executes none of its @@ -410,8 +411,8 @@ function registryInitializer( functionDeclaration && ts.isVariableDeclaration(functionDeclaration) && functionDeclaration.initializer && - functionDeclaration.getSourceFile() === statement.getSourceFile() && - functionDeclaration.getStart() < statement.getStart() + ts.getSourceFileOfNode(functionDeclaration) === ts.getSourceFileOfNode(statement) && + ts.tokenStartOfNode(functionDeclaration) < ts.tokenStartOfNode(statement) ) return true; const info = symbol && lowerer.classBySymbol.get(symbol); @@ -421,8 +422,8 @@ function registryInitializer( // decorated classes and local factories keep ordinary initialization. return ( !!declaration && - declaration.getSourceFile() === statement.getSourceFile() && - declaration.getStart() < statement.getStart() && + ts.getSourceFileOfNode(declaration) === ts.getSourceFileOfNode(statement) && + ts.tokenStartOfNode(declaration) < ts.tokenStartOfNode(statement) && ts.isClassDeclaration(declaration) && !info!.classDecorators && !info!.generic && @@ -476,19 +477,20 @@ function registryOwnerIsData( ) ) return false; - if (declaration.getSourceFile() !== statement.getSourceFile()) { + if (ts.getSourceFileOfNode(declaration) !== ts.getSourceFileOfNode(statement)) { const modules = [...lowerer.initNameOf.keys()]; return ( !!frozen && - modules.indexOf(declaration.getSourceFile()) >= 0 && - modules.indexOf(declaration.getSourceFile()) < modules.indexOf(statement.getSourceFile()) + modules.indexOf(ts.getSourceFileOfNode(declaration)) >= 0 && + modules.indexOf(ts.getSourceFileOfNode(declaration)) < + modules.indexOf(ts.getSourceFileOfNode(statement)) ); } - if (declaration.getStart() >= statement.getStart()) return false; + if (ts.tokenStartOfNode(declaration) >= ts.tokenStartOfNode(statement)) return false; if (frozen) return true; let safe = true; const visit = (node: ts.Node): void => { - if (!safe || node.getStart() >= statement.getStart()) return; + if (!safe || ts.tokenStartOfNode(node) >= ts.tokenStartOfNode(statement)) return; if ( ts.isIdentifier(node) && node !== declaration.name && @@ -515,7 +517,7 @@ function registryOwnerIsData( } ts.forEachChild(node, visit); }; - visit(statement.getSourceFile()); + visit(ts.getSourceFileOfNode(statement)); return safe; } @@ -527,7 +529,7 @@ function classWriteIsUnobserved( info: ClassInfo, statement: ts.Statement, ): boolean { - const source = statement.getSourceFile(); + const source = ts.getSourceFileOfNode(statement); const declaration = info.decl!; if ( declaration.members.nodes.some( @@ -543,7 +545,10 @@ function classWriteIsUnobserved( ) return false; for (const earlier of source.statements.nodes) { - if (earlier.getStart() <= declaration.getStart() || earlier.getStart() >= statement.getStart()) + if ( + ts.tokenStartOfNode(earlier) <= ts.tokenStartOfNode(declaration) || + ts.tokenStartOfNode(earlier) >= ts.tokenStartOfNode(statement) + ) continue; if ( ts.isFunctionDeclaration(earlier) || diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts index a77f33c6b..1c362cb09 100644 --- a/packages/compiler/src/frontend/lowering/divergences.ts +++ b/packages/compiler/src/frontend/lowering/divergences.ts @@ -281,7 +281,7 @@ export function scanDivergences( ts.isVariableDeclaration(decl) && decl.initializer !== undefined && ts.isObjectLiteralExpression(decl.initializer) && - decl.getSourceFile() === inner.getSourceFile() + ts.getSourceFileOfNode(decl) === ts.getSourceFileOfNode(inner) ) { lit = decl.initializer; binding = inner.text; @@ -291,7 +291,9 @@ export function scanDivergences( const contextual = safely(() => checker.getContextualType(lit!)); if (contextual === undefined) continue; if (namedObjectDeclaration(checker, contextual) === null) continue; - const declared = safely(() => checker.getPropertiesOfType(contextual).map((p) => p.name)); + const declared = safely(() => + checker.getPropertiesOfType(contextual).map((p) => ts.symbolName(p)), + ); if (declared === undefined) continue; const written = lit.properties.nodes.map((p) => propertyNameText(p)!); const assigned = @@ -463,7 +465,7 @@ function identityUse(root: ts.Node, name: string, from = -1): IdentityUse | null return cur; }; const textOf = (node: ts.Node): string => { - const text = node.getText(); + const text = ts.getTextOfNode(node); return text.length > 60 ? `${text.slice(0, 57)}...` : text; }; const visit = (node: ts.Node): "skip" | "stop" | undefined => { @@ -591,7 +593,7 @@ function elementWrite(root: ts.Node, name: string, from: number): string | null ) target = node.operand; if (target !== undefined && throughElement(target)) { - found = target.getText(); + found = ts.getTextOfNode(target); return "stop"; } if ( @@ -670,8 +672,8 @@ function widthCopyBetween( // still observes the same writes: no divergence to report. const accessor = ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor; if (sourceProps.some((p) => (p.flags & accessor) !== 0)) return null; - const sourceNames = new Set(sourceProps.map((p) => p.name)); - const targetNames = checker.getPropertiesOfType(t).map((p) => p.name); + const sourceNames = new Set(sourceProps.map((p) => ts.symbolName(p))); + const targetNames = checker.getPropertiesOfType(t).map((p) => ts.symbolName(p)); if (targetNames.length === sourceNames.size && targetNames.every((n) => sourceNames.has(n))) return null; } @@ -684,7 +686,7 @@ function isProgramClassType(checker: ts.TypeChecker, type: ts.Type): boolean { return ( symbol !== undefined && (symbol.flags & ts.SymbolFlags.Class) !== 0 && - symbol.declarations.some((d) => !d.getSourceFile().isDeclarationFile) + symbol.declarations.some((d) => !ts.getSourceFileOfNode(d).isDeclarationFile) ); } @@ -698,7 +700,7 @@ function isPlainRecordType(checker: ts.TypeChecker, type: ts.Type): boolean { if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Class) !== 0) return false; // Library-declared object types (Map, Date, Promise, ...) are not records. const decls = symbol === undefined ? [] : symbol.declarations; - if (decls.some((d) => d.getSourceFile().isDeclarationFile)) return false; + if (decls.some((d) => ts.getSourceFileOfNode(d).isDeclarationFile)) return false; return true; } @@ -711,7 +713,7 @@ function namedObjectDeclaration(checker: ts.TypeChecker, type: ts.Type): ts.Node if (symbol === undefined) return null; if ((symbol.flags & ts.SymbolFlags.Class) !== 0) return null; const decl = symbol.declarations[0]; - if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null; + if (decl === undefined || ts.getSourceFileOfNode(decl).isDeclarationFile) return null; return decl; } @@ -765,7 +767,7 @@ function classDeclarationOfTypeNode( const symbol = lowerer.checker.getSymbolAtLocation(node.typeName); if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) === 0) return null; const decl = symbol.declarations.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); - if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null; + if (decl === undefined || ts.getSourceFileOfNode(decl).isDeclarationFile) return null; return decl as ts.ClassLikeDeclaration; } @@ -776,7 +778,7 @@ function classDeclarationOfType( const symbol = type.getSymbol(); if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) === 0) return null; const decl = symbol.declarations.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d)); - if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null; + if (decl === undefined || ts.getSourceFileOfNode(decl).isDeclarationFile) return null; return decl as ts.ClassLikeDeclaration; } diff --git a/packages/compiler/src/frontend/lowering/dyn-streams.ts b/packages/compiler/src/frontend/lowering/dyn-streams.ts new file mode 100644 index 000000000..2ac09e8e5 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/dyn-streams.ts @@ -0,0 +1,326 @@ +/* Checked-dynamic stream slots (TypeMapperCtx.dynNodeStreams). + * + * Under the general stream representation (general-streams.ts), + * NodeJS.ReadableStream and NodeJS.WritableStream map to the runtime stream + * classes. JavaScript code also hands such slots its own stream objects: + * constructor-function streams over EventEmitter or the legacy Stream, plain + * objects implementing on/pipe/write/end, streams of bundled npm code. A + * program that moves an untyped value into a stream slot therefore lowers + * again with checked-dynamic slots: those two interfaces map to dyn, so a + * slot holds either a boxed native stream (its typed capsule, which unboxes + * to the same stream) or the JavaScript object itself, and a value returned + * to JavaScript is the object that came in. + * + * A member call on such a slot dispatches per value at run time. A native + * stream unboxes and takes the ordinary class-method lowering (the same + * stream surface a typed stream has); any other value calls its own member + * with JavaScript's dynamic semantics (dynInvoke). Programs without such a + * flow never take this pass, so their IR is unchanged. */ +import type { IrExpr, IrStmt, IrType } from "../../ir/ir.js"; +import { dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/build.js"; +import { DYN } from "../../ir/ir.js"; +import * as ts from "../ts7/adapter.js"; +import { locOf } from "../program.js"; +import { isDeclaredInAmbientNamespace } from "../type-mapper.js"; +import { resultIsDiscarded } from "./call-position.js"; +import { lowerObjectMethodCall } from "./lower-calls.js"; +import type { Lowerer } from "./lowerer.js"; + +/** The side a NodeJS stream interface names: "r" for NodeJS.ReadableStream, + * "w" for NodeJS.WritableStream (through null/undefined and intersections), + * or null for any other type. */ +export function nodeStreamInterfaceSide(lowerer: Lowerer, type: ts.Type): "r" | "w" | null { + const parts = (t: ts.Type): ts.Type[] => + (t.flags & ts.TypeFlags.Intersection) !== 0 ? ts.constituentTypes(t).flatMap(parts) : [t]; + for (const part of parts(lowerer.checker.getNonNullableType(type))) { + const psym = part.getSymbol() ?? part.getAliasSymbol(); + if ( + (psym === undefined || ts.symbolName(psym) !== "ReadableStream") && + (psym === undefined || ts.symbolName(psym) !== "WritableStream") + ) + continue; + const declared = psym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + lowerer.isStdlibFile(ts.getSourceFileOfNode(d)) && + isDeclaredInAmbientNamespace(d, "NodeJS"), + ); + if (declared) return ts.symbolName(psym) === "ReadableStream" ? "r" : "w"; + } + return null; +} + +/** The runtime class a native value of the side unboxes to. */ +export function nativeStreamClass(side: "r" | "w"): IrType { + return { kind: "object", className: side === "r" ? "%Readable" : "%Writable" }; +} + +/** The native stream of a checked-dynamic value (+1, typed as the side's + * class) or null: the stream.dynNative libCall, which tests the value's + * typed capsule without throwing. */ +function dynNativeStream( + lowerer: Lowerer, + value: IrExpr, + side: "r" | "w", + fn: "stream.dynNative" | "stream.dynNativePipe" = "stream.dynNative", +): IrExpr { + const native = nativeStreamClass(side); + return { + kind: "libCall", + fn, + args: [value, numLit(side === "r" ? 1 : 2, value.loc)], + type: { kind: "union", unionId: lowerer.unions.intern([native, { kind: "nullT" }]) }, + loc: value.loc, + }; +} + +/** Runs `lower` with `node` lowering to `value` and its member calls taking + * `type` as the receiver representation. */ +function withStreamReceiver( + lowerer: Lowerer, + node: ts.Expression, + value: IrExpr, + type: IrType, + lower: () => T, +): T { + const previous = lowerer.receiverTypeOverrides.get(node); + lowerer.receiverTypeOverrides.set(node, type); + try { + return lowerer.withExpressionOverride(node, value, lower); + } finally { + if (previous) lowerer.receiverTypeOverrides.set(node, previous); + else lowerer.receiverTypeOverrides.delete(node); + } +} + +/** `stream.member(args)` on a NodeJS.ReadableStream / NodeJS.WritableStream + * receiver under checked-dynamic stream slots, or null when the call is not + * one. */ +export function lowerDynStreamMemberCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (!lowerer.dynNodeStreams) return null; + // The native arm re-enters the call lowering with the receiver bound. + if (lowerer.chainRecvByNode.has(access.expression)) return null; + if (access.questionDotToken || call.questionDotToken || !ts.isIdentifier(access.name)) + return null; + if (call.arguments.nodes.some(ts.isSpreadElement)) return null; + const side = nodeStreamInterfaceSide(lowerer, lowerer.typeOf(access.expression)); + if (side === null) return null; + const loc = locOf(call); + const receiverValue = lowerer.lowerExpr(access.expression); + const receiver = lowerer.declareHiddenLocal("%streamReceiver", receiverValue.type); + const receiverRef = varRef(receiver.id, receiver.type, locOf(access.expression)); + const stmts: IrStmt[] = [{ kind: "varDecl", localId: receiver.id, init: receiverValue, loc }]; + const seq = (result: IrExpr): IrExpr => ({ + kind: "seqExpr", + stmts, + result, + type: result.type, + loc, + }); + // A typed stream whose declared type is one of the interfaces (a child + // stdio read): the class-method path on its own representation. + if (receiverValue.type.kind !== "dyn") { + const typed = + receiverValue.type.kind === "object" + ? withStreamReceiver(lowerer, access.expression, receiverRef, receiverValue.type, () => + lowerObjectMethodCall(lowerer, call, access), + ) + : lowerer.withExpressionOverride(access.expression, receiverRef, () => + lowerer.lowerCall(call), + ); + if (typed === null) lowerer.noLowering(`${access.name.text} on this stream`, call); + return seq(typed); + } + const nativeType = nativeStreamClass(side); + const nullable = dynNativeStream(lowerer, receiverRef, side); + const nativeOrNull = lowerer.declareHiddenLocal("%streamNative", nullable.type); + stmts.push({ kind: "varDecl", localId: nativeOrNull.id, init: nullable, loc }); + const unionId = nullable.type.kind === "union" ? nullable.type.unionId : ""; + const tag = lowerer.armTag(unionId, nativeType); + const native = lowerer.declareHiddenLocal("%streamNativeValue", nativeType); + const nativeRef = varRef(native.id, nativeType, locOf(access.expression)); + const nativeDecl: IrStmt = { + kind: "varDecl", + localId: native.id, + init: { + kind: "unionNarrow", + unionId, + tag, + value: varRef(nativeOrNull.id, nullable.type, loc), + type: nativeType, + loc, + }, + loc, + }; + const typed = withStreamReceiver(lowerer, access.expression, nativeRef, nativeType, () => + lowerObjectMethodCall(lowerer, call, access), + ); + if (typed === null) + lowerer.noLowering( + `${side === "r" ? "NodeJS.ReadableStream" : "NodeJS.WritableStream"}.${access.name.text}`, + call, + ); + // Any other value calls its own member, with the arguments as untyped + // values (typed callbacks box behind checked thunks). + const invoked: IrExpr = { + kind: "dynInvoke", + recv: receiverRef, + method: access.name.text, + calleeName: ts.getTextOfNode(access), + args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + type: DYN, + loc, + }; + const cond: IrExpr = { + kind: "unionIsTag", + unionId, + tag, + negated: false, + value: varRef(nativeOrNull.id, nullable.type, loc), + type: { kind: "bool" }, + loc, + }; + const resultType = resultIsDiscarded(call) ? null : lowerer.irTypeOf(call); + if (resultType === null || resultType.kind === "void") { + stmts.push({ + kind: "if", + cond, + then: [nativeDecl, { kind: "exprStmt", expr: typed, loc }], + else_: [{ kind: "exprStmt", expr: invoked, loc }], + loc, + }); + return seq(dynUndefinedExpr(loc)); + } + const result = lowerer.declareHiddenLocal("%streamResult", resultType); + result.mutable = true; + stmts.push( + { kind: "varDecl", localId: result.id, init: null, loc }, + { + kind: "if", + cond, + then: [ + nativeDecl, + { + kind: "assign", + localId: result.id, + value: lowerer.coerceInto(call, typed, resultType), + loc, + }, + ], + else_: [ + { + kind: "assign", + localId: result.id, + value: lowerer.coerceInto(call, invoked, resultType), + loc, + }, + ], + loc, + }, + ); + return seq(varRef(result.id, resultType, loc)); +} + +/** `for await (const chunk of stream)` over a checked-dynamic + * NodeJS.ReadableStream: a native stream iterates through the Readable + * lowering; any other value throws the not-supported Error (async + * iteration of JavaScript stream objects has no lowering). */ +export function lowerForAwaitDynStream( + lowerer: Lowerer, + stmt: ts.ForOfStatement, + lowerLoop: () => IrStmt | null, +): IrStmt | null { + if (!lowerer.dynNodeStreams || lowerer.chainRecvByNode.has(stmt.expression)) return null; + if (nodeStreamInterfaceSide(lowerer, lowerer.typeOf(stmt.expression)) !== "r") return null; + const loc = locOf(stmt.expression); + const value = lowerer.lowerExpr(stmt.expression); + const native: IrType = + value.type.kind === "dyn" ? { kind: "object", className: "%Duplex" } : value.type; + const local = lowerer.declareHiddenLocal("%streamIterated", native); + const init: IrExpr = + value.type.kind === "dyn" + ? { + kind: "libCall", + fn: "stream.unboxDyn", + args: [ + value, + strLit( + "for await over stream objects other than native streams is not supported yet", + loc, + ), + ], + type: native, + loc, + } + : value; + const loop = withStreamReceiver( + lowerer, + stmt.expression, + varRef(local.id, native, loc), + native, + lowerLoop, + ); + if (loop === null) return null; + return { + kind: "block", + body: [{ kind: "varDecl", localId: local.id, init, loc }, loop], + loc: locOf(stmt), + }; +} + +/** A statement that runs `onNative` with the native stream a + * checked-dynamic pipe or unpipe destination stands for (typed as the + * side's class), or `otherwise` for any other value. The destination's + * stream is a boxed native one or a constructor-function stream's + * (stream.dynNativePipe): only the runtime's pipe reads it, while member + * calls on a slot keep a constructor-function stream's own methods. */ +export function dynNativeBranch( + lowerer: Lowerer, + value: IrExpr, + side: "r" | "w", + onNative: (native: IrExpr) => IrStmt, + otherwise: IrStmt, +): IrStmt { + const loc = value.loc; + const nativeType = nativeStreamClass(side); + const nullable = dynNativeStream(lowerer, value, side, "stream.dynNativePipe"); + const nativeOrNull = lowerer.declareHiddenLocal("%streamNative", nullable.type); + const unionId = nullable.type.kind === "union" ? nullable.type.unionId : ""; + const tag = lowerer.armTag(unionId, nativeType); + const native = lowerer.declareHiddenLocal("%streamNativeValue", nativeType); + const nullableRef = varRef(nativeOrNull.id, nullable.type, loc); + return { + kind: "block", + body: [ + { kind: "varDecl", localId: nativeOrNull.id, init: nullable, loc }, + { + kind: "if", + cond: { + kind: "unionIsTag", + unionId, + tag, + negated: false, + value: nullableRef, + type: { kind: "bool" }, + loc, + }, + then: [ + { + kind: "varDecl", + localId: native.id, + init: { kind: "unionNarrow", unionId, tag, value: nullableRef, type: nativeType, loc }, + loc, + }, + onNative(varRef(native.id, nativeType, loc)), + ], + else_: [otherwise], + loc, + }, + ], + loc, + }; +} diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index d311d6d79..a80367dd0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -83,7 +83,7 @@ export function lowerArrayLiteral( // structurally equal checker types. Their native class ABIs still differ. if ( !expected && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && mapped?.kind === "array" && mapped.elem.kind === "classval" ) @@ -202,7 +202,7 @@ export function lowerArrayLiteral( // keep the checked-dynamic fallback below. if ( lowerer.implicitParamTypes !== null && - npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null && + npmStaticPackageOfPath(ts.getSourceFileOfNode(expr).fileName) !== null && (mapped === null || mapped.kind === "dyn" || (mapped.kind === "array" && mapped.elem.kind === "dyn")) && @@ -222,7 +222,7 @@ export function lowerArrayLiteral( mapped = arrayOf(first.type); } } - if (!expected && isJsSourceFile(expr.getSourceFile()) && mapped?.kind === "array") { + if (!expected && isJsSourceFile(ts.getSourceFileOfNode(expr)) && mapped?.kind === "array") { const elem = mapped.elem; const arms = elem.kind === "union" ? lowerer.unions.get(elem.unionId)?.arms : [elem]; if (arms?.length && arms.every((arm) => arm.kind === "classval")) mapped = DYN; @@ -234,7 +234,7 @@ export function lowerArrayLiteral( } if ( !expected && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.elements.nodes.some( (element) => ts.isSpreadElement(element) && @@ -247,7 +247,7 @@ export function lowerArrayLiteral( // only in a checked-dynamic array: a static element type would copy it. if ( !expected && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && mapped?.kind !== "dyn" && expr.elements.nodes.some( (element) => !ts.isSpreadElement(element) && jsHoldsDynObject(lowerer, element), @@ -269,7 +269,7 @@ export function lowerArrayLiteral( // real keys. A structural union can otherwise drop descriptor fields. if ( !expected && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && mapped?.kind === "array" && mapped.elem.kind === "union" && expr.elements.nodes.some(ts.isObjectLiteralExpression) @@ -316,7 +316,7 @@ export function lowerArrayLiteral( // to DYN wholesale now (mapType's dyn-element array rule), so the // literal IS the dyn array. if ( - isJsSourceFile(expr.getSourceFile()) || + isJsSourceFile(ts.getSourceFileOfNode(expr)) || mapped?.kind === "dyn" || lowerer.checkerAnyArray(expr) ) { diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 8ef631f60..ccf9dc8c0 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -531,7 +531,7 @@ export function lowerNativeFunctionAssignment( ): IrExpr | null { const target = expr.left; if ( - !isJsSourceFile(expr.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(expr)) || (!ts.isPropertyAccessExpression(target) && !ts.isElementAccessExpression(target)) || target.questionDotToken ) @@ -903,7 +903,7 @@ export function lowerCompoundValueToTarget( // catchable TypeError, as the expanded assignment does). if ( (target.type.kind === "dyn" || rhs.type.kind === "dyn") && - (isJsSourceFile(expr.getSourceFile()) || + (isJsSourceFile(ts.getSourceFileOfNode(expr)) || (lowerer.dynConvertible(read.type) && lowerer.dynConvertible(rhs.type))) ) { const left = lowerer.coerceInto(expr.left, read, DYN); @@ -1142,7 +1142,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr recv && (recv.type.kind === "dyn" || (recv.type.kind === "array" && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(recv.type))) ) { return lowerDynMemberAssignment( @@ -1534,7 +1534,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr // Numeric operators dispatch on ToNumeric's actual Number/BigInt // result, including values flowing through native callable properties. if ( - isJsSourceFile(expr.getSourceFile()) || + isJsSourceFile(ts.getSourceFileOfNode(expr)) || op === ts.SyntaxKind.PlusToken || [ ts.SyntaxKind.LessThanToken, @@ -1981,8 +1981,60 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr case ts.SyntaxKind.LessThanLessThanToken: case ts.SyntaxKind.GreaterThanGreaterThanToken: case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: { - if (!bothNum) + // Booleans take part as ToNumber converts them, true to 1 and false + // to 0: JavaScript code writes `isFn(a) ^ isFn(b)` and `flags | ok`. + // A hoisted JavaScript `var` can also hold undefined, and ToInt32 + // maps ToNumber(undefined) (NaN) and ToNumber(null) to 0. + const optionalBoolTag = (e: IrExpr): number => { + if (e.type.kind !== "union") return -1; + const arms = lowerer.unions.get(e.type.unionId)?.arms ?? []; + const ok = arms.every( + (a) => a.kind === "bool" || a.kind === "undefinedT" || a.kind === "nullT", + ); + return ok ? arms.findIndex((a) => a.kind === "bool") : -1; + }; + const numeric = (e: IrExpr): boolean => + e.type.kind === "f64" || e.type.kind === "bool" || optionalBoolTag(e) >= 0; + const boolBits = !bothNum && numeric(arithmeticLeft) && numeric(arithmeticRight); + if (!bothNum && !boolBits) operatorUnsupported(lowerer, expr, ts.tokenToString(op) ?? "?", [left.type, right.type]); + const bitPrefix: IrStmt[] = []; + const oneOrZero = (cond: IrExpr): IrExpr => ({ + kind: "ternary", + cond, + then: numLit(1, loc), + else_: numLit(0, loc), + type: F64, + loc, + }); + // The tag test reads an operand twice: when either side needs it, + // both sides evaluate once into temporaries, in source order. A left + // operand that is safe to repeat still takes a temporary when the + // right one does: the right operand runs first in the prefix, and it + // can assign what the left operand reads. + const stash = optionalBoolTag(arithmeticLeft) >= 0 || optionalBoolTag(arithmeticRight) >= 0; + const rightTemp = stash && !isSafeToRepeat(arithmeticRight); + const stable = (e: IrExpr, beforeTemp: boolean): IrExpr => { + if (!stash || (isSafeToRepeat(e) && !beforeTemp)) return e; + const tmp = lowerer.declareHiddenLocal("%bits", e.type); + bitPrefix.push({ kind: "varDecl", localId: tmp.id, init: e, loc }); + return varRef(tmp.id, e.type, loc); + }; + const bitOperand = (operand: IrExpr, beforeTemp: boolean): IrExpr => { + const e = stable(operand, beforeTemp); + if (e.type.kind === "bool") return oneOrZero(e); + const tag = optionalBoolTag(e); + if (tag < 0 || e.type.kind !== "union") return e; + const unionId = e.type.unionId; + return { + kind: "ternary", + cond: { kind: "unionIsTag", unionId, tag, negated: false, value: e, type: BOOL, loc }, + then: oneOrZero({ kind: "unionNarrow", unionId, tag, value: e, type: BOOL, loc }), + else_: numLit(0, loc), + type: F64, + loc, + }; + }; const bitOp = op === ts.SyntaxKind.AmpersandToken ? "&" @@ -1995,14 +2047,17 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr : op === ts.SyntaxKind.GreaterThanGreaterThanToken ? ">>" : ">>>"; - return { + const bits: IrExpr = { kind: "bin", op: bitOp, - left: arithmeticLeft, - right: arithmeticRight, + left: bitOperand(arithmeticLeft, rightTemp), + right: bitOperand(arithmeticRight, false), type: F64, loc, }; + return bitPrefix.length === 0 + ? bits + : { kind: "seqExpr", stmts: bitPrefix, result: bits, type: F64, loc }; } case ts.SyntaxKind.EqualsEqualsEqualsToken: case ts.SyntaxKind.ExclamationEqualsEqualsToken: @@ -2409,7 +2464,7 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE } if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && op === ts.SyntaxKind.AmpersandAmpersandToken && left.type.kind === "union" && lowerer.unions.get(left.type.unionId)?.arms.some((arm) => arm.kind === "bool") && @@ -2457,7 +2512,7 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE op === ts.SyntaxKind.AmpersandAmpersandToken && target === null && lowerer.implicitParamTypes !== null && - npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null && + npmStaticPackageOfPath(ts.getSourceFileOfNode(expr).fileName) !== null && (lowerer.typeOf(expr).flags & ts.TypeFlags.Any) !== 0 && left.type.kind === "union" ) { @@ -2618,7 +2673,7 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE // When both operands have native checked representations, preserve // the deciding value and short-circuit evaluation in that domain. if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(left.type) && lowerer.dynConvertible(right.type) ) { diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index f39b7c428..bede93072 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -231,11 +231,11 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre let receiverIr = neverTaintedJsType(lowerer, expr.expression, lowerer.typeOf(expr.expression)) ? null : lowerer.mapTypeOf(lowerer.typeOf(expr.expression)); - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { const receiver = tryLowerExpression(lowerer, expr.expression); if (receiver?.type.kind === "dyn") { const key = lowerer.lowerExprExpecting(expr.argumentExpression, DYN); - const optional = hasOptionalChainGuard(expr.expression); + const optional = hasOptionalChainGuard(expr.expression, lowerer); return lowerer.maybeNarrow( { kind: "dynKeyGet", @@ -316,7 +316,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre locOf(expr.argumentExpression), expr.argumentExpression, ); - if (isJsSourceFile(expr.getSourceFile()) && recv.type.kind === "bytes") { + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && recv.type.kind === "bytes") { return { kind: "dynKeyGet", value: lowerer.coerceInto(expr.expression, recv, DYN), @@ -378,7 +378,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre lowerer.lowerExpr(expr.argumentExpression), expr.argumentExpression, ); - const optional = hasOptionalChainGuard(expr.expression); + const optional = hasOptionalChainGuard(expr.expression, lowerer); return lowerer.maybeNarrow( { kind: "dynKeyGet", @@ -488,7 +488,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre ? lowerer.coerceToExpected(rawKey, DYN) : lowerRecordPropertyKey(lowerer, rawKey, expr.argumentExpression); if (key.type.kind === "string" || key.type.kind === "dyn") { - const opt = hasOptionalChainGuard(expr.expression); + const opt = hasOptionalChainGuard(expr.expression, lowerer); return lowerer.maybeNarrow( { kind: "dynKeyGet", @@ -565,7 +565,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre if ( receiver?.type.kind === "dyn" || (receiver && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && (receiver.type.kind === "union" || receiver.type.kind === "bytes") && lowerer.dynConvertible(receiver.type)) ) { @@ -769,7 +769,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre if ( arr.type.kind === "array" && (index.type.kind === "dyn" || - (isJsSourceFile(expr.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(expr)) && index.type.kind !== "f64" && lowerer.dynConvertible(index.type))) ) { @@ -822,7 +822,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre arr.type.kind === "array" && arr.kind === "arrIntrinsic" && arr.method === "slice" && - npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null + npmStaticPackageOfPath(ts.getSourceFileOfNode(expr).fileName) !== null ) { const safe = lowerSafeIndexRead(lowerer, arr, index, locOf(expr)); if (safe) return safe; @@ -1306,7 +1306,11 @@ export function lowerRecordKeyRead( ) { declared = shape.fields[0]!.type; } - if (!declared && isJsSourceFile(expr.getSourceFile()) && recordKeyResultOk(lowerer, shape, DYN)) + if ( + !declared && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && + recordKeyResultOk(lowerer, shape, DYN) + ) declared = DYN; if (!declared) { lowerer.unsupported( @@ -1425,7 +1429,7 @@ export function lowerElementCompoundParts( if (isKeyedFieldAccess(lowerer, target)) return { expr: lowerFieldCompoundValue(lowerer, target, op, expr.right, loc), stmts: null }; const checked = lowerer.lowerExpr(target.expression); - if (checked?.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) { + if (checked?.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const stmts: IrStmt[] = []; const save = (value: IrExpr, name: string): IrExpr => { const local = lowerer.declareHiddenLocal(name, value.type); @@ -1516,7 +1520,7 @@ export function lowerElementCompoundParts( const rhs = lowerer.lowerExpr(expr.right); const numericRhs = rhs.type.kind === "dyn" && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && receiver.type.kind === "bytes" ? { kind: "libCall" as const, fn: "dyn.toNumberCoerce" as const, args: [rhs], type: F64, loc } : lowerOptionalNumber(lowerer, rhs, loc); @@ -1566,7 +1570,7 @@ export function lowerElementCompoundParts( type: F64, loc, }; - } else if (valueType.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) { + } else if (valueType.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const left = lowerer.coerceToExpected(oldRef(), DYN), right = lowerer.coerceToExpected(rhs, DYN); computed = dynCompoundValue(op, left, right, loc); @@ -1700,7 +1704,7 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): loc: locOf(expr), }; if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && receiver.type.kind === "object" && lowerer.dynConvertible(receiver.type) ) { @@ -1724,7 +1728,7 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): loc: locOf(expr), }; } - if (checkedReceiver?.type.kind === "object" && isJsSourceFile(expr.getSourceFile())) { + if (checkedReceiver?.type.kind === "object" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const info = lowerer.classes.get(checkedReceiver.type.className); const key = recordKeyLiteralText(target.argumentExpression) ?? @@ -1739,7 +1743,7 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): } } let receiverIr = lowerer.mapTypeOf(lowerer.typeOf(target.expression)); - if (isJsSourceFile(expr.getSourceFile()) && checkedReceiver) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && checkedReceiver) { const represented = checkedReceiver.type.kind === "union" ? lowerer.stripUndefinedArm(checkedReceiver.type) diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index c2e5ddcbb..a7596cc6d 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -133,7 +133,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: ); } if (projection?.kind === "builtin-function") { - if (!isJsSourceFile(expr.getSourceFile())) { + if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) { const callable = lowerer.lowerBuiltinCallableValue( { module: projection.module, member: projection.member }, loc, @@ -149,7 +149,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // `EventEmitter`, `Readable` or `TypeError` (or a const alias of // one) as a value in a TypeScript file: the runtime class's class // object. JavaScript files keep the identity token. - if (!isJsSourceFile(expr.getSourceFile())) { + if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) { const runtimeClass = runtimeClassValueOf(lowerer, expr); if (runtimeClass) return lowerer.classValueRef(runtimeClass, expr); } @@ -187,7 +187,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: const clause = decl.parent.parent.parent; if ( (ts.isCaseClause(clause) || ts.isDefaultClause(clause)) && - !(expr.getStart() >= clause.getStart() && expr.end <= clause.end) + !(ts.tokenStartOfNode(expr) >= ts.tokenStartOfNode(clause) && expr.end <= clause.end) ) { lowerer.unsupported( "SC1090", @@ -211,6 +211,11 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: if (arithmetic && !runtimeOptionalUseOf(expr)) { return { kind: "varRef", localId: local.id, type: local.type, loc }; } + // A failed checked value in a fallback tail reads its dynamic value + // plainly: `undefined` is a value it can hold (checked-fallback.ts). + if ((active || captured) && lowerer.fallbackTaint !== null && local.type.kind === "dyn") { + return { kind: "varRef", localId: local.id, type: local.type, loc }; + } if (active || captured) { const use = runtimeOptionalUseOf(expr); if (use?.complex) { @@ -385,7 +390,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: (expr.text === "__dirname" || expr.text === "__filename") && lowerer.isStdlibGlobal(expr, expr.text) ) { - const sf = expr.getSourceFile(); + const sf = ts.getSourceFileOfNode(expr); if (isNodeEsmFile(sf, lowerer.program) && !/\.tsx?$/.test(sf.fileName)) { return { kind: "libCall", @@ -552,7 +557,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // JavaScript sources: an otherwise unsupported builtin VALUE is // the same identity-token story as stdlib globals above (the // harness adds worker_threads.Worker to its identity Set). - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { // EventEmitter and the stream classes are the runtime's untyped // constructors, so constructor functions can inherit from them. const classKind = builtinMemberClassKind(bi.module, bi.member); @@ -652,7 +657,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // type-mapper.ts pins the one concrete signature `(value: string) => // primitive`; direct calls `String(x)` never reach here — the call // lowering intercepts them with the wider static coercions). - const jsPrimitiveCtorSelector = isJsSourceFile(expr.getSourceFile()); + const jsPrimitiveCtorSelector = isJsSourceFile(ts.getSourceFileOfNode(expr)); const primitiveName = stdlibGlobalNameOf(lowerer, expr); if ( primitiveName === "String" || @@ -664,7 +669,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: } if (lowerer.isStdlibGlobal(expr, "ArrayBuffer")) return { kind: "libCall", fn: "arrayBuffer.constructor", args: [], type: DYN, loc }; - if (isJsSourceFile(expr.getSourceFile()) && lowerer.isStdlibGlobal(expr, "Array")) + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.isStdlibGlobal(expr, "Array")) return { kind: "libCall", fn: "dyn.arrayConstructor", args: [], type: DYN, loc }; if ( Object.values(BYTES_ELEMENT_NAME).includes(expr.text) && @@ -763,10 +768,10 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: expr, canonical, loc, - isJsSourceFile(expr.getSourceFile()), + isJsSourceFile(ts.getSourceFileOfNode(expr)), ); if (builtinValue !== null) return builtinValue; - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { return { kind: "strLit", value: `[builtin ${canonical}]`, type: STRING, loc }; } // The families with a WHY: each hint states what makes the @@ -986,7 +991,7 @@ function cjsExportAccessorRead(lowerer: Lowerer, ident: ts.Identifier): IrExpr | if ( binding !== undefined && (ts.isImportSpecifier(binding) || ts.isExportSpecifier(binding)) && - !isCjsJsFile(binding.getSourceFile(), lowerer.program) + !isCjsJsFile(ts.getSourceFileOfNode(binding), lowerer.program) ) { const snapshot = cjsAccessorSnapshotOf(lowerer, getter); if (snapshot !== null) diff --git a/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts b/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts index 7a4f9c557..e53d27aa9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts +++ b/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts @@ -44,7 +44,7 @@ export function literalFieldKeys(lowerer: Lowerer, keyNode: ts.Expression): stri export function isKeyedFieldAccess(lowerer: Lowerer, access: ts.ElementAccessExpression): boolean { if ( access.questionDotToken || - isJsSourceFile(access.getSourceFile()) || + isJsSourceFile(ts.getSourceFileOfNode(access)) || lowerer.isProcessEnv(access.expression) ) return false; diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index 9f98ac2db..a70f0dfa9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -10,6 +10,7 @@ import { type IrLocal, type IrType, NULL_T, + STRING, type SrcLoc, UNDEFINED_T, arrayOf, @@ -25,7 +26,10 @@ import { hasRuntimeStatics } from "../class-runtime-statics.js"; import { jsBindingHasOpenWrites } from "../lower-stmts.js"; import { narrowClassUnion, narrowStoredClassValue, structuralNarrowing } from "../class-unions.js"; import { checkedClassAssertion, checkedClassUnionAssertion } from "../class-assertions.js"; -import { isOptionalProcessStreamProperty } from "../builtins/process.js"; +import { + isOptionalProcessStreamProperty, + isOptionalProcessVersionsRead, +} from "../builtins/process.js"; import { requestGeneralStreamSurface } from "../general-streams.js"; import { literalValues } from "../../literal-values.js"; import { literalUnionArm } from "../../union-discriminants.js"; @@ -92,7 +96,7 @@ export function lowerTernary( ) { falseArmNarrows.push(n); } - n.forEachChild(collect); + ts.forEachChild(n, collect); }; collect(expr.whenFalse); } @@ -333,7 +337,7 @@ export function lowerTernary( // call ABI. A checker join can erase a rest/arguments pack even though // a later call supplies additional observable arguments. const callableJoin = - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && thenRaw.type.kind === "func" && elseRaw.type.kind === "func" && !typeEquals(thenRaw.type, elseRaw.type) && @@ -343,7 +347,7 @@ export function lowerTernary( // a conditional's type even after that field acquired other values. // A checked arm must retain its actual representation at the join. const checkedJsJoin = - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && (thenRaw.type.kind === "dyn" || elseRaw.type.kind === "dyn") && lowerer.dynConvertible(thenRaw.type) && lowerer.dynConvertible(elseRaw.type); @@ -385,7 +389,7 @@ export function lowerTernary( export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrExpr { if ( (expr.type.kind === "union" || expr.type.kind === "dyn") && - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && ts.isPropertyAccessExpression(node) ) { const symbol = lowerer.checker.getSymbolAtLocation(node.name); @@ -396,7 +400,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx ts.isPropertyDeclaration(decl) && !decl.initializer && !decl.type && - isJsSourceFile(decl.getSourceFile()), + isJsSourceFile(ts.getSourceFileOfNode(decl)), ) ) { // Inference describes later writes, not the initial undefined. @@ -413,10 +417,13 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx // instead of misreading the payload (the dyn boundary's usual stance). // Object/array narrowings stay dyn-typed and keep their fences. if (expr.type.kind === "dyn") { + // A process.versions entry declared as a string can be absent here. + if (isOptionalProcessVersionsRead(lowerer, node)) + return lowerer.coerceToExpected(expr, lowerer.withUndefinedArm(STRING)); // An inferred JS member's storage may be deliberately wider than its // declaration (prototype-selected buffers, null-initialized scratch // objects). Extract at the consuming operation, not at the read. - if (isJsSourceFile(node.getSourceFile()) && ts.isPropertyAccessExpression(node)) { + if (isJsSourceFile(ts.getSourceFileOfNode(node)) && ts.isPropertyAccessExpression(node)) { const receiver = lowerer.mapTypeOf(lowerer.typeOf(node.expression)); if (receiver?.kind === "object") { const info = lowerer.classes.get(receiver.className); @@ -432,7 +439,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx } } if ( - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) ) { let receiver = node.expression; @@ -461,7 +468,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx if (bound && lowerer.mapTypeOf(bound)?.kind === "dyn") return expr; } } - if (isJsSourceFile(node.getSourceFile()) && ts.isIdentifier(node)) { + if (isJsSourceFile(ts.getSourceFileOfNode(node)) && ts.isIdentifier(node)) { const symbol = lowerer.resolveValueSymbol(node); if ( symbol && @@ -495,7 +502,9 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx decl.initializer && !decl.type && !/@(?:param|type)\b/.test( - decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()), + ts + .getSourceFileOfNode(decl) + .text.slice(decl.parent?.pos ?? decl.pos, ts.tokenStartOfNode(decl)), ), ) ) @@ -522,7 +531,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx // has no such property. Preserve the checked read for JS capability // probes instead of treating a declaration as a runtime type guard. if ( - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && ts.isPropertyAccessExpression(node) && ["isTTY", "columns", "rows"].includes(node.name.text) && lowerer.isStdlibMember(node) @@ -532,7 +541,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx // checker flow calls its non-null branch never. The live checked // value remains authoritative; never is not a numeric guard. if ( - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && (lowerer.typeOf(node).flags & ts.TypeFlags.Never) !== 0 ) return expr; @@ -589,6 +598,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx if (expr.type.kind !== "union") return expr; if (ts.isPropertyAccessExpression(node) && isOptionalProcessStreamProperty(lowerer, node)) return expr; + if (isOptionalProcessVersionsRead(lowerer, node)) return expr; // Represented fields and index values can include undefined even when // checker inference or lib.d.ts declares strings (RegExp captures). @@ -839,11 +849,11 @@ export function v8CalleeText(node: ts.Node): string { const object = v8CalleeText(node.expression); return ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key) ? `${object}.${key.text}` - : `${object}[${key.getText()}]`; + : `${object}[${ts.getTextOfNode(key)}]`; } if (ts.isPropertyAccessExpression(node) && !node.questionDotToken) return `${v8CalleeText(node.expression)}.${node.name.text}`; - return node.getText(); + return ts.getTextOfNode(node); } /** Node's TypeError for the operation consuming `node` when it holds a diff --git a/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts b/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts index e3b9bf025..51b7f4028 100644 --- a/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts +++ b/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts @@ -25,7 +25,7 @@ export function lowerNativeProxy(lowerer: Lowerer, expr: ts.NewExpression): IrEx if (args.length !== 2 || args.some(ts.isSpreadElement)) lowerer.noLowering("new Proxy with this argument list", expr); const handler = args[1]!; - if (isJsSourceFile(expr.getSourceFile()) && ts.isObjectLiteralExpression(handler)) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && ts.isObjectLiteralExpression(handler)) { for (const prop of handler.properties.nodes) { const callback = ts.isMethodDeclaration(prop) ? prop diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 77c4c8fc5..e276326c9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -276,7 +276,7 @@ export function lowerDynObjectLiteral( const raw = builtin === "path" || builtin === "path/posix" || builtin === "path/win32" ? pathModuleValue(lowerer, builtin === "path" ? "path/posix" : builtin, locOf(prop)) - : isJsSourceFile(expr.getSourceFile()) + : isJsSourceFile(ts.getSourceFileOfNode(expr)) ? lowerer.lowerExprExpecting(prop.expression, DYN) : lowerer.lowerExpr(prop.expression); fenceSymbolFieldCopy(lowerer, prop.expression, raw.type); @@ -410,7 +410,7 @@ export function lowerDynObjectLiteral( // Repeated references must compare equal and mutations remain shared. if ( !boxValue && - isJsSourceFile(prop.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(prop)) && v.kind === "dynFrom" && v.value.type.kind === "array" ) { @@ -495,7 +495,7 @@ function fenceClosureProbe( slotType: IrType | undefined, attempt: () => IrExpr, ): IrExpr | null { - if (!isJsSourceFile(node.getSourceFile())) return null; + if (!isJsSourceFile(ts.getSourceFileOfNode(node))) return null; let inner: ts.Node = node; while (ts.isParenthesizedExpression(inner)) inner = inner.expression; if ( @@ -580,7 +580,10 @@ function literalUnionArmOf( if (own.isUnionType()) return null; props.length = 0; for (const property of lowerer.checker.getPropertiesOfType(own)) { - props.push({ name: property.name, type: lowerer.checker.getTypeOfSymbol(property) }); + props.push({ + name: ts.symbolName(property), + type: lowerer.checker.getTypeOfSymbol(property), + }); } } /** Cheap literal discriminants avoid checker traffic for unrelated arms. @@ -766,7 +769,7 @@ export function lowerObjectLiteral( // Preserve their checked arrays instead of assigning never[] a numeric ABI. if ( !expected && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.properties.nodes.some( (prop) => ts.isPropertyAssignment(prop) && @@ -779,7 +782,7 @@ export function lowerObjectLiteral( } if ( !expected && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.properties.nodes.some( (prop) => ts.isPropertyAssignment(prop) && @@ -794,7 +797,7 @@ export function lowerObjectLiteral( // the represented binding rather than checking it back into that type. if ( !expected && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.properties.nodes.some((prop) => { let value = ts.isPropertyAssignment(prop) ? prop.initializer @@ -822,7 +825,7 @@ export function lowerObjectLiteral( // JavaScript object methods/accessors carry a live receiver. Keep the // literal in checked native storage instead of erasing its prototype ABI. if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && (expr.properties.nodes.length === 0 || expr.properties.nodes.some( (prop) => @@ -838,7 +841,7 @@ export function lowerObjectLiteral( // binding. Construct in that representation: an intermediate record would // check nested aliases back into inferred shapes and lose their identity. if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && ts.isVariableDeclaration(expr.parent) && expr.parent.initializer === expr && ts.isIdentifier(expr.parent.name) && @@ -850,7 +853,7 @@ export function lowerObjectLiteral( // methods absent from its inferred record shape. Copy the real properties. if ( !expected && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.properties.nodes.some( (property) => ts.isSpreadAssignment(property) && @@ -870,7 +873,7 @@ export function lowerObjectLiteral( // values. TypeScript keeps the record world and its fence: this shape // only arises in checked-dynamic JS. if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.properties.nodes.some((p) => { const n = ts.isSpreadAssignment(p) ? undefined : p.name; return n !== undefined && ts.isComputedPropertyName(n); @@ -917,7 +920,7 @@ export function lowerObjectLiteral( // receiver (capturing the record under construction would be an // RC cycle), and the generic lexical-this walk would incorrectly // capture an ENCLOSING method's `this` — the object-method rule. - if (!isJsSourceFile(expr.getSourceFile())) { + if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) { rejectThisInObjectAccessor(lowerer, prop.body ?? prop); } } @@ -993,7 +996,7 @@ export function lowerObjectLiteral( // `null`, which maps to nothing on its own). if ( tsType.isUnionType() && - ts.constituentTypes(tsType).some((t) => t.getSymbol()?.name === "PromiseLike") + ts.constituentTypes(tsType).some((t) => ts.symbolNameOf(t.getSymbol()) === "PromiseLike") ) { tsType = lowerer.checker.getAwaitedType(tsType) ?? tsType; } @@ -1014,7 +1017,7 @@ export function lowerObjectLiteral( // A JavaScript call can contextually type an object literal as string even // though the literal itself is a record. Keep its own shape so a caller // performing ToString can convert it after the value is built. - if (mapped?.kind === "string" && isJsSourceFile(expr.getSourceFile())) { + if (mapped?.kind === "string" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const own = lowerer.mapTypeOf(lowerer.typeOf(expr)); if (own?.kind === "record") mapped = own; } @@ -1147,7 +1150,7 @@ export function lowerObjectLiteral( // (rest-param function types) narrow away. SEMANTICS.md documents the // enumeration divergence. if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && (!mapped || expr.properties.nodes.some( (p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p), @@ -1187,7 +1190,7 @@ export function lowerObjectLiteral( // consumers ride the keyed-dyn paths. TypeScript keeps the fence. if ( mapped?.kind === "dyn" || - (!mapped && isJsSourceFile(expr.getSourceFile())) || + (!mapped && isJsSourceFile(ts.getSourceFileOfNode(expr))) || (!expected && mapped && jsOpenObjectType(expr, mapped, lowerer.shapes, lowerer.unions).kind === "dyn") @@ -1203,7 +1206,7 @@ export function lowerObjectLiteral( // extra fields, spreads, accessors, and keyed shapes retain their paths. if ( expected && - !isJsSourceFile(expr.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.properties.nodes.every( (p) => ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p), ) @@ -1255,7 +1258,7 @@ export function lowerObjectLiteral( // JavaScript Dates carry native object identity in checked storage. An // inferred scalar Date field would erase that identity before boxing. if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && shape.fields.some((field) => field.type.kind === "date") ) { return lowerDynObjectLiteral(lowerer, expr); @@ -1265,7 +1268,7 @@ export function lowerObjectLiteral( // so the literal's shape NARROWS to its plain fields (reads resolve // through the lifted accessors; Object.keys over the value omits // accessor names — the documented divergence). - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { const accessorNames = new Set( expr.properties.nodes .filter((p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p)) @@ -1301,7 +1304,7 @@ export function lowerObjectLiteral( ); if ( topLevelJsDecl && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && assignedProperties.length === expr.properties.nodes.length ) { const provided = new Set(assignedProperties.map((prop) => propNameText(lowerer, prop.name))); @@ -1328,7 +1331,7 @@ export function lowerObjectLiteral( // multiple spreads, spread-after-explicit order, and shape-changing width // copies retain their existing lowering and diagnostics. if ( - !isJsSourceFile(expr.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(expr)) && !shape.indexValue && !shape.tuple && !shapeHasAccessorSlots(shape) && @@ -2219,7 +2222,7 @@ export function lowerObjectLiteral( // closures is side-effect-free, so their position among the data // fields' source-order evaluation is unobservable — exactly JS, // where accessor definitions evaluate nothing. - if (isJsSourceFile(expr.getSourceFile())) continue; + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) continue; const name = propNameText(lowerer, prop.name); // key-form-checked above const slotName = `${ts.isGetAccessorDeclaration(prop) ? "%get" : "%set"}:${name}`; const slotT = fieldTypes.get(slotName); @@ -2341,7 +2344,7 @@ export function lowerObjectLiteral( if ( !(err instanceof PoisonError) || !pureMember || - !isJsSourceFile(expr.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(expr)) || lowerer.diagSink !== null || lowerer.diags.length <= propDiagsBefore || lowerer.diags.slice(propDiagsBefore).some((d) => d.code === "SC9001") @@ -2436,7 +2439,7 @@ export function lowerObjectLiteral( * their identity. Changing a discriminator or adding variant-specific * fields needs a different result layout and stays on the ordinary path. */ function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpression): IrExpr | null { - if (isJsSourceFile(expr.getSourceFile())) return null; + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) return null; const spread = expr.properties.nodes[0]; if (!spread || !ts.isSpreadAssignment(spread)) return null; let sourceNode = spread.expression; @@ -3190,6 +3193,22 @@ export function lowerShorthandValue( `generic functions as values (call '${propName.text}' directly)`, ); } + // A program class name (`{ parse, TomlError }`, an imported class + // included): the class object, exactly the long form `{ C: C }`. + // Generic classes keep the identifier path's contextual pinning. + const classInfo = lowerer.classBySymbol.get(resolved); + if (classInfo && !classInfo.generic) { + const decoratedGlobal = classInfo.classDecorators?.valueGlobalId; + if (decoratedGlobal !== undefined) { + return { + kind: "varRef", + localId: decoratedGlobal, + type: { kind: "classval", className: classInfo.def.name }, + loc, + }; + } + return lowerer.classValueRef(classInfo, propName); + } } lowerer.rejectUnresolvedSymbol( symbol diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index 995514a4d..fbcecdc32 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -92,13 +92,17 @@ export function isRequireMainFilename(lowerer: Lowerer, expr: ts.Expression): bo const main = expr.expression; if (!ts.isPropertyAccessExpression(main) || main.name.text !== "main") return false; if (!lowerer.isStdlibGlobal(main.expression, "require")) return false; - return !isNodeEsmFile(expr.getSourceFile(), lowerer.program); + return !isNodeEsmFile(ts.getSourceFileOfNode(expr), lowerer.program); } export function isOptionalChainTail(lowerer: Lowerer, expr: ts.Expression): boolean { const tail = chainTailDot(lowerer, expr); if (!tail) return false; - const recvT = lowerer.mapTypeOf(lowerer.typeOf(tail.expression)); + const recvTs = lowerer.typeOf(tail.expression); + // An `any` receiver short-circuits the whole tail too: `a?.b.c` reads + // `.c` normally once `a` is present. + if (recvTs.flags & ts.TypeFlags.Any) return true; + const recvT = lowerer.mapTypeOf(recvTs); if (recvT?.kind === "dyn") return true; if (recvT?.kind === "union") { const def = lowerer.unions.get(recvT.unionId); @@ -119,7 +123,7 @@ export function isOptionalChainTail(lowerer: Lowerer, expr: ts.Expression): bool * throwing. Walks the receiver spine only; the argument of the access * itself is not part of the guard. Parentheses end the chain * (`(a?.b).c` reads `.c` from undefined); `!` does not. */ -export function hasOptionalChainGuard(expr: ts.Expression): boolean { +export function hasOptionalChainGuard(expr: ts.Expression, lowerer?: Lowerer): boolean { let cur: ts.Expression = expr; for (;;) { if (ts.isNonNullExpression(cur)) { @@ -131,7 +135,11 @@ export function hasOptionalChainGuard(expr: ts.Expression): boolean { ts.isElementAccessExpression(cur) || ts.isCallExpression(cur) ) { - if (cur.questionDotToken) return true; + // Inside an optional chain's guarded body (dynamic reads pass the + // lowerer), the enclosing optChain already short-circuits on the + // consumed token, so the steps above it read normally: `a?.b.c` + // throws when `a.b` is undefined. + if (cur.questionDotToken) return lowerer === undefined || !lowerer.chainHandled.has(cur); cur = cur.expression; continue; } @@ -257,14 +265,14 @@ export function lowerOptionalChain( const spreads: { arg: number; what: string }[] = []; const args = expr.arguments.nodes.map((arg, index) => { if (!ts.isSpreadElement(arg)) return lowerer.lowerExprExpecting(arg, DYN); - spreads.push({ arg: index, what: arg.expression.getText() }); + spreads.push({ arg: index, what: ts.getTextOfNode(arg.expression) }); return lowerer.lowerExprExpecting(arg.expression, DYN); }); const body: IrExpr = { kind: "dynCall", callee, ...(thisValue ? { receiver: thisValue } : {}), - calleeName: recvNode.getText(), + calleeName: ts.getTextOfNode(recvNode), args, ...(spreads.length ? { spreads } : {}), type: DYN, diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index 65c8fbcbc..7fe5061ae 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -206,7 +206,7 @@ function isParseArgsDynProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpress const sym = lowerer.checker.getSymbolAtLocation(expr.name); if (!sym) return false; return sym.declarations.some((d) => { - if (!lowerer.isStdlibFile(d.getSourceFile())) return false; + if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) return false; let inParseArgsType = false; for (let node: ts.Node | undefined = d.parent; node; node = node.parent) { if ( @@ -234,7 +234,7 @@ function isStatFsDynProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression return ( !!symbol && symbol.declarations.some((declaration) => { - if (!lowerer.isStdlibFile(declaration.getSourceFile())) return false; + if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(declaration))) return false; const parent = declaration.parent; if (!ts.isInterfaceDeclaration(parent)) return false; if ( @@ -540,7 +540,7 @@ export function lowerPropertyAccessExpr( if ( represented && isMatchSliceType(lowerer, represented.type) && - (lowerer.isStdlibMember(expr) || isJsSourceFile(expr.getSourceFile())) + (lowerer.isStdlibMember(expr) || isJsSourceFile(ts.getSourceFileOfNode(expr))) ) { const recv = represented; const key: IrExpr = { kind: "strLit", value: expr.name.text, type: STRING, loc }; @@ -573,7 +573,7 @@ export function lowerPropertyAccessExpr( ts.isIdentifier(expr.expression) && expr.expression.text === "arguments" && !lowerer.ctx.argumentsLocal && - lowerer.typeOf(expr.expression).getSymbol()?.name === "IArguments" && + ts.symbolNameOf(lowerer.typeOf(expr.expression).getSymbol()) === "IArguments" && lowerer.isStdlibSymbol(lowerer.typeOf(expr.expression).getSymbol()) ) { let fn: ts.Node | undefined = expr.parent; @@ -644,9 +644,9 @@ export function lowerPropertyAccessExpr( if ( !expr.questionDotToken && isModuleExportsAccess(expr) && - isCjsJsFile(expr.getSourceFile(), lowerer.program) + isCjsJsFile(ts.getSourceFileOfNode(expr), lowerer.program) ) { - const whole = cjsClassExprWholeExportOf(expr.getSourceFile()); + const whole = cjsClassExprWholeExportOf(ts.getSourceFileOfNode(expr)); if (whole) { let inBody = false; for ( @@ -659,7 +659,7 @@ export function lowerPropertyAccessExpr( break; } } - if (inBody || expr.getStart() > whole.stmt.getEnd()) { + if (inBody || ts.tokenStartOfNode(expr) > whole.stmt.end) { return lowerer.classValueRef(lowerer.lowerClassExpressionInfo(whole.classExpr), expr); } } @@ -806,7 +806,7 @@ export function lowerPropertyAccessExpr( if ( !expr.questionDotToken && stdlibGlobalNameOf(lowerer, expr.expression) === "globalThis" && - isJsSourceFile(expr.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(expr)) ) { const canonical = stdlibGlobalNameOf(lowerer, expr); if (canonical !== null) { @@ -854,7 +854,7 @@ export function lowerPropertyAccessExpr( // them, and calls dispatch through the dyn method machinery // instead. if ( - (isJsSourceFile(expr.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(expr)) && (lowerer.mapTypeOf(lowerer.typeOf(expr.expression)) === null || (lowerer.typeOf(expr.expression).flags & ts.TypeFlags.Never) !== 0 || // A never-tainted receiver type maps (never rides as f64) but @@ -873,7 +873,7 @@ export function lowerPropertyAccessExpr( type: STRING, loc: locOf(expr.name), }; - const opt = hasOptionalChainGuard(expr.expression); + const opt = hasOptionalChainGuard(expr.expression, lowerer); return lowerer.maybeNarrow( { kind: "dynKeyGet", @@ -895,7 +895,7 @@ export function lowerPropertyAccessExpr( : { kind: "strIntrinsic", method: "length", receiver: recv, args: [], type: F64, loc }; } } - if (isJsSourceFile(expr.getSourceFile()) && expr.name.text === "constructor") { + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.name.text === "constructor") { const receiver = tryLowerExpression(lowerer, expr.expression); if ( receiver?.type.kind === "dyn" || @@ -959,7 +959,7 @@ export function lowerPropertyAccessExpr( // storage can expose its own name and length in both source languages. if ( !["apply", "bind", "call", "toString", "caller", "arguments"].includes(expr.name.text) && - (isJsSourceFile(expr.getSourceFile()) || + (isJsSourceFile(ts.getSourceFileOfNode(expr)) || expr.name.text === "name" || expr.name.text === "length" || expr.name.text === "constructor") @@ -972,7 +972,9 @@ export function lowerPropertyAccessExpr( const receiverSymbol = receiverType.getSymbol(); const checkedFunction = lowerer.checker.getCallSignatures(receiverType).length > 0 || - (receiverSymbol?.name === "Function" && lowerer.isStdlibSymbol(receiverSymbol)); + (receiverSymbol !== undefined && + ts.symbolName(receiverSymbol) === "Function" && + lowerer.isStdlibSymbol(receiverSymbol)); if (probed?.type.kind === "dyn" && checkedFunction) { const key: IrExpr = { kind: "strLit", @@ -980,7 +982,7 @@ export function lowerPropertyAccessExpr( type: STRING, loc: locOf(expr.name), }; - const opt = hasOptionalChainGuard(expr.expression); + const opt = hasOptionalChainGuard(expr.expression, lowerer); return lowerer.maybeNarrow( { kind: "dynKeyGet", @@ -994,7 +996,7 @@ export function lowerPropertyAccessExpr( ); } if ( - (isJsSourceFile(expr.getSourceFile()) || expr.name.text === "constructor") && + (isJsSourceFile(ts.getSourceFileOfNode(expr)) || expr.name.text === "constructor") && probed?.type.kind === "func" && canBoxFuncIntoDyn( probed.type, @@ -1028,17 +1030,17 @@ export function lowerPropertyAccessExpr( if (expr.name.text === "done" || expr.name.text === "value") { const receiverType = lowerer.typeOf(expr.expression); const recvSym = - receiverType.getAliasSymbol()?.name === "IteratorResult" + ts.symbolNameOf(receiverType.getAliasSymbol()) === "IteratorResult" ? receiverType.getAliasSymbol() : receiverType.getSymbol(); if ( - (recvSym?.name === "IteratorResult" || - recvSym?.name === "IteratorYieldResult" || - recvSym?.name === "IteratorReturnResult") && + ((recvSym !== undefined && ts.symbolName(recvSym) === "IteratorResult") || + (recvSym !== undefined && ts.symbolName(recvSym) === "IteratorYieldResult") || + (recvSym !== undefined && ts.symbolName(recvSym) === "IteratorReturnResult")) && recvSym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) && - lowerer.isStdlibFile(d.getSourceFile()), + lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { const recv = lowerer.lowerExpr(expr.expression); @@ -1089,7 +1091,8 @@ export function lowerPropertyAccessExpr( // members owned by these implemented families bypass the stdlib fence. const segmentDataSymbol = lowerer.typeOf(expr.expression).getSymbol(); const segmentDataProperty = - segmentDataSymbol?.name === "SegmentData" && + segmentDataSymbol !== undefined && + ts.symbolName(segmentDataSymbol) === "SegmentData" && lowerer.isStdlibSymbol(segmentDataSymbol) && ["segment", "index", "input", "isWordLike"].includes(expr.name.text); // os.constants members vary by platform (the declared groups list @@ -1111,7 +1114,7 @@ export function lowerPropertyAccessExpr( type: STRING, loc: locOf(expr.name), }; - const opt = hasOptionalChainGuard(expr.expression); + const opt = hasOptionalChainGuard(expr.expression, lowerer); const read: IrExpr = { kind: "dynKeyGet", key, @@ -1196,7 +1199,7 @@ export function lowerPropertyAccessExpr( // validated dynCheck as usual. if ( recvLowered.type.kind === "dyn" || - (isJsSourceFile(expr.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(expr)) && (["f64", "bool", "bigint", "string", "symbol"].includes(recvLowered.type.kind) || (recvLowered.type.kind === "union" && lowerer.dynConvertible(recvLowered.type)))) ) { @@ -1206,7 +1209,7 @@ export function lowerPropertyAccessExpr( type: STRING, loc: locOf(expr.name), }; - const opt = hasOptionalChainGuard(expr.expression); + const opt = hasOptionalChainGuard(expr.expression, lowerer); return lowerer.maybeNarrow( { kind: "dynKeyGet", @@ -1264,8 +1267,8 @@ export function lowerPropertyAccessExpr( recvShape && !recvShape.tuple && lowerer.implicitParamTypes !== null && - isJsSourceFile(expr.getSourceFile()) && - npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null + isJsSourceFile(ts.getSourceFileOfNode(expr)) && + npmStaticPackageOfPath(ts.getSourceFileOfNode(expr).fileName) !== null ) { return { kind: "seqExpr", @@ -1356,6 +1359,27 @@ export function lowerPropertyAccessExpr( } const asserted = assertedMemberRead(lowerer, expr, recvLowered, loc); if (asserted) return asserted; + // A checker-`any` receiver whose value is a FRESH typed result of an + // earlier step in the chain (`out.split(" ").length`, where split + // extracted the string and answered a native string[]): the member read + // goes through the boxed checked-dynamic value, as the method-call twin + // does. The box copies; a call result has no other holder to observe it. + if ( + (lowerer.typeOf(expr.expression).flags & ts.TypeFlags.Any) !== 0 && + isFreshCallResult(expr.expression) && + lowerer.dynConvertible(recvLowered.type) + ) { + return lowerer.maybeNarrow( + { + kind: "dynKeyGet", + value: lowerer.coerceToExpected(recvLowered, DYN), + key: { kind: "strLit", value: expr.name.text, type: STRING, loc }, + type: DYN, + loc, + }, + expr, + ); + } lowerer.unsupported( "SC1090", expr, @@ -1363,6 +1387,13 @@ export function lowerPropertyAccessExpr( ); } +/** A call expression, through parentheses: its value has no other holder. */ +function isFreshCallResult(node: ts.Expression): boolean { + let e = node; + while (ts.isParenthesizedExpression(e)) e = e.expression; + return ts.isCallExpression(e); +} + /** `(source as { repoName: string }).repoName` where the assertion names a * member some arms of the stored union of records do not declare: Node * reads the property off whichever object it is, so arms that declare it @@ -1484,7 +1515,12 @@ export function lowerIntrinsicProperty( | "BigInt" )[]) { if (!lowerer.chainBlocked(expr) && lowerer.isStdlibGlobal(expr, name)) { - return primitiveCtorClosure(lowerer, name, locOf(expr), isJsSourceFile(expr.getSourceFile())); + return primitiveCtorClosure( + lowerer, + name, + locOf(expr), + isJsSourceFile(ts.getSourceFileOfNode(expr)), + ); } } if (lowerer.stdlibGlobalMember(expr, "Reflect") === "ownKeys") { @@ -1662,7 +1698,7 @@ export function lowerIntrinsicProperty( ) return null; const name = expr.name.text; - if (kind === "array" && name === "constructor" && isJsSourceFile(expr.getSourceFile())) { + if (kind === "array" && name === "constructor" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const receiver = lowerer.lowerExpr(expr.expression); if (receiver.type.kind === "dyn") return { @@ -1690,7 +1726,7 @@ export function lowerIntrinsicProperty( const strictReceiver = (expected: IrType): IrExpr => { const value = lowerer.lowerExpr(expr.expression); if (value.type.kind === "dyn") { - if (isJsSourceFile(expr.getSourceFile()) && name === "length") return value; + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && name === "length") return value; return lowerer.coerceInto(expr.expression, value, expected); } return lowerer.runtimeOptionalPropertyReceiver(expr.expression, value, expected, name) ?? value; @@ -2210,7 +2246,7 @@ function lowerReplacedExportsRead( if (expr.questionDotToken) return null; const recv = expr.expression; if (!ts.isIdentifier(recv) || recv.text !== "exports") return null; - const sf = expr.getSourceFile(); + const sf = ts.getSourceFileOfNode(expr); if (!isJsSourceFile(sf) || isNodeEsmFile(sf, lowerer.program)) return null; if (lowerer.peekLocal(recv) || lowerer.globalOf(recv)) return null; // a user binding shadows // Write position is the export-assignment machinery's territory. @@ -2247,7 +2283,7 @@ function lowerCjsExportTableThisMember( expr: ts.PropertyAccessExpression, ): IrExpr | null { if (expr.expression.kind !== ts.SyntaxKind.ThisKeyword || expr.questionDotToken) return null; - if (!isJsSourceFile(expr.getSourceFile())) return null; + if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null; // The enclosing accessor, crossing only arrow boundaries (arrows // inherit `this`; any other function form rebinds it). let n: ts.Node | undefined = expr.parent; @@ -2447,7 +2483,8 @@ function lowerMatchGroupsRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressio const groups = matchResultNamedGroupsOf(lowerer, expr.expression); // An untraced regex: TypeScript reaches the member fence; JavaScript // would otherwise read the slice's absent expando as undefined. - if (groups === null && isJsSourceFile(expr.getSourceFile())) lowerer.stdlibMemberFence(expr); + if (groups === null && isJsSourceFile(ts.getSourceFileOfNode(expr))) + lowerer.stdlibMemberFence(expr); if (groups === null) return null; if (groups.length === 0) { // Node: a match result of a group-less regex answers undefined — @@ -2464,7 +2501,7 @@ function lowerMatchGroupsRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressio } const recv = lowerer.lowerExpr(expr.expression); // A JavaScript callback parameter holds the row as a dynamic array. - const dynRow = recv.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile()); + const dynRow = recv.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr)); if (!dynRow && !isMatchSliceType(lowerer, recv.type)) return null; return lowerGroupsProjection(lowerer, recv, groups, loc); } @@ -2690,7 +2727,7 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi kind: "dynKeyGet", value, key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) }, - ...(expr.questionDotToken || hasOptionalChainGuard(expr.expression) + ...(expr.questionDotToken || hasOptionalChainGuard(expr.expression, lowerer) ? { optional: true as const } : {}), type: DYN, @@ -3041,12 +3078,12 @@ export function lowerUnionProperty( // instance argument (e.g. Matrix4.makeTranslation). Preserve primitive // missing-property semantics and dispatch class data through its native // capsule instead of demanding a field shared by every union arm. - if (isJsSourceFile(expr.getSourceFile()) && lowerer.dynConvertible(value.type)) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(value.type)) { return { kind: "dynKeyGet", value: lowerer.coerceToExpected(value, DYN), key, - ...(hasOptionalChainGuard(expr.expression) ? { optional: true as const } : {}), + ...(hasOptionalChainGuard(expr.expression, lowerer) ? { optional: true as const } : {}), type: DYN, loc: locOf(expr), }; @@ -3193,7 +3230,7 @@ export function classFieldTarget( const represented = saved ?? lowerer.lowerExpr(receiverNode); if ( represented.type.kind === "dyn" && - isJsSourceFile(receiverNode.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(receiverNode)) && !fieldName.startsWith("#") ) { return { container: "dynamic", obj: represented, field: fieldName, fieldType: DYN }; @@ -3240,7 +3277,7 @@ export function classFieldTarget( if (fieldType) { const obj = lowerObjectReceiver(); if ( - isJsSourceFile(receiverNode.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(receiverNode)) && !fieldName.startsWith("#") && [...lowerer.classes.values()].some( (candidate) => @@ -3278,7 +3315,7 @@ export function classFieldTarget( fieldType: getF ? getF.sig.ret : setF!.sig.params[0]!.type, }; } - if (info.decl && isJsSourceFile(info.decl.getSourceFile())) { + if (info.decl && isJsSourceFile(ts.getSourceFileOfNode(info.decl))) { const property = lowerer.checker.getPropertyOfType( lowerer.checker.getTypeAtLocation(receiverNode), fieldName, @@ -3497,7 +3534,7 @@ export function fieldTarget( : [declared]; return declaredArms.every((a) => ivArms.some((b) => typeEquals(a, b))); }; - if (!nameSym || nameSym.name === ts.InternalSymbolName.Index || canonicalized()) { + if (!nameSym || ts.symbolName(nameSym) === ts.InternalSymbolName.Index || canonicalized()) { let obj = lowerer.lowerExpr(access.expression); if (obj.type.kind === "union" && lowerer.armTag(obj.type.unionId, UNDEFINED_T) >= 0) { const present = lowerer.stripUndefinedArm(obj.type); @@ -3609,7 +3646,7 @@ export function fieldGetExpr( } if ( !target.field.startsWith("#") && - isJsSourceFile(blame.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(blame)) && lowerer.dynConvertible(getF.sig.ret) ) { const value: IrExpr = { @@ -3891,7 +3928,9 @@ export function builtinGlobalValueAt( )); if (reflects) { const name = - owner !== null ? `${owner}.${callee.name.text}` : callee.getText().replace(/\s+/g, ""); + owner !== null + ? `${owner}.${callee.name.text}` + : ts.getTextOfNode(callee).replace(/\s+/g, ""); lowerer.noLowering( `${name}(${canonical}, ...)`, expr, @@ -3934,8 +3973,8 @@ function fenceBuiltinConstructorStaticRead( const symbol = lowerer.typeOf(access.expression).getSymbol(); if ( symbol !== undefined && - symbol.name.endsWith("Constructor") && - GLOBAL_CONSTRUCTOR_VALUES.has(symbol.name.slice(0, -"Constructor".length)) && + ts.symbolName(symbol).endsWith("Constructor") && + GLOBAL_CONSTRUCTOR_VALUES.has(ts.symbolName(symbol).slice(0, -"Constructor".length)) && lowerer.isStdlibSymbol(symbol) ) lowerer.stdlibMemberFence(access); @@ -4012,7 +4051,7 @@ function jsFunctionExpandoMember(lowerer: Lowerer, access: ts.PropertyAccessExpr return ( decls.length > 0 && decls.every((d) => { - if (!isJsSourceFile(d.getSourceFile())) return false; + if (!isJsSourceFile(ts.getSourceFileOfNode(d))) return false; const assign = ts.isBinaryExpression(d) ? d : d.parent; return ( assign !== undefined && diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 91d1468c2..f8b8c7604 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -470,7 +470,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc ); } const constructor = tryLowerExpression(lowerer, expr.right); - if (constructor?.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) { + if (constructor?.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); return checkedClassInstanceOf(lowerer, value, constructor, loc); } @@ -478,7 +478,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc // function's prototype against the value's prototype chain. if ( constructor?.type.kind === "func" && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(constructor.type) ) { const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); @@ -1080,7 +1080,11 @@ function lowerPrivateIn( // when the value is a class instance at or below the declarer. A typed // reference carries the class it was boxed under: the declarer or a // descendant answers true; an ancestor answers per instance. - if (recv.type.kind === "dyn" && recv.kind === "varRef" && isJsSourceFile(expr.getSourceFile())) { + if ( + recv.type.kind === "dyn" && + recv.kind === "varRef" && + isJsSourceFile(ts.getSourceFileOfNode(expr)) + ) { // A function-local class has one brand per evaluation. if (info.localClass) { const owner = localPrivateBrandClass(lowerer, priv); @@ -1381,7 +1385,7 @@ export function lowerInExpression( recv.type.kind === "classval" ? lowerer.classes.get(recv.type.className) : undefined; if ( (classInfo && (classInfo.callableBase || hasRuntimeStatics(classInfo))) || - (isJsSourceFile(expr.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(expr)) && recv.type.kind === "func" && lowerer.dynConvertible(recv.type)) ) { diff --git a/packages/compiler/src/frontend/lowering/expressions/typeof.ts b/packages/compiler/src/frontend/lowering/expressions/typeof.ts index eaa6b231d..cc3ac06fb 100644 --- a/packages/compiler/src/frontend/lowering/expressions/typeof.ts +++ b/packages/compiler/src/frontend/lowering/expressions/typeof.ts @@ -71,7 +71,7 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc (operand.text === "__dirname" || operand.text === "__filename") && lowerer.isStdlibGlobal(operand, operand.text) ) { - const sf = operand.getSourceFile(); + const sf = ts.getSourceFileOfNode(operand); if (isNodeEsmFile(sf, lowerer.program) && !/\.tsx?$/.test(sf.fileName)) return { kind: "strLit", value: "undefined", type: STRING, loc }; } @@ -219,7 +219,7 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc // its own lowering as an identity token (a string), so the operand's // representation cannot answer. Node's answer follows the export's // kind, and the binding read has no effects. - const imported = isJsSourceFile(tested.getSourceFile()) + const imported = isJsSourceFile(ts.getSourceFileOfNode(tested)) ? lowerer.builtinImportOf(tested) : null; if (imported && builtinModuleConstOf(lowerer, imported.module, imported.member) === undefined) { diff --git a/packages/compiler/src/frontend/lowering/expressions/union-fields.ts b/packages/compiler/src/frontend/lowering/expressions/union-fields.ts index f2dd0d0f0..6454bc1fd 100644 --- a/packages/compiler/src/frontend/lowering/expressions/union-fields.ts +++ b/packages/compiler/src/frontend/lowering/expressions/union-fields.ts @@ -117,7 +117,7 @@ export function lowerUnionFieldRead( access: ts.PropertyAccessExpression, receiver: IrExpr, ): IrExpr | null { - if (receiver.type.kind !== "union" || isJsSourceFile(access.getSourceFile())) return null; + if (receiver.type.kind !== "union" || isJsSourceFile(ts.getSourceFileOfNode(access))) return null; const unionId = receiver.type.unionId; const arms = lowerer.unions.get(unionId)?.arms; if (!arms || arms.length < 2) return null; diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts index 41d8f6332..104057906 100644 --- a/packages/compiler/src/frontend/lowering/expressions/updates.ts +++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts @@ -110,7 +110,7 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio } // Match the checked numeric boundary used by JS binary arithmetic. const operand = - raw.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile()) + raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr)) ? { kind: "dynCheck" as const, value: raw, type: F64, loc } : lowerOptionalNumber(lowerer, raw, loc, expr.operand); if (operand.type.kind !== "f64") operatorUnsupported(lowerer, expr, "-", [raw.type]); @@ -263,7 +263,7 @@ export function lowerIncDec( const receiver = save(raw, "%incrementArray"); const rawIndex = lowerer.lowerExpr(access.argumentExpression); const index = save( - rawIndex.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile()) + rawIndex.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr)) ? { kind: "libCall", fn: "dyn.toNumberCoerce", args: [rawIndex], type: F64, loc } : rawIndex, "%incrementIndex", @@ -308,10 +308,13 @@ export function lowerIncDec( // class-backed protocol view): the expanded form `o.v = o.v + 1`, its // read checked into number like any other read of the member. const checkedNumber = - !isJsSourceFile(expr.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(expr)) && raw.type.kind === "dyn" && lowerer.mapTypeOf(lowerer.typeOf(access))?.kind === "f64"; - if ((isJsSourceFile(expr.getSourceFile()) || checkedNumber) && raw.type.kind === "dyn") { + if ( + (isJsSourceFile(ts.getSourceFileOfNode(expr)) || checkedNumber) && + raw.type.kind === "dyn" + ) { const receiver = save(raw, "%incrementReceiver"); const key = save( ts.isElementAccessExpression(access) @@ -352,7 +355,7 @@ export function lowerIncDec( ), ); } - if (isJsSourceFile(expr.getSourceFile()) && lowerer.dynConvertible(raw.type)) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(raw.type)) { const receiver = save(lowerer.coerceInto(access.expression, raw, DYN), "%incrementReceiver"); const key = save( ts.isElementAccessExpression(access) @@ -818,7 +821,7 @@ export function lowerFieldCompoundValue( rhsNode ? lowerer.lowerExpr(rhsNode) : { kind: "numLit", value: 1, type: F64, loc }, "%compoundRhs", ); - if (isJsSourceFile(access.getSourceFile()) && lowerer.dynConvertible(rhs.type)) { + if (isJsSourceFile(ts.getSourceFileOfNode(access)) && lowerer.dynConvertible(rhs.type)) { const numeric = ["-", "*", "/", "%", "**"].includes(op); const right = lowerer.coerceToExpected(rhs, DYN); const value = save( @@ -912,7 +915,7 @@ export function lowerFieldCompoundValue( "%compoundRhs", ); const numericRhs: IrExpr = - rhs.type.kind === "dyn" && isJsSourceFile(access.getSourceFile()) + rhs.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(access)) ? { kind: "dynCheck", value: rhs, type: F64, loc: rhs.loc } : lowerOptionalNumber(lowerer, rhs, loc); let value: IrExpr; @@ -927,7 +930,7 @@ export function lowerFieldCompoundValue( } else if (bigintValue) { value = bigintValue; } else if ( - (target.fieldType.kind === "dyn" && isJsSourceFile(access.getSourceFile())) || + (target.fieldType.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(access))) || // An untyped right operand: the expanded `o.f = o.f op rhs`; the field // write below checks the result into the declared field type. (rhs.type.kind === "dyn" && diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index c0e70013c..9500d6d2d 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -98,8 +98,8 @@ export function appendImplicitUndefinedReturn( /** Whether the source explicitly documents a return contract. */ export function hasExplicitJsDocReturn(decl: ts.SignatureDeclaration): boolean { - const sourceFile = decl.getSourceFile(); - return /@returns?\b/.test(sourceFile.text.slice(decl.pos, decl.getStart(sourceFile))); + const sourceFile = ts.getSourceFileOfNode(decl); + return /@returns?\b/.test(sourceFile.text.slice(decl.pos, ts.tokenStartOfNode(decl))); } export function promiseCarriesDyn(lowerer: Lowerer, type: IrType): boolean { @@ -130,14 +130,14 @@ export function declaredReturnType( decl: ts.SignatureDeclaration, blame: ts.Node, ): IrType { - if (isJsSourceFile(decl.getSourceFile()) && ts.isGetAccessorDeclaration(decl)) return DYN; + if (isJsSourceFile(ts.getSourceFileOfNode(decl)) && ts.isGetAccessorDeclaration(decl)) return DYN; const sig = lowerer.checker.getSignatureFromDeclaration(decl); if (!sig) lowerer.unsupported("SC1090", decl, "this function form"); const retTsType = lowerer.checker.getReturnTypeOfSignature(sig); // An inferred null/undefined return in JS can read a mutable checked-value // field. Keep the actual value instead of restricting it to the initializer. if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && (retTsType.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0 ) return DYN; @@ -154,7 +154,7 @@ export function declaredReturnType( ) mappedReturn = mapAsyncGeneratorAnnotation(retTsType, lowerer.typeCtx); if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) && !decl.asteriskToken && mappedReturn?.kind !== "promise" @@ -162,7 +162,11 @@ export function declaredReturnType( const fallback = dynFallbackType(lowerer, decl, retTsType); return fallback?.kind === "promise" ? fallback : { kind: "promise", inner: DYN }; } - if (mappedReturn && isJsSourceFile(decl.getSourceFile()) && functionCanReturnUndefined(decl)) + if ( + mappedReturn && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && + functionCanReturnUndefined(decl) + ) mappedReturn = lowerer.withUndefinedArmOf(mappedReturn) ?? mappedReturn; // A generic-class method body that can return an indexed read that // misses returns undefined there, whatever the declared result says. @@ -171,33 +175,33 @@ export function declaredReturnType( mappedReturn.kind === "promise" ? { kind: "promise", inner: lowerer.runtimeOptionalType(mappedReturn.inner) } : lowerer.runtimeOptionalType(mappedReturn); - if (isJsSourceFile(decl.getSourceFile()) && mappedReturn?.kind === "void") { + if (isJsSourceFile(ts.getSourceFileOfNode(decl)) && mappedReturn?.kind === "void") { let returnsValue = false; const scan = (node: ts.Node): void => { if (node !== decl && ts.isFunctionLike(node)) return; if (ts.isReturnStatement(node) && node.expression) returnsValue = true; - node.forEachChild(scan); + ts.forEachChild(node, scan); }; scan(decl); if (returnsValue) return DYN; } - if (isJsSourceFile(decl.getSourceFile()) && mappedReturn?.kind === "array") return DYN; + if (isJsSourceFile(ts.getSourceFileOfNode(decl)) && mappedReturn?.kind === "array") return DYN; if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasExplicitJsDocReturn(decl) && mappedReturn?.kind === "record" ) return DYN; if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasExplicitJsDocReturn(decl) && mappedReturn?.kind === "date" ) return DYN; if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasExplicitJsDocReturn(decl) && mappedReturn?.kind === "union" && @@ -207,7 +211,7 @@ export function declaredReturnType( // JS inference assigns Number to arithmetic over any, although the // runtime operands can both be BigInts. Preserve that native result. if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasExplicitJsDocReturn(decl) && mappedReturn?.kind === "f64" @@ -237,13 +241,13 @@ export function declaredReturnType( ) ) numericAnyReturn = true; - node.forEachChild(check); + ts.forEachChild(node, check); }; check(decl); if (numericAnyReturn) return DYN; } if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !hasExplicitJsDocReturn(decl) && mappedReturn !== null && promiseCarriesDyn(lowerer, mappedReturn) && @@ -264,7 +268,7 @@ export function declaredReturnType( // value rides its own box, calls go through the boxed thunk (JS // arity), and typed slots re-check with dynCheck as usual. if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && decl.type === undefined && lowerer.mapTypeOf(retTsType)?.kind === "func" ) { @@ -304,7 +308,7 @@ export function declaredReturnType( (arm.kind === "object" && !hasExplicitJsDocReturn(decl)), ); if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && decl.type === undefined && recordReturn !== null && recordReturn !== undefined && @@ -468,5 +472,5 @@ export function blockBodyOf(decl: ts.FunctionLikeDeclaration): ts.Block | null { const body = decl.body; if (body === undefined) return null; if (ts.isBlock(body)) return body; - return decl.forEachChild((c) => (ts.isBlock(c) ? c : undefined)) ?? null; + return ts.forEachChild(decl, (c) => (ts.isBlock(c) ? c : undefined)) ?? null; } diff --git a/packages/compiler/src/frontend/lowering/general-streams.ts b/packages/compiler/src/frontend/lowering/general-streams.ts index 0113a9958..d31439465 100644 --- a/packages/compiler/src/frontend/lowering/general-streams.ts +++ b/packages/compiler/src/frontend/lowering/general-streams.ts @@ -88,9 +88,11 @@ export function stdioIntoGeneralStream( ): IrExpr | null { if (!lowerer.generalNodeStreams || expr.type.kind !== "procStream") return null; const writable: IrType = { kind: "object", className: "%Writable" }; + // Checked-dynamic stream slots (dyn-streams.ts) hold the Writable boxed. const accepts = (expected.kind === "object" && expected.className === "%Writable") || - (expected.kind === "union" && lowerer.armTag(expected.unionId, writable) >= 0); + (expected.kind === "union" && lowerer.armTag(expected.unionId, writable) >= 0) || + (expected.kind === "dyn" && lowerer.dynNodeStreams); if (!accepts) return null; return { kind: "libCall", @@ -112,3 +114,23 @@ export function requestGeneralStreamSurface(lowerer: Lowerer, receiver: ts.Expre if (isGeneralNodeStreamInterface(type, lowerer.typeCtx)) lowerer.requiresGeneralNodeStreams = true; } + +/** Under the general representation, an untyped value reaching a runtime + * stream class slot (the checked unwrap) or an untyped pipe/pipeline + * stage: the program lowers again with checked-dynamic stream slots + * (dyn-streams.ts), where NodeJS.ReadableStream / NodeJS.WritableStream + * hold JavaScript stream objects too. */ +export function noteUntypedStreamFlow(lowerer: Lowerer, expected: IrType | null): void { + if (lowerer.dynNodeStreams) return; + if (!lowerer.generalNodeStreams) { + // An untyped pipe or pipeline stage needs both: the next pass lowers + // with the general representation and checked-dynamic slots at once. + if (expected === null) { + lowerer.requiresGeneralNodeStreams = true; + lowerer.requiresDynNodeStreams = true; + } + return; + } + if (expected === null || containsPart(lowerer, expected, (p) => isGeneralStream(lowerer, p))) + lowerer.requiresDynNodeStreams = true; +} diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index b586884c5..880cccc8b 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -225,7 +225,7 @@ export function collectGenericSignature(lowerer: Lowerer, decl: ts.FunctionDecla 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(), }); @@ -306,7 +306,7 @@ export function genericCallInstance( // untyped value, such as an untyped forwarding call) is checked-dynamic. const mappedReturn = lowerer.mapTypeOf(retTs) ?? - ((retTs.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(info.decl.getSourceFile()) + ((retTs.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(ts.getSourceFileOfNode(info.decl)) ? DYN : null); if (!mappedReturn) { @@ -552,7 +552,7 @@ function internGenericInstance( return JSON.stringify(tsBound.value); } const bound = bindings.get(tp); - return bound ? lowerer.fmt(bound) : tp.name; + return bound ? lowerer.fmt(bound) : ts.symbolName(tp); }) .join(", "); // Deep polymorphic recursion renders unbounded types — keep messages sane. @@ -699,7 +699,8 @@ function unifySignatureBindings( if (!bindings.has(sym)) { const mapped = lowerer.mapTypeOf(inst) ?? - ((inst.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(info.decl.getSourceFile()) + ((inst.flags & ts.TypeFlags.Any) !== 0 && + !isJsSourceFile(ts.getSourceFileOfNode(info.decl)) ? DYN : null); // A visitor's inferred T can be void. It still binds the @@ -811,7 +812,7 @@ function unifySignatureBindings( } if (declared.flags & ts.TypeFlags.Object) { for (const dp of lowerer.checker.getPropertiesOfType(declared)) { - const ip = lowerer.checker.getPropertyOfType(inst, dp.name); + const ip = lowerer.checker.getPropertyOfType(inst, ts.symbolName(dp)); if (ip) unify( lowerer.checker.getTypeOfSymbol(dp), @@ -1171,7 +1172,7 @@ function genericValueInstance( const rendered = info.typeParams .map((tp) => { const bound = bindings.get(tp); - return bound ? lowerer.fmt(bound) : tp.name; + return bound ? lowerer.fmt(bound) : ts.symbolName(tp); }) .join(", "); lowerer.typeParamBindings = bindings; @@ -1270,7 +1271,7 @@ function paramWrittenInBody( hit = true; return; } - m.forEachChild(scan); + ts.forEachChild(m, scan); }; scan(n); return hit; @@ -1305,7 +1306,7 @@ function paramWrittenInBody( written = true; return; } - n.forEachChild(walk); + ts.forEachChild(n, walk); }; walk(body); return written; @@ -1480,7 +1481,7 @@ export function implicitAnyParamSymbolsOf( }); // Destructured object parameters monomorphize too (implicitPatternsOf); // their slots stay null here. - if (!hasImplicitParams && isJsSourceFile(decl.getSourceFile())) + if (!hasImplicitParams && isJsSourceFile(ts.getSourceFileOfNode(decl))) hasImplicitParams = implicitPatternsOf(lowerer, decl).some((pattern) => pattern !== null); return hasImplicitParams ? out : null; } @@ -1630,7 +1631,7 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType | // declaredReturnType widens non-generic JavaScript functions. if ( mapped !== null && - isJsSourceFile(info.decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(info.decl)) && functionCanReturnUndefined(info.decl) ) return lowerer.withUndefinedArmOf(mapped) ?? mapped; @@ -1678,7 +1679,7 @@ function bindableImplicitIr(t: IrType | null): t is IrType { function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrType | null { while (ts.isParenthesizedExpression(arg)) arg = arg.expression; if ( - isJsSourceFile(arg.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(arg)) && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) ) { const saved = lowerer.diagSink; @@ -1701,7 +1702,7 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp } if ( ts.isObjectLiteralExpression(arg) && - isJsSourceFile(arg.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(arg)) && (arg.properties.nodes.length === 0 || arg.properties.nodes.some( (property) => @@ -1716,7 +1717,7 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp if (ts.isIdentifier(arg)) return lowerer.peekLocal(arg)?.type ?? lowerer.globalOf(arg)?.type ?? null; if ( - isJsSourceFile(arg.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(arg)) && (ts.isPropertyAccessExpression(arg) || ts.isElementAccessExpression(arg)) ) { // Mutable JS members can retain an initializer-only checker type (for @@ -1726,7 +1727,7 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp if (represented) return represented.type; } if ( - isJsSourceFile(arg.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(arg)) && (ts.isCallExpression(arg) || ts.isConditionalExpression(arg) || (ts.isBinaryExpression(arg) && @@ -1843,7 +1844,7 @@ export function implicitCallInstance( (neverTaintedJsType(lowerer, arg, t) ? null : lowerer.mapTypeOf(t)) ?? dynFallbackType(lowerer, arg, t); if ( - isJsSourceFile(arg.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(arg)) && ts.isArrayLiteralExpression(arg) && mapped?.kind === "array" && (mapped.elem.kind === "classval" || @@ -1868,7 +1869,7 @@ export function implicitCallInstance( // preserve represented arms through the checked boundary instead // of specializing a call to a union that would discard them. if ( - isJsSourceFile(arg.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(arg)) && mapped?.kind === "func" && !lowerer.dynConvertible(mapped) ) { @@ -2066,7 +2067,7 @@ export function implicitLocalFnNodeOf( if (cached !== undefined) return cached ? (cached.decl as ts.FunctionExpression | ts.ArrowFunction) : null; const probe = (): ts.FunctionExpression | ts.ArrowFunction | null => { - if (!implicitMonoFile(decl.getSourceFile())) return null; + if (!implicitMonoFile(ts.getSourceFileOfNode(decl))) return null; if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null; let init: ts.Expression = decl.initializer; while (ts.isParenthesizedExpression(init)) init = init.expression; @@ -2097,7 +2098,7 @@ export function implicitLocalFnNodeOf( const d = s ? s.valueDeclaration : undefined; if ( d && - d.getSourceFile() === decl.getSourceFile() && + ts.getSourceFileOfNode(d) === ts.getSourceFileOfNode(decl) && !(d.pos >= init.pos && d.end <= init.end) ) { // Declared outside the initializer, in this file: a capture @@ -2116,7 +2117,7 @@ export function implicitLocalFnNodeOf( } } } - n.forEachChild(scan); + ts.forEachChild(n, scan); }; scan(init.body); return captures ? null : init; @@ -2154,8 +2155,8 @@ export function implicitLocalFnInfoOf( decl: fnNode, baseName: name, qualifiedName: lowerer.qualify( - decl.getSourceFile(), - nsPathPrefix(stmt ?? decl, decl) + `${name}%l${decl.getStart()}`, + ts.getSourceFileOfNode(decl), + nsPathPrefix(stmt ?? decl, decl) + `${name}%l${ts.tokenStartOfNode(decl)}`, ), typeParams: [], instances: new Map(), @@ -2249,7 +2250,10 @@ export function objLitGenericFnInfoOf( const info: GenericFnInfo = { decl: fnNode, baseName: name, - qualifiedName: lowerer.qualify(fnNode.getSourceFile(), `%ol${fnNode.getStart()}.${name}`), + qualifiedName: lowerer.qualify( + ts.getSourceFileOfNode(fnNode), + `%ol${ts.tokenStartOfNode(fnNode)}.${name}`, + ), typeParams, instances: new Map(), objectLiteral: true, @@ -2381,7 +2385,10 @@ export function bindingGenericFnInfoOf( const info: GenericFnInfo = { decl: fnNode, baseName: name, - qualifiedName: lowerer.qualify(decl.getSourceFile(), nsPathPrefix(stmt ?? decl, decl) + name), + qualifiedName: lowerer.qualify( + ts.getSourceFileOfNode(decl), + nsPathPrefix(stmt ?? decl, decl) + name, + ), typeParams, instances: new Map(), }; diff --git a/packages/compiler/src/frontend/lowering/import-meta.ts b/packages/compiler/src/frontend/lowering/import-meta.ts index baf550e6d..796e64b69 100644 --- a/packages/compiler/src/frontend/lowering/import-meta.ts +++ b/packages/compiler/src/frontend/lowering/import-meta.ts @@ -20,7 +20,7 @@ export function isImportMeta(expr: ts.Expression): boolean { /** File-backed metadata is fixed by the compiled module graph. Reads share * one implementation across dot access, folded keys and selected bindings. */ export function importMetaField(lowerer: Lowerer, node: ts.Node, name: string): IrExpr { - const sf = node.getSourceFile(); + const sf = ts.getSourceFileOfNode(node); const fileName = lowerer.targetPlatform === "wasi" ? (wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/")) diff --git a/packages/compiler/src/frontend/lowering/intern-tables.ts b/packages/compiler/src/frontend/lowering/intern-tables.ts index 32bb71858..fc69237db 100644 --- a/packages/compiler/src/frontend/lowering/intern-tables.ts +++ b/packages/compiler/src/frontend/lowering/intern-tables.ts @@ -142,7 +142,7 @@ interface TableTypes { /** The key and value types of an InternTable checker type. */ function tableTypes(lowerer: Lowerer, type: ts.Type, blame: ts.Node): TableTypes { const parts = type.isUnionType() ? ts.constituentTypes(type) : [type]; - const table = parts.find((p) => p.getSymbol()?.name === "InternTable"); + const table = parts.find((p) => ts.symbolNameOf(p.getSymbol()) === "InternTable"); const args = table ? lowerer.checker.getTypeArguments(table as ts.TypeReference) : []; if (args.length !== 2) lowerer.noLowering("InternTable without type arguments", blame); const key = lowerer.mapTypeOf(args[0]!); diff --git a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts index d39e5cceb..b9bbf6216 100644 --- a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts +++ b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts @@ -81,10 +81,10 @@ export function isValueReturningConstructorClass(decl: ts.ClassDeclaration): boo if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) return; if (ts.isReturnStatement(node) && node.expression !== undefined && !insideArrow(node, ctor!)) returnsValue = true; - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - ctor.body.forEachChild(visit); - for (const parameter of ctor.parameters.nodes) parameter.forEachChild(visit); + ts.forEachChild(ctor.body, visit); + for (const parameter of ctor.parameters.nodes) ts.forEachChild(parameter, visit); return returnsValue && !observesThis; } @@ -215,7 +215,7 @@ export function lowerValueReturningConstructorNew( "spread arguments when constructing a class whose constructor returns a value", ); } - const key = `ctorValue:${decl.getSourceFile().fileName}:${decl.getStart()}`; + const key = `ctorValue:${ts.getSourceFileOfNode(decl).fileName}:${ts.tokenStartOfNode(decl)}`; let name = lowerer.valueHelpers.get(key); if (name === undefined) { name = `%ctorValue.${lowerer.valueHelpers.size}`; @@ -235,7 +235,7 @@ export function lowerValueReturningConstructorNew( { kind: "runtimeFence", code: "SC1090", - message: `constructors that return no object (the bare instance of '${decl.name?.text ?? "class"}' has no representation here) are not supported yet [SC1090 at ${decl.getSourceFile().fileName}:${ts.getLineAndCharacterOfPosition(decl.getSourceFile(), ctor.getStart()).line + 1}]`, + message: `constructors that return no object (the bare instance of '${decl.name?.text ?? "class"}' has no representation here) are not supported yet [SC1090 at ${ts.getSourceFileOfNode(decl).fileName}:${ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(decl), ts.tokenStartOfNode(ctor)).line + 1}]`, loc: locOf(ctor), }, ]; @@ -261,8 +261,8 @@ export function lowerValueReturningConstructorNew( .map((arg) => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) })); const result = lowerer.declareHiddenLocal("%constructed", DYN); const value: IrExpr = { kind: "varRef", localId: result.id, type: DYN, loc }; - const sf = expr.getSourceFile(); - const line = ts.getLineAndCharacterOfPosition(sf, expr.getStart()).line + 1; + const sf = ts.getSourceFileOfNode(expr); + const line = ts.getLineAndCharacterOfPosition(sf, ts.tokenStartOfNode(expr)).line + 1; const objectLike: IrExpr = { kind: "logical", op: "||", diff --git a/packages/compiler/src/frontend/lowering/json-require.ts b/packages/compiler/src/frontend/lowering/json-require.ts index 7c688dcda..927745253 100644 --- a/packages/compiler/src/frontend/lowering/json-require.ts +++ b/packages/compiler/src/frontend/lowering/json-require.ts @@ -36,7 +36,7 @@ export function jsonRequireTarget( if (call === undefined || !ts.isCallExpression(call)) return null; const spec = requireSpecOf(call); if (spec === null || !ts.isIdentifier(call.expression)) return null; - const sf = call.getSourceFile(); + const sf = ts.getSourceFileOfNode(call); if (!isCjsJsFile(sf, lowerer.program)) return null; if (createRequireCalleeFileOf(lowerer, call.expression) !== null) return null; const target = resolveImport(lowerer.program, sf, spec, "require"); diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index cb31ecc79..460f4d53a 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -887,10 +887,10 @@ export function lowerAssertDirectCall( * call's SOURCE TEXT (Node re-reads the file at runtime — the frontend has * the same bytes at compile time). */ function lowerAssertOk(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr { - requireStatementPosition(lowerer, expr, expr.expression.getText()); + requireStatementPosition(lowerer, expr, ts.getTextOfNode(expr.expression)); if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) { lowerer.noLowering( - `${expr.expression.getText()} with ${expr.arguments.nodes.length} arguments`, + `${ts.getTextOfNode(expr.expression)} with ${expr.arguments.nodes.length} arguments`, expr, "the supported forms are assert(value) and assert(value, message)", ); @@ -912,7 +912,7 @@ function lowerAssertOk(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): // Node reads the CALLSITE LINE and slices the expression out of it // (internal/errors/error_source.js) — a call spanning lines truncates // at the first newline, so the compile-time text does too. - const source = expr.getText().split("\n", 1)[0]!; + const source = ts.getTextOfNode(expr).split("\n", 1)[0]!; const text = `The expression evaluated to a falsy value:\n\n ${source}\n`; msg = { kind: "strLit", value: text, type: STRING, loc }; generated = true; diff --git a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts index f095c5b9c..0e48f2040 100644 --- a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts +++ b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts @@ -137,7 +137,9 @@ function literalBooleanOption( function nodeReadablePlan(lowerer: Lowerer, expression: ts.Expression): NodeReadablePlan | null { const direct = - lowerer.stageSourceType(expression) ?? lowerer.mapTypeOf(lowerer.typeOf(expression)); + lowerer.receiverTypeOverrides.get(expression) ?? + lowerer.stageSourceType(expression) ?? + lowerer.mapTypeOf(lowerer.typeOf(expression)); if (direct?.kind === "object") { const sides = streamSidesOf(lowerer, lowerer.classes.get(direct.className)); if (sides === "r" || sides === "rw") { diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index b0b7884f2..b1b4086cd 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,5 +1,6 @@ import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js"; import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js"; +import { lowerDynStreamMemberCall } from "./dyn-streams.js"; import { lowerBuiltinClassCall } from "./builtins/builtin-class-dyn.js"; import { refuseModuleMemberThis } from "./module-member-this.js"; import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js"; @@ -677,7 +678,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const isStorage = parts.length > 0 && parts.every((part) => { - const name = part.getSymbol()?.name; + const name = ts.symbolNameOf(part.getSymbol()); return name === "ArrayBuffer" || name === "SharedArrayBuffer"; }); if (isStorage) { @@ -851,7 +852,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { (source.type.kind === "dyn" || source.type.kind === "func" || (source.type.kind === "union" && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(source.type)) || ((source.type.kind === "map" || source.type.kind === "set") && lowerer.dynConvertible(source.type)) || @@ -867,7 +868,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.lowerExprExpecting(ts.isSpreadElement(arg) ? arg.expression : arg, DYN), ); const spreads = expr.arguments.nodes.flatMap((arg, index) => - ts.isSpreadElement(arg) ? [{ arg: index, what: arg.expression.getText() }] : [], + ts.isSpreadElement(arg) ? [{ arg: index, what: ts.getTextOfNode(arg.expression) }] : [], ); return { kind: "seqExpr", @@ -878,7 +879,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { kind: "dynCall", callee, receiver, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args, ...(spreads.length > 0 ? { spreads } : {}), type: DYN, @@ -1183,7 +1184,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // argument list. Typed .ts spreads keep completeArgs' rest packing. const jsSpreadArgs = expr.arguments.nodes.some((a) => ts.isSpreadElement(a)) && - isJsSourceFile(expr.getSourceFile()); + isJsSourceFile(ts.getSourceFileOfNode(expr)); if (lowerer.isTopLevelFnSymbol(expr.expression) && !lowerer.peekLocal(expr.expression)) { // `import g = N.f; g()` — the alias's own source-order guards // (a no-op for every non-import= binding). @@ -1393,12 +1394,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if ( g && (g.type.kind === "func" || g.type.kind === "dyn") && - !cjsExportCallIgnoresThis(lowerer, expr.getSourceFile(), expr.expression.name.text) + !cjsExportCallIgnoresThis(lowerer, ts.getSourceFileOfNode(expr), expr.expression.name.text) ) { lowerer.unsupported( "SC1090", expr, - `calling '${expr.expression.getText()}' through the module's exports object when the function may read 'this' (Node binds it to the exports object, which compiles to a static table — call a local function instead)`, + `calling '${ts.getTextOfNode(expr.expression)}' through the module's exports object when the function may read 'this' (Node binds it to the exports object, which compiles to a static table — call a local function instead)`, ); } if (g && (g.type.kind === "func" || g.type.kind === "dyn")) { @@ -1413,6 +1414,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { } if (ts.isPropertyAccessExpression(expr.expression)) { const intrinsic = + // Checked-dynamic stream slots dispatch per value (dyn-streams.ts). + lowerDynStreamMemberCall(lowerer, expr, expr.expression) ?? fenceNodeModuleMutationCall(lowerer, expr, expr.expression) ?? // `f.apply(thisArg, args)` forwarding a fixed argument tuple (typed // `arguments`, a tuple rest parameter): a direct call. @@ -1591,7 +1594,11 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // monomorphized against the defining literal's declaration. lowerObjLitGenericMethodCall(lowerer, expr, expr.expression); if (intrinsic) return intrinsic; - if (isJsSourceFile(expr.getSourceFile())) { + { + const boxed = lowerDynReceiverMethodCall(lowerer, expr, expr.expression, true); + if (boxed) return boxed; + } + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { const staticField = lowerStaticFieldRead(lowerer, expr.expression); if (staticField?.type.kind === "dyn" && !expr.arguments.nodes.some(ts.isSpreadElement)) { return { @@ -1659,7 +1666,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.unsupported( "SC1090", expr, - `calls through function-with-properties values ('${expr.expression.expression.getText()}' is callable AND carries members — the chalk shape; no hybrid representation exists yet: export the base and the property as separate functions)`, + `calls through function-with-properties values ('${ts.getTextOfNode(expr.expression.expression)}' is callable AND carries members — the chalk shape; no hybrid representation exists yet: export the base and the property as separate functions)`, ); } } @@ -1684,7 +1691,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { ); } } - lowerer.unsupported("SC1090", expr, `method calls like '${expr.expression.getText()}'`); + lowerer.unsupported("SC1090", expr, `method calls like '${ts.getTextOfNode(expr.expression)}'`); } // `s[Symbol.iterator]()` on a string: the string iterator as a value. @@ -1723,7 +1730,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { ) { const access = expr.expression; const value = tryLowerExpression(lowerer, access.expression); - if (value?.type.kind === "array" && !isJsSourceFile(expr.getSourceFile())) { + if (value?.type.kind === "array" && !isJsSourceFile(ts.getSourceFileOfNode(expr))) { const callback = lowerArrayCallbackCall(lowerer, expr, access, value); if (callback) return callback; } @@ -1758,7 +1765,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { kind: "dynCall", callee, receiver, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: expr.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, @@ -1877,7 +1884,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { 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"; @@ -1939,7 +1946,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // per use like every any-origin value. if ( (expr.arguments.nodes.length > callee.type.params.length || callee.type.rest === true) && - (isJsSourceFile(expr.getSourceFile()) || + (isJsSourceFile(ts.getSourceFileOfNode(expr)) || (callee.type.rest === true && callee.type.restAbi === undefined)) && !expr.arguments.nodes.some((a) => ts.isSpreadElement(a)) && canBoxFuncIntoDyn( @@ -1971,7 +1978,7 @@ function lowerCheckedCallableValue( callee.type.kind !== "func" || !( (callee.type.rest === true && callee.type.restAbi === undefined) || - (isJsSourceFile(call.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(call)) && call.arguments.nodes.length > callee.type.params.length) ) || !lowerer.dynConvertible(callee.type) @@ -2536,7 +2543,7 @@ function lowerArrayPrototypeBorrowCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if ( - !isJsSourceFile(call.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(call)) || call.questionDotToken || access.questionDotToken || access.name.text !== "call" || @@ -2748,7 +2755,7 @@ function lowerObjectOwnCall( result = guardNullProto( receiverRef, type.shapeId, - `${methodAccess.getText()} is not a function`, + `${ts.getTextOfNode(methodAccess)} is not a function`, result, loc, ); @@ -3311,6 +3318,13 @@ const ITERATOR_HELPER_METHODS = new Set([ "find", ]); +/** A call expression, through parentheses: its value has no other holder. */ +function isFreshCallResult(node: ts.Expression): boolean { + let e = node; + while (ts.isParenthesizedExpression(e)) e = e.expression; + return ts.isCallExpression(e); +} + /** METHOD calls on dyn receivers (`pkg.name.replace(...)`, `rawName.split`, * `ws.packages.filter(...)` — JSON.parse-derived values): validate the * receiver's dyn kind, extract, and ride the STATIC method machinery — the @@ -3328,6 +3342,7 @@ function lowerDynReceiverMethodCall( lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression, + boxTypedTemporary = false, ): IrExpr | null { if (lowerer.chainBlocked(call, access)) return null; if (stdlibGlobalNameOf(lowerer, access.expression) !== null) { @@ -3362,7 +3377,7 @@ function lowerDynReceiverMethodCall( const recvSymbol = recvTs.getSymbol(); if ( recvSymbol && - CHECKED_ITERATOR_TYPES.has(recvSymbol.name) && + CHECKED_ITERATOR_TYPES.has(ts.symbolName(recvSymbol)) && lowerer.isStdlibSymbol(recvSymbol) && ITERATOR_HELPER_METHODS.has(access.name.text) && lowerer.isStdlibMember(access) @@ -3430,7 +3445,7 @@ function lowerDynReceiverMethodCall( kind: "dynCall", callee: member, receiver, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, @@ -3452,7 +3467,7 @@ function lowerDynReceiverMethodCall( return null; if ( probed?.type.kind === "union" && - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && lowerer.unions .get(probed.type.unionId) ?.arms.every( @@ -3502,7 +3517,7 @@ function lowerDynReceiverMethodCall( if ( checkerUntyped && recv.type.kind === "union" && - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && isStringMethodName(access.name.text) && lowerer.unions .get(recv.type.unionId) @@ -3516,6 +3531,22 @@ function lowerDynReceiverMethodCall( // 'unknown' slot does. if (checkerUntyped && DYN_HANDLE_KINDS.has(recv.type.kind)) recv = lowerer.coerceToExpected(recv, DYN); + // The last-resort retry (boxTypedTemporary — every other lowering + // already declined): a checker-`any` receiver whose value is a FRESH + // typed result of an earlier step in the chain (`out.split("\n")` + // extracted the string and answered a native string[]) has no typed + // home under its `any` spelling, so it boxes into the checked-dynamic + // tree and the call dispatches on the runtime kind — `.map`, `.pop`, + // `.filter` on that array run JS-exact. Only call results qualify: + // the box copies, and a temporary has no other holder to observe it. + if ( + boxTypedTemporary && + checkerUntyped && + recv.type.kind !== "dyn" && + isFreshCallResult(access.expression) && + lowerer.dynConvertible(recv.type) + ) + recv = lowerer.coerceToExpected(recv, DYN); if (recv.type.kind !== "dyn") return null; if ( access.name.text === "normalize" && @@ -3572,7 +3603,7 @@ function lowerDynReceiverMethodCall( kind: "dynInvoke", recv: value, method: "normalize", - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: hasArgument ? [argument] : [], type: DYN, loc, @@ -3608,7 +3639,7 @@ function lowerDynReceiverMethodCall( // dynKeyGet cannot stand in for JavaScript's prototype lookup. Only // an authored declaration can justify the own-member path. const authored = prop.declarations.some( - (decl) => !lowerer.isStdlibFile(decl.getSourceFile()), + (decl) => !lowerer.isStdlibFile(ts.getSourceFileOfNode(decl)), ); return ( authored && @@ -3650,7 +3681,7 @@ function lowerDynReceiverMethodCall( lowerer, call, access.name.text, - access.getText(), + ts.getTextOfNode(access), recv, ); if (primitive) return primitive; @@ -3665,7 +3696,7 @@ function lowerDynReceiverMethodCall( args: [ recv, argument ? lowerer.lowerExprExpecting(argument, DYN) : dynUndefinedExpr(locOf(call)), - { kind: "strLit", value: access.getText(), type: STRING, loc: locOf(call) }, + { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc: locOf(call) }, ], type: STRING, loc: locOf(call), @@ -3680,7 +3711,10 @@ function lowerDynReceiverMethodCall( return { kind: "libCall", fn: "dyn.valueOf", - args: [recv, { kind: "strLit", value: access.getText(), type: STRING, loc: locOf(call) }], + args: [ + recv, + { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc: locOf(call) }, + ], type: DYN, loc: locOf(call), }; @@ -3768,15 +3802,15 @@ function lowerDynReceiverMethodCall( // statement cannot leave the expression). const stringRefusal = (read: IrExpr): IrExpr => { if (!stringOnlyName) return read; - const sf = call.getSourceFile(); - const line = ts.getLineAndCharacterOfPosition(sf, call.getStart(sf)).line + 1; + const sf = ts.getSourceFileOfNode(call); + const line = ts.getLineAndCharacterOfPosition(sf, ts.tokenStartOfNode(call)).line + 1; return { kind: "ternary", cond: { kind: "dynTest", test: "string", value: receiver, type: BOOL, loc }, then: nodeThrowExpr( 0, "SC1090", - `method calls like '${access.getText()}' on a string are not supported yet [SC1090 at ${sf.fileName}:${line}]`, + `method calls like '${ts.getTextOfNode(access)}' on a string are not supported yet [SC1090 at ${sf.fileName}:${line}]`, DYN, loc, ), @@ -3812,7 +3846,7 @@ function lowerDynReceiverMethodCall( kind: "dynCall", callee, receiver, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args, type: DYN, loc, @@ -4102,7 +4136,7 @@ export function lowerDynDispatchMethodCall( ): IrExpr | null { const method = access.name.text; if ( - (!DYN_DISPATCH_METHODS.has(method) && !isJsSourceFile(call.getSourceFile())) || + (!DYN_DISPATCH_METHODS.has(method) && !isJsSourceFile(ts.getSourceFileOfNode(call))) || lowerer.chainBlocked(call, access) ) return null; @@ -4211,7 +4245,7 @@ export function lowerDynDispatchMethodCall( kind: "dynInvoke", recv: { kind: "varRef", localId: saved.id, type: DYN, loc: locOf(access.expression) }, method, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: [dynUndefinedExpr(locOf(call.arguments.nodes[0]!))], type: DYN, loc: locOf(call), @@ -4233,7 +4267,7 @@ export function lowerDynDispatchMethodCall( kind: "dynInvoke", recv, method, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args, type: DYN, loc: locOf(call), @@ -4453,7 +4487,7 @@ function lowerNumberToStringCall( call.arguments.nodes[0] ? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN) : dynUndefinedExpr(locOf(call)), - { kind: "strLit", value: access.getText(), type: STRING, loc: locOf(call) }, + { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc: locOf(call) }, ], type: STRING, loc: locOf(call), @@ -4576,7 +4610,7 @@ function lowerDefaultToStringCall( // A null-prototype record (Object.groupBy) has no toString: Node // throws " is not a function". const nullProtoT = nullProtoRecordShape(lowerer, recvT); - const missing = nullProtoT ? `${access.getText()} is not a function` : null; + const missing = nullProtoT ? `${ts.getTextOfNode(access)} is not a function` : null; if (!missing && pureObjectToStringReceiver(access.expression)) return constant; const recv = lowerer.lowerExpr(access.expression); const key = `objToStr:${typeKey(recv.type)}${missing ? `:${missing}` : ""}`; @@ -4727,7 +4761,7 @@ function lowerPrimitiveProtoCall( (name === "toFixed" || name === "toExponential") && call.arguments.nodes.length <= 1 && !call.arguments.nodes.some(ts.isSpreadElement) && - isJsSourceFile(call.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(call)) ) { const operand = lowerer.lowerExpr(recv); if ( @@ -4740,7 +4774,7 @@ function lowerPrimitiveProtoCall( lowerer, call, name, - `${recv.getText()}.${name}`, + `${ts.getTextOfNode(recv)}.${name}`, lowerer.coerceToExpected(operand, DYN), ); if (dispatched) return dispatched; @@ -4753,7 +4787,7 @@ function lowerPrimitiveProtoCall( kind: "dynInvoke", recv: lowerer.coerceToExpected(operand, DYN), method: name, - calleeName: `${recv.getText()}.${name}`, + calleeName: `${ts.getTextOfNode(recv)}.${name}`, args: call.arguments.nodes.map((argument) => lowerer.lowerExprExpecting(argument, DYN)), type: DYN, loc: locOf(call), @@ -4763,7 +4797,7 @@ function lowerPrimitiveProtoCall( name === "toPrecision" && call.arguments.nodes.length <= 1 && !call.arguments.nodes.some(ts.isSpreadElement) && - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && recvKind === "union" ) { const operand = lowerer.lowerExpr(recv); @@ -4772,7 +4806,7 @@ function lowerPrimitiveProtoCall( lowerer, call, name, - `${recv.getText()}.${name}`, + `${ts.getTextOfNode(recv)}.${name}`, lowerer.coerceToExpected(operand, DYN), ); if (dispatched) return dispatched; @@ -4992,9 +5026,10 @@ function isSoleAbstractMethod(decl: ts.FunctionDeclaration | ts.MethodDeclaratio if (!ts.getModifiers(decl)?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword)) return false; const owner = decl.parent; if (!ts.isClassDeclaration(owner) && !ts.isClassExpression(owner)) return false; - const name = decl.name.getText(); + const name = ts.getTextOfNode(decl.name); return !owner.members.nodes.some( - (member) => member !== decl && ts.isMethodDeclaration(member) && member.name.getText() === name, + (member) => + member !== decl && ts.isMethodDeclaration(member) && ts.getTextOfNode(member.name) === name, ); } @@ -5012,7 +5047,7 @@ function reconcileOverloadReturn( call.type.kind === "object" && rdecl && ts.isMethodDeclaration(rdecl) && - (!isJsSourceFile(rdecl.getSourceFile()) || returnsOnlyThis(rdecl)) + (!isJsSourceFile(ts.getSourceFileOfNode(rdecl)) || returnsOnlyThis(rdecl)) ) { call = lowerer.maybeNarrow(call, expr); } @@ -5174,7 +5209,7 @@ export function lowerTaggedTemplate(lowerer: Lowerer, expr: ts.TaggedTemplateExp let callee = lowerer.lowerExpr(expr.tag); if ( callee.type.kind === "func" && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(callee.type) ) { callee = { kind: "dynFrom", value: callee, type: DYN, loc }; @@ -5192,7 +5227,7 @@ export function lowerTaggedTemplate(lowerer: Lowerer, expr: ts.TaggedTemplateExp ]; const calleeName = ts.isPropertyAccessExpression(expr.tag) || ts.isElementAccessExpression(expr.tag) - ? expr.tag.getText() + ? ts.getTextOfNode(expr.tag) : ts.isIdentifier(expr.tag) ? expr.tag.text : "value"; @@ -5262,7 +5297,7 @@ function loweredTemplateStrings( loc, }) : null; - const key = `templateStringsGlobal:${loc.file}:${expr.template.getStart()}:${raw ? "checked" : "typed"}`; + const key = `templateStringsGlobal:${loc.file}:${ts.tokenStartOfNode(expr.template)}:${raw ? "checked" : "typed"}`; let getter = lowerer.valueHelpers.get(key); if (!getter) { const site = lowerer.globalsList.length; @@ -5977,7 +6012,7 @@ export function lowerPromiseMethodCall( kind: "dynInvoke", recv: lowerer.coerceToExpected(receiver, DYN), method: "finally", - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: [lowerer.coerceToExpected(cb, DYN)], type: DYN, loc, @@ -6078,7 +6113,7 @@ export function lowerPromiseMethodCall( if ( member === "catch" && (inner.kind === "dyn" || - (isJsSourceFile(call.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(call)) && (inner.kind === "void" || lowerer.dynConvertible(inner)))) ) { let cb = lowerer.lowerExpr(call.arguments.nodes[0]!); @@ -7472,7 +7507,7 @@ function lowerObjectStaticCall( const prototype = lowerClassPrototypeAssign(lowerer, call); if (prototype) return prototype; if ( - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && call.arguments.nodes.length > 0 && !call.arguments.nodes.some(ts.isSpreadElement) ) { @@ -7744,7 +7779,12 @@ function lowerObjectStaticCall( args: [ pRef(), src, - { kind: "strLit", value: srcNode.getText(), type: STRING, loc: argLoc }, + { + kind: "strLit", + value: ts.getTextOfNode(srcNode), + type: STRING, + loc: argLoc, + }, ], type: VOID, loc: argLoc, @@ -7801,7 +7841,9 @@ function lowerObjectStaticCall( (exactClassOfReceiver(lowerer, targetNode.expression) !== null || storedClassValueType(lowerer, targetNode.expression)?.kind === "classval"); const declaredTarget = - target?.type.kind === "dyn" && !prototypeTarget && !isJsSourceFile(targetNode.getSourceFile()) + target?.type.kind === "dyn" && + !prototypeTarget && + !isJsSourceFile(ts.getSourceFileOfNode(targetNode)) ? lowerer.mapTypeOf(lowerer.typeOf(targetNode)) : null; if (target && declaredTarget && isDynTypedRefType(declaredTarget)) @@ -8000,7 +8042,7 @@ function lowerObjectStaticCall( while (ts.isParenthesizedExpression(inner) || ts.isAsExpression(inner)) inner = inner.expression; const value = - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && (ts.isObjectLiteralExpression(inner) || ts.isArrayLiteralExpression(inner)) ? lowerer.lowerExprExpecting(argNode, DYN) : lowerer.lowerExpr(argNode); @@ -8221,7 +8263,7 @@ function lowerObjectStaticCall( probed.type.kind === "bool"); const isJsUnion = probed?.type.kind === "union" && - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && lowerer.dynConvertible(probed.type); if ( isDyn || @@ -8243,7 +8285,7 @@ function lowerObjectStaticCall( // write to the arrays well typed. JavaScript may store anything into // them, so it keeps the checked-dynamic arrays. const resultT = - member !== "values" && !isJsSourceFile(call.getSourceFile()) + member !== "values" && !isJsSourceFile(ts.getSourceFileOfNode(call)) ? lowerer.mapTypeOf(lowerer.typeOf(call)) : null; if (member === "keys" && resultT?.kind === "array" && resultT.elem.kind === "string") @@ -8266,7 +8308,11 @@ function lowerObjectStaticCall( // program only iterates or indexes the result, or passes it to a // read-only array method, a native vector of the checked values skips // building that array and, for for-of, the iterator protocol. - if (member === "values" && !isJsSourceFile(call.getSourceFile()) && consumesValueList(call)) + if ( + member === "values" && + !isJsSourceFile(ts.getSourceFileOfNode(call)) && + consumesValueList(call) + ) return { kind: "libCall", fn: "dyn.objValueList", @@ -8304,7 +8350,7 @@ function lowerObjectStaticCall( if ( (member === "values" || member === "entries") && (enumerationType?.kind === "dyn" || - (enumerationType === null && isJsSourceFile(call.getSourceFile()))) + (enumerationType === null && isJsSourceFile(ts.getSourceFileOfNode(call)))) ) { let receiver = lowerer.coerceToExpected(lowerer.lowerExpr(argNode), DYN); if (receiver.type.kind !== "dyn") lowerer.badType(argNode, lowerer.typeOf(argNode)); @@ -8835,7 +8881,7 @@ export function lowerRecordFieldCall( const target = lowerer.fieldTarget(access); const loc = locOf(call); const receiverLocal = - target && isJsSourceFile(call.getSourceFile()) + target && isJsSourceFile(ts.getSourceFileOfNode(call)) ? lowerer.declareHiddenLocal("%callReceiver", target.obj.type) : null; const init: IrStmt | null = @@ -8855,7 +8901,7 @@ export function lowerRecordFieldCall( // values (util.DebugLogger). if ( callee.type.kind === "dyn" && - (isJsSourceFile(call.getSourceFile()) || + (isJsSourceFile(ts.getSourceFileOfNode(call)) || lowerer.checker.getCallSignatures(lowerer.typeOf(access)).length > 0) ) { const receiver = @@ -8873,7 +8919,7 @@ export function lowerRecordFieldCall( kind: "dynCall", callee, ...(receiver ? { receiver } : {}), - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, @@ -8944,7 +8990,7 @@ function lowerUrlStaticCall( if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken !== undefined) return null; if (!ts.isIdentifier(callee.expression)) return null; const sym = lowerer.resolveValueSymbol(callee.expression); - if (!sym || !lowerer.isStdlibSymbol(sym) || sym.name !== "URL") return null; + if (!sym || !lowerer.isStdlibSymbol(sym) || ts.symbolName(sym) !== "URL") return null; if (callee.name.text === "canParse" || callee.name.text === "parse") { return lowerUrlFactory(lowerer, call, callee.name.text); } @@ -9302,7 +9348,7 @@ function lowerObjLitGenericMethodCall( kind: "dynInvoke", recv: { kind: "dynFrom", value, type: DYN, loc }, method: name, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, @@ -9311,7 +9357,7 @@ function lowerObjLitGenericMethodCall( lowerer.unsupported( "SC1090", call, - `Function.prototype.${name} on a function whose signature cannot cross the checked-value boundary (call '${access.expression.getText()}(...)' directly)`, + `Function.prototype.${name} on a function whose signature cannot cross the checked-value boundary (call '${ts.getTextOfNode(access.expression)}(...)' directly)`, ); } // STANDARD-LIBRARY generic members (Promise.then, Object. @@ -9339,7 +9385,7 @@ function lowerObjLitGenericMethodCall( kind: "dynInvoke", recv: receiver, method: name, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, @@ -9381,6 +9427,7 @@ export function lowerObjectMethodCall( ): IrExpr | null { if (lowerer.chainBlocked(access, call)) return null; let mappedReceiver = + lowerer.receiverTypeOverrides.get(access.expression) ?? structuralReadType(lowerer, access.expression) ?? lowerer.mapTypeOf(lowerer.typeOf(access.expression)); // EventEmitter's static side (the class, a value of it, or the events @@ -9399,7 +9446,7 @@ export function lowerObjectMethodCall( (lowerer.implicitParamTypes !== null || lowerer.classImplementedProtocol(lowerer.typeOf(access.expression)) || ts.isNewExpression(access.expression) || - isJsSourceFile(call.getSourceFile())) + isJsSourceFile(ts.getSourceFileOfNode(call))) ? ts.isNewExpression(access.expression) ? lowerer.lowerExpr(access.expression) : tryLowerExpression(lowerer, access.expression) @@ -9418,7 +9465,7 @@ export function lowerObjectMethodCall( if (dispatched) return dispatched; const arms = lowerer.unions.get(mappedReceiver.unionId)?.arms; if ( - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && arms?.every( (arm) => arm.kind === "object" && !lowerer.classes.get(arm.className)?.def.runtime, ) && @@ -9572,7 +9619,7 @@ export function lowerObjectMethodCall( callee, lowerer.armTag(callee.type.unionId, callableType), callableType, - () => `${access.getText()} is not a function`, + () => `${ts.getTextOfNode(access)} is not a function`, ); const helper = inPlace ? null @@ -9586,7 +9633,7 @@ export function lowerObjectMethodCall( if ( ((callee.type.rest === true && callee.type.restAbi === undefined) || call.arguments.nodes.length > callee.type.params.length) && - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && lowerer.dynConvertible(callee.type) ) { const boxed: IrExpr = { kind: "dynFrom", value: callee, type: DYN, loc }; @@ -9597,7 +9644,7 @@ export function lowerObjectMethodCall( kind: "dynCall", callee: boxed, ...receiver, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args, type: DYN, loc, @@ -9615,7 +9662,7 @@ export function lowerObjectMethodCall( kind: "dynCall", callee, ...receiver, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args, type: DYN, loc, @@ -9624,7 +9671,7 @@ export function lowerObjectMethodCall( } if ( !fieldType && - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && !info.def.runtime && !info.builtinError && !info.builtinEmitter && @@ -9654,7 +9701,7 @@ export function lowerObjectMethodCall( kind: "dynCall", callee, receiver, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, @@ -9703,7 +9750,7 @@ export function lowerObjectMethodCall( call.arguments.nodes, fallback, loc, - access.getText(), + ts.getTextOfNode(access), ); return { kind: "seqExpr", @@ -9739,7 +9786,7 @@ export function lowerObjectMethodCall( const declarerDecl = found.declarer.decl; if ( adapted && - !(declarerDecl && isJsSourceFile(declarerDecl.getSourceFile())) && + !(declarerDecl && isJsSourceFile(ts.getSourceFileOfNode(declarerDecl))) && !lowerer.overrideBelow(info, method) ) { lowerer.noteEdge(`%${found.declarer.def.name}.${method}`); @@ -9829,7 +9876,7 @@ function lowerUnionObjectMethodCall( kind: "dynCall", callee, receiver, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, @@ -9991,7 +10038,7 @@ function lowerUnionObjectDynFieldCall( kind: "dynCall", callee, receiver: lowerer.coerceToExpected(receiverRef, DYN), - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args, type: DYN, loc, @@ -10373,7 +10420,7 @@ function maybePromiseHandlerResult( function lowerObjectAssignOntoError(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { const argNodes = call.arguments.nodes; if ( - isJsSourceFile(call.getSourceFile()) || + isJsSourceFile(ts.getSourceFileOfNode(call)) || argNodes.length < 2 || argNodes.some((a) => ts.isSpreadElement(a)) ) diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 680812905..0c6287cc7 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,4 +1,5 @@ import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; +import { adaptPromiseExecutor } from "./promise-executor-adapter.js"; import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; import { constructedFromValue, @@ -100,7 +101,7 @@ import { isAssertClassNew } from "./assert-instances.js"; import { lowerArrayConstructor } from "./containers/array-construction.js"; import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js"; import { lowerObjectConstructor } from "./containers/indexed-objects.js"; -import { knownBufEncoding } from "./containers/bytes.js"; +import { knownBufEncoding, lowerNodeModulesBufferConstruct } from "./containers/bytes.js"; import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; import { lowerSearchParamsNew } from "./builtins/url.js"; import { lowerTextCodecNew } from "./builtins/text-codecs.js"; @@ -117,6 +118,7 @@ import { import { STREAM_API_MEMBERS, STREAM_PROP_MEMBERS, + STREAM_COMPLETION_CALLBACK_INDEX, UNDERSCORE_METHODS, lowerStreamNew, lowerStreamSuperCall, @@ -182,9 +184,9 @@ export function returnsOnlyThis(member: ts.MethodDeclaration): boolean { while (value && ts.isParenthesizedExpression(value)) value = value.expression; if (!value || value.kind !== ts.SyntaxKind.ThisKeyword) valid = false; } - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - body.forEachChild(visit); + ts.forEachChild(body, visit); return valid; } @@ -215,9 +217,9 @@ export function returnsOnlyThisValue(member: ts.MethodDeclaration): boolean { } if (!value || value.kind !== ts.SyntaxKind.ThisKeyword) valid = false; } - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - body.forEachChild(visit); + ts.forEachChild(body, visit); return valid; } @@ -340,7 +342,7 @@ export function storedClassValueType( } if ( (ts.isArrowFunction(callee) || ts.isFunctionExpression(callee)) && - isJsSourceFile(callee.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(callee)) && !callee.typeParameters?.nodes.length && !lowerer.preparingClassFactories.has(callee) ) { @@ -409,7 +411,7 @@ export function storedClassValueType( } if ( !factory || - !isJsSourceFile(factory.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(factory)) || factory.typeParameters?.nodes.length || !ts.isVariableDeclaration(factory.parent) || factory.parent.initializer !== factory || @@ -807,7 +809,7 @@ function symbolSlotReturnType( ): IrType | null { if (symbolFields.size === 0) return null; if (fnLike.type !== undefined || !fnLike.body) return null; - if (!isJsSourceFile(fnLike.getSourceFile())) return null; + if (!isJsSourceFile(ts.getSourceFileOfNode(fnLike))) return null; if (fnLike.asteriskToken !== undefined) return null; if (fnLike.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null; const stmts = fnLike.body.statements.nodes; @@ -817,9 +819,9 @@ function symbolSlotReturnType( const visit = (n: ts.Node): void => { if (ts.isFunctionLike(n)) return; if (ts.isReturnStatement(n)) returns.push(n); - n.forEachChild(visit); + ts.forEachChild(n, visit); }; - fnLike.body.forEachChild(visit); + ts.forEachChild(fnLike.body, visit); let out: IrType | null = null; for (const r of returns) { let e = r.expression; @@ -1059,7 +1061,7 @@ export function builtinStreamInfoOf( } let irName: string | null = null; for (const [name, rec] of RUNTIME_STREAM_CLASSES) { - if (rec.lib === symbol.name) irName = name; + if (rec.lib === ts.symbolName(symbol)) irName = name; } if (!irName) return null; // Both the bundled declarations and @types/node declare the classes @@ -1100,7 +1102,7 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E // JavaScript documentation cannot guarantee that a call or an indexed // read returns a present number. Numeric fields receiving those values // must retain undefined, so later arithmetic performs JS coercion (NaN). - if (type.kind === "f64" && isJsSourceFile(expression.getSourceFile())) { + if (type.kind === "f64" && isJsSourceFile(ts.getSourceFileOfNode(expression))) { const assignment = expression.parent; const left = ts.isBinaryExpression(assignment) ? assignment.left : null; const field = @@ -1132,7 +1134,7 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E if (ts.isCallExpression(rhs) || ts.isElementAccessExpression(rhs)) acceptsUnchecked = true; } - node.forEachChild(scan); + ts.forEachChild(node, scan); }; scan(owner); if (acceptsUnchecked) return DYN; @@ -1169,7 +1171,7 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E owner = owner.parent; if (owner && (ts.isClassDeclaration(owner) || ts.isClassExpression(owner)) && owner.name) { const className = owner.name.text; - for (const statement of owner.getSourceFile().statements.nodes) { + for (const statement of ts.getSourceFileOfNode(owner).statements.nodes) { if ( !ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression) @@ -1272,7 +1274,7 @@ function returnsWidenedJsField( ret: IrType, ): boolean { if ( - !isJsSourceFile(member.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(member)) || member.type || member.asteriskToken || hasExplicitJsDocReturn(member) @@ -1335,7 +1337,7 @@ function returnsWidenedJsField( const scan = (node: ts.Node): void => { if (result || ts.isFunctionLike(node)) return; if (ts.isReturnStatement(node) && node.expression && widened(node.expression)) result = true; - node.forEachChild(scan); + ts.forEachChild(node, scan); }; if (member.body) scan(member.body); return result; @@ -1415,7 +1417,7 @@ export function builtinEmitterInfoOf( } return null; } - if (symbol.name !== "EventEmitter") return null; + if (ts.symbolName(symbol) !== "EventEmitter") return null; const declared = symbol.declarations.some((d) => { if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false; let node: ts.Node | undefined = d.parent; @@ -1445,7 +1447,7 @@ export function builtinErrorInfoOf( if (alias !== null) return alias.kind === "error" ? alias.info : null; if (!lowerer.isStdlibSymbol(symbol)) return null; for (const [irName, rec] of RUNTIME_ERROR_CLASSES) { - if (rec.lib === symbol.name) return lowerer.classes.get(irName) ?? null; + if (rec.lib === ts.symbolName(symbol)) return lowerer.classes.get(irName) ?? null; } return null; } @@ -1590,9 +1592,9 @@ function collectClassShapeDeferring(lowerer: Lowerer, decl: ts.ClassDeclaration) if ( baseDeclaration && !lowerer.isStdlibSymbol(baseSymbol ?? undefined) && - !baseDeclaration.getSourceFile().isDeclarationFile && - (baseDeclaration.getSourceFile() !== decl.getSourceFile() || - baseDeclaration.getStart() < decl.getStart()) + !ts.getSourceFileOfNode(baseDeclaration).isDeclarationFile && + (ts.getSourceFileOfNode(baseDeclaration) !== ts.getSourceFileOfNode(decl) || + ts.tokenStartOfNode(baseDeclaration) < ts.tokenStartOfNode(decl)) ) lowerer.collectClassShape(baseDeclaration); lowerer.collectClassShapeInner(decl); @@ -1797,7 +1799,7 @@ export function collectClassShapeInner( // source (V8 has not shipped the proposal; the type-stripping // loaders leave `@dec` in place) — there is no runtime behavior // to be exact against. - if (isJsSourceFile(decl.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(decl))) { lowerer.unsupported( "SC1090", (classDecoratorNodes[0] ?? decoratorNodesOf(decoratedMembers[0]!)[0])!, @@ -1935,7 +1937,7 @@ export function collectClassShapeInner( const source = signatures.length === 1 ? signatures[0]!.declaration : undefined; if ( !source || - !isJsSourceFile(source.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(source)) || !(ts.isFunctionDeclaration(source) || ts.isFunctionExpression(source)) ) return false; @@ -2051,13 +2053,13 @@ export function collectClassShapeInner( const baseDecl = propBase.decl; if ( baseDecl != null && - baseDecl.getSourceFile() === decl.getSourceFile() && - baseDecl.getStart() > decl.getStart() + ts.getSourceFileOfNode(baseDecl) === ts.getSourceFileOfNode(decl) && + ts.tokenStartOfNode(baseDecl) > ts.tokenStartOfNode(decl) ) { lowerer.unsupported( "SC1090", t, - `extending '${t.expression.getText()}' above the statement that assigns it (the property is still undefined when this class evaluates — Node throws here; assign the base first)`, + `extending '${ts.getTextOfNode(t.expression)}' above the statement that assigns it (the property is still undefined when this class evaluates — Node throws here; assign the base first)`, ); } base = propBase; @@ -2075,7 +2077,7 @@ export function collectClassShapeInner( lowerer.unsupported( "SC1090", t, - `extending the reassigned property '${t.expression.getText()}' (the runtime base is whichever assignment ran last — bind the class exactly once)`, + `extending the reassigned property '${ts.getTextOfNode(t.expression)}' (the runtime base is whichever assignment ran last — bind the class exactly once)`, ); } } @@ -2294,7 +2296,7 @@ export function collectClassShapeInner( // Public JS accessors need live constructor descriptors, including // receiver-sensitive getters and setters inherited by subclasses. const checkedStatics = - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && ((base && hasRuntimeStatics(base)) || decl.members.nodes.some( (member) => @@ -2368,7 +2370,7 @@ export function collectClassShapeInner( for (const member of decl.members.nodes) { if ( (functionLocalClass(decl) || checkedStatics) && - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && (ts.isClassStaticBlockDeclaration(member) || ts .getModifiers(member) @@ -2379,7 +2381,7 @@ export function collectClassShapeInner( } if ( functionLocalClass(decl) && - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && (ts.isMethodDeclaration(member) || ts.isAccessor(member)) && member.body && !ts.isPrivateIdentifier(member.name) @@ -2451,7 +2453,7 @@ export function collectClassShapeInner( if (type.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name)); if ( type.kind === "dyn" && - !isJsSourceFile(member.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(member)) && !lowerer.classFieldStoresDyn(lowerer.typeOf(member.name)) && (member.type !== undefined || (lowerer.typeOf(member.name).flags & ts.TypeFlags.Any) === 0) @@ -2485,7 +2487,7 @@ export function collectClassShapeInner( } staticMethods.set(member.name.text, { params: shapes, ret: ft.ret, member }); if ( - implicitMonoFile(decl.getSourceFile()) && + implicitMonoFile(ts.getSourceFileOfNode(decl)) && !functionLocalClass(decl) && inst === undefined && decl.typeParameters === undefined @@ -2542,7 +2544,7 @@ export function collectClassShapeInner( // read of an Error-rooted class is the checked-dynamic property // (fieldTarget's errorCause container), so only types that read // back unchanged from it are accepted. - if (!isJsSourceFile(member.getSourceFile())) { + if (!isJsSourceFile(ts.getSourceFileOfNode(member))) { if ( modifiers?.some( (m) => @@ -2705,6 +2707,24 @@ export function collectClassShapeInner( ); } } + // A completion callback declared with the broad `Function` type + // (`cb: Function`) takes the checked-dynamic callable the runtime + // passes implicitly typed callbacks; calls through it are + // dynamic calls of that callable. + const callbackIndex = STREAM_COMPLETION_CALLBACK_INDEX.get(name); + const callbackParam = + ts.isMethodDeclaration(member) && callbackIndex !== undefined + ? member.parameters.nodes[callbackIndex] + : undefined; + if ( + callbackParam !== undefined && + ts.isIdentifier(callbackParam.name) && + !callbackParam.dotDotDotToken && + !callbackParam.questionToken && + !callbackParam.initializer && + lowerer.checker.typeToString(lowerer.typeOf(callbackParam.name)) === "Function" + ) + lowerer.checkedCallbackParams.add(callbackParam); } } if (ts.isPropertyDeclaration(member)) { @@ -2743,10 +2763,12 @@ export function collectClassShapeInner( // unconditional `this[key] = ...` at the constructor's top // level), checked with the unguarded fields below. const declaredOnly = - !member.initializer && !isJsSourceFile(member.getSourceFile()) && !!member.type; + !member.initializer && !isJsSourceFile(ts.getSourceFileOfNode(member)) && !!member.type; if ( fieldName === null || - (!member.initializer && !isJsSourceFile(member.getSourceFile()) && !declaredOnly) + (!member.initializer && + !isJsSourceFile(ts.getSourceFileOfNode(member)) && + !declaredOnly) ) { lowerer.unsupported( "SC1090", @@ -2764,7 +2786,9 @@ export function collectClassShapeInner( ? (lowerer.mapTypeOf(inferred) ?? dynFallbackType(lowerer, member, inferred)) : DYN; let type = - mapped?.kind === "record" && isJsSourceFile(member.getSourceFile()) && !member.type + mapped?.kind === "record" && + isJsSourceFile(ts.getSourceFileOfNode(member)) && + !member.type ? DYN : mapped ? jsOpenObjectType(member, mapped, lowerer.shapes, lowerer.unions) @@ -2773,11 +2797,11 @@ export function collectClassShapeInner( !type || type.kind === "void" || (type.kind === "dyn" && - !isJsSourceFile(member.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(member)) && !lowerer.classFieldStoresDyn(inferred)) ) lowerer.badType(member, inferred); - if (isJsSourceFile(member.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(member))) { type = inheritedJsFieldType(lowerer, decl, fieldName, type); if (base?.fields.get(fieldName)?.kind === "dyn") type = DYN; if (base && !fields.has(fieldName) && lowerer.dynConvertible(type)) type = DYN; @@ -2891,7 +2915,7 @@ export function collectClassShapeInner( // as undefined on construction, verified), and reads/writes ride // the ordinary undefined-armed union machinery. let type = lowerer.irTypeOf(member.name); - if (isJsSourceFile(member.getSourceFile()) && !member.initializer) { + if (isJsSourceFile(ts.getSourceFileOfNode(member)) && !member.initializer) { const constructor = decl.members.nodes.find( (part): part is ts.ConstructorDeclaration => ts.isConstructorDeclaration(part) && !!part.body, @@ -2919,19 +2943,25 @@ export function collectClassShapeInner( // Unannotated JavaScript callback slots retain the original // callable and its receiver instead of adapting its signature. if ( - isJsSourceFile(member.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(member)) && !member.type && - !/@type\b/.test(member.getSourceFile().text.slice(member.pos, member.getStart())) && + !/@type\b/.test( + ts.getSourceFileOfNode(member).text.slice(member.pos, ts.tokenStartOfNode(member)), + ) && (type.kind === "func" || type.kind === "record") && lowerer.dynConvertible(type) ) type = DYN; // JS permits reads before a constructor's first assignment even // when its inferred field type omits undefined. - if (isJsSourceFile(member.getSourceFile()) && !member.initializer && type.kind !== "dyn") { + if ( + isJsSourceFile(ts.getSourceFileOfNode(member)) && + !member.initializer && + type.kind !== "dyn" + ) { type = lowerer.withUndefinedArmOf(type) ?? DYN; } - if (isJsSourceFile(member.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(member))) { type = inheritedJsFieldType(lowerer, decl, member.name.text, type); if (base?.fields.get(member.name.text)?.kind === "dyn") type = DYN; if (base && !fields.has(member.name.text) && lowerer.dynConvertible(type)) type = DYN; @@ -2942,7 +2972,7 @@ export function collectClassShapeInner( // storage as implicit constructor-assigned JS fields. if ( type.kind === "dyn" && - !isJsSourceFile(member.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(member)) && !lowerer.classFieldStoresDyn(lowerer.typeOf(member.name)) ) { lowerer.unsupported( @@ -3214,7 +3244,7 @@ export function collectClassShapeInner( // name (the override stays on the vtable), a shadowed field, or // a generic-class instantiation's member. if ( - implicitMonoFile(decl.getSourceFile()) && + implicitMonoFile(ts.getSourceFileOfNode(decl)) && !functionLocalClass(decl) && ts.isIdentifier(member.name) && !lowerer.virtualJsMethods.has(member) && @@ -3255,14 +3285,14 @@ export function collectClassShapeInner( // JS inference may retain only a symbol sentinel while a method // also returns values from an untyped mutable field or local. if ( - isJsSourceFile(member.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(member)) && !member.type && !hasExplicitJsDocReturn(member) && ft.ret.kind === "symbol" ) ft.ret = DYN; if ( - isJsSourceFile(member.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(member)) && lowerer.prototypeMethodAccesses.has(mName) && !lowerer.findMethodOn(base, mName)?.declarer.def.runtime && shapes.every((p) => p.mode === "required" || p.mode === "omittable") @@ -3316,7 +3346,7 @@ export function collectClassShapeInner( // against the STATIC receiver's shape, so `m(x?: number)` and // `m(x: number | undefined)` interchange soundly in overrides. const overridden = lowerer.findMethodOn(base, mName); - if (overridden && isJsSourceFile(member.getSourceFile())) { + if (overridden && isJsSourceFile(ts.getSourceFileOfNode(member))) { // An unannotated JS override can return a checked value into // the inherited native ABI, just as its untyped parameters do. // Body lowering inserts the conversion at each return. @@ -3364,7 +3394,7 @@ export function collectClassShapeInner( // readers keep the exact-signature fence. if ( overridden && - isJsSourceFile(member.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(member)) && shapes.length < overridden.sig.params.length && !bodyReadsArguments(member) && shapes.every( @@ -3392,9 +3422,9 @@ export function collectClassShapeInner( // a statically typed receiver see the same ABI. if ( overridden && - !isJsSourceFile(member.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(member)) && overridden.declarer.decl !== null && - isJsSourceFile(overridden.declarer.decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(overridden.declarer.decl)) && member.asteriskToken === undefined && !overridden.sig.async && !overridden.sig.gen && @@ -3445,9 +3475,9 @@ export function collectClassShapeInner( )) ) { const fencedJsOverride = - isJsSourceFile(member.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(member)) && overridden.declarer.decl !== null && - isJsSourceFile(overridden.declarer.decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(overridden.declarer.decl)) && overridden.sig.gen === undefined; // A TypeScript override that only changes the result, into a // type that converts into the inherited one (`Promise` over @@ -3497,7 +3527,7 @@ export function collectClassShapeInner( // into the inherited type (`toString() { return // this.message }` over Error's). (ft.ret.kind === "dyn" && - isJsSourceFile(member.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(member)) && overridden.sig.ret.kind !== "void" && canDynCheckTo( overridden.sig.ret, @@ -3571,7 +3601,7 @@ export function collectClassShapeInner( overridden && !returnAdapters.has(mName) && (asyncMember || overridden.sig.async === true) && - !isJsSourceFile(member.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(member)) && !functionLocalClass(decl) && member.asteriskToken === undefined && overridden.sig.gen === undefined && @@ -3593,7 +3623,7 @@ export function collectClassShapeInner( (asyncMember || overridden.sig.async === true) ) { const message = `overriding ${overridden.sig.async === true ? "the async method" : "a method with an async method"} '${mName}' (async methods dispatch statically — the vtable slot machinery has no fiber-spawn story)`; - if (isJsSourceFile(member.getSourceFile())) + if (isJsSourceFile(ts.getSourceFileOfNode(member))) methodEntryFences.set(mName, unsupportedDiag("SC1090", locOf(member.name), message)); else lowerer.unsupported("SC1090", member.name, message); } @@ -3602,7 +3632,7 @@ export function collectClassShapeInner( (member.asteriskToken !== undefined || overridden.sig.gen !== undefined) ) { const message = `overriding the generator method '${mName}' (generator methods dispatch statically — a virtual slot cannot enter their spawn wrapper)`; - if (isJsSourceFile(member.getSourceFile())) + if (isJsSourceFile(ts.getSourceFileOfNode(member))) methodEntryFences.set(mName, unsupportedDiag("SC1090", locOf(member.name), message)); else lowerer.unsupported("SC1090", member.name, message); } @@ -3674,7 +3704,7 @@ export function collectClassShapeInner( const mName = `${isGet ? "get" : "set"}:${prop}`; if ( fields.has(prop) && - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && base?.fields.has(prop) && !fieldOrder.some((field) => field.name === prop) ) { @@ -3989,7 +4019,7 @@ export function collectClassShapeInner( // assignment ran — the zeroed-memory trap a TS declaration order // forbids via strictPropertyInitialization — so they keep a named // fence instead of reading undefined without a diagnostic. - if (isJsSourceFile(decl.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(decl))) { const hasAccessor = (name: string): boolean => methods.has(`get:${name}`) || methods.has(`set:${name}`) || @@ -4013,7 +4043,7 @@ export function collectClassShapeInner( // goes through the key. const lateBoundByKey = new Map(); for (const p of instType ? lowerer.checker.getPropertiesOfType(instType) : []) { - if (!p.name.startsWith("__@")) continue; + if (!ts.symbolName(p).startsWith("__@")) continue; const keySym = lateBoundKeySymOf(lowerer, p); if (keySym) lateBoundByKey.set(keySym, p); } @@ -4069,7 +4099,7 @@ export function collectClassShapeInner( if ( t && (t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0 && - isJsSourceFile(decl.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(decl)) ) type = DYN; if (type) type = jsOpenObjectType(assign, type, lowerer.shapes, lowerer.unions); @@ -4187,10 +4217,14 @@ export function collectClassShapeInner( // static layouts cannot represent. Named fence, at the first // assignment site. for (const p of instType ? lowerer.checker.getPropertiesOfType(instType) : []) { - if (errorRooted && p.name === "cause") continue; - if (fields.has(p.name) || methods.has(p.name)) continue; - if (hasAccessor(p.name)) continue; - if (base && (base.fields.has(p.name) || lowerer.findMethodOn(base, p.name))) continue; + if (errorRooted && ts.symbolName(p) === "cause") continue; + if (fields.has(ts.symbolName(p)) || methods.has(ts.symbolName(p))) continue; + if (hasAccessor(ts.symbolName(p))) continue; + if ( + base && + (base.fields.has(ts.symbolName(p)) || lowerer.findMethodOn(base, ts.symbolName(p))) + ) + continue; const site = p.declarations.find( (d) => ts.isPropertyAccessExpression(d) || @@ -4204,7 +4238,7 @@ export function collectClassShapeInner( // identities (symbol parameters, mutable bindings, computed // descriptions) or assignments outside the constructor's top // level. - if (p.name.startsWith("__@")) { + if (ts.symbolName(p).startsWith("__@")) { const keySym = lateBoundKeySymOf(lowerer, p); const key = keySym ? classSymbolKeyOfSymbol(lowerer, keySym) : null; if (key && symbolFields.has(key.identity)) continue; @@ -4224,24 +4258,26 @@ export function collectClassShapeInner( // Unmappable inferences and arm-less kinds keep the fence. // TypeScript classes keep the compile-time fence too: an annotated // program can spell `T | undefined` itself. - if (isJsSourceFile(decl.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(decl))) { const original = undefArmedFieldType(lowerer, p); const armed = - original === null ? null : inheritedJsFieldType(lowerer, decl, p.name, original); + original === null + ? null + : inheritedJsFieldType(lowerer, decl, ts.symbolName(p), original); if (armed !== null) { // Unknown JS properties use the instance bag: they do not // exist until assigned, and a subclass may declare the same // name as an ordinary layout field. if (armed.kind === "dyn") continue; - fields.set(p.name, armed); - fieldOrder.push({ name: p.name, type: armed, initializer: undefined }); + fields.set(ts.symbolName(p), armed); + fieldOrder.push({ name: ts.symbolName(p), type: armed, initializer: undefined }); continue; } } lowerer.unsupported( "SC1090", site, - `fields assigned outside the constructor's top level ('this.${p.name}' would be undefined until the first assignment runs — assign it unconditionally at the top of the constructor)`, + `fields assigned outside the constructor's top level ('this.${ts.symbolName(p)}' would be undefined until the first assignment runs — assign it unconditionally at the top of the constructor)`, ); } } @@ -4284,7 +4320,7 @@ export function collectClassShapeInner( if ( ctor && !mixinForwarding && - isJsSourceFile(ctor.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(ctor)) && !ctorParams.some((shape) => shape.mode === "rest" || shape.mode === "dynRest") && bodyReadsArguments(ctor) ) { @@ -4679,7 +4715,7 @@ export function lowerStaticFieldInits(lowerer: Lowerer, info: ClassInfo): IrStmt // Keep their refusal attached to the class declaration instead of // letting a poisoned initializer escape the module lowering pass. if (!(error instanceof PoisonError)) throw error; - if (info.decl && isJsSourceFile(info.decl.getSourceFile())) { + if (info.decl && isJsSourceFile(ts.getSourceFileOfNode(info.decl))) { const fence = lowerer.diagSink === null ? lowerer.deferToRuntimeFence(diagsBefore, info.decl, { kind: "statement" }) @@ -4806,8 +4842,16 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[] | { pos: number; kind: "field"; f: ClassInfo["staticFields"][number] } | { pos: number; kind: "block"; b: ts.ClassStaticBlockDeclaration }; const items: Item[] = [ - ...info.staticFields.map((f): Item => ({ pos: f.initializer.getStart(), kind: "field", f })), - ...(info.staticBlocks ?? []).map((b): Item => ({ pos: b.getStart(), kind: "block", b })), + ...info.staticFields.map((f): Item => ({ + pos: ts.tokenStartOfNode(f.initializer), + kind: "field", + f, + })), + ...(info.staticBlocks ?? []).map((b): Item => ({ + pos: ts.tokenStartOfNode(b), + kind: "block", + b, + })), ].sort((a, b) => a.pos - b.pos); for (const item of items) { if (item.kind === "block") { @@ -4854,7 +4898,7 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[] } catch (e) { if (!(e instanceof PoisonError)) throw e; lowerer.countStatementFailed(f.initializer, locOf(f.initializer).file); - if (isJsSourceFile(f.initializer.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(f.initializer))) { const fence = lowerer.diagSink === null ? lowerer.deferToRuntimeFence(diagsBefore, f.initializer, { kind: "statement" }) @@ -4913,7 +4957,7 @@ export function analyzeClassDecoration(lowerer: Lowerer, info: ClassInfo): void ts.isVariableDeclaration(vdecl) && vdecl.initializer === undefined && (ts.getCombinedModifierFlags(vdecl) & ts.ModifierFlags.Ambient) !== 0 && - !vdecl.getSourceFile().isDeclarationFile; + !ts.getSourceFileOfNode(vdecl).isDeclarationFile; if (ambientVar || ambientUndefinedFnSymbolOf(lowerer, ambientCallee) !== null) { shapes.push({ kind: "ambientThrow", name: ambientCallee.text }); continue; @@ -5536,7 +5580,7 @@ export function lowerStaticFieldRead( // JavaScript often assigns public statics after the class declaration. // Their storage stays mutable and checked, while reads retain the // checker's concrete type instead of losing the method receiver. - const type = isJsSourceFile(expr.getSourceFile()) + const type = isJsSourceFile(ts.getSourceFileOfNode(expr)) ? null : lowerer.mapTypeOf(lowerer.typeOf(expr)); return type ? lowerer.coerceToExpected(value, type) : value; @@ -5577,7 +5621,7 @@ function namedEvaluationName(expr: ts.ClassLikeDeclaration): string { * (private-brands.ts), so the class must be a named declaration (undici's * webidl FastIterableIterator). The checker rejects writes to that binding. */ function localPrivateFieldsAgree(expr: ts.ClassLikeDeclaration): boolean { - if (!isJsSourceFile(expr.getSourceFile()) || !ts.isClassDeclaration(expr) || !expr.name) + if (!isJsSourceFile(ts.getSourceFileOfNode(expr)) || !ts.isClassDeclaration(expr) || !expr.name) return false; if ( expr.members.nodes.some( @@ -5685,7 +5729,7 @@ export function lowerClassExpressionInfo( expr.members.nodes.some( (member) => decoratorNodesOf(member).length || - (!isJsSourceFile(expr.getSourceFile()) && + (!isJsSourceFile(ts.getSourceFileOfNode(expr)) && (ts.isClassStaticBlockDeclaration(member) || ts .getModifiers(member) @@ -5809,7 +5853,7 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara decoratorNodesOf(expr).length > 0 || expr.members.nodes.some((m) => decoratorNodesOf(m).length > 0) ) { - if (!isJsSourceFile(expr.getSourceFile()) && expr.typeParameters === undefined) { + if (!isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.typeParameters === undefined) { const thrown = guaranteedDecorationThrow(lowerer, expr); if (thrown) { // The expression's static type never materializes — the read @@ -6095,12 +6139,12 @@ function enclosingTopLevelClassAssignment(expr: ts.ClassExpression): ts.BinaryEx * `Common.I`): a second assignment makes the binding dynamic, so * callers refuse to pin the first one's class. */ function countAssignmentsTo(assign: ts.BinaryExpression): number { - const sf = assign.getSourceFile(); + const sf = ts.getSourceFileOfNode(assign); // `exports.I` and `module.exports.I` are the SAME binding in Node // (exports aliases module.exports until a table replaces it) — fold // the member spellings together before comparing. const canon = (lhs: ts.Expression): string => { - const text = lhs.getText().replace(/\s+/g, ""); + const text = ts.getTextOfNode(lhs).replace(/\s+/g, ""); return text.startsWith("module.exports.") ? text.slice("module.".length) : text; }; const target = canon(assign.left); @@ -6158,10 +6202,10 @@ export function lowerStaticMethodCall( if (!ts.isIdentifier(access.expression)) { if ( !isModuleExportsAccess(access.expression) || - !isCjsJsFile(access.getSourceFile(), lowerer.program) + !isCjsJsFile(ts.getSourceFileOfNode(access), lowerer.program) ) { let receiverType = lowerer.mapTypeOf(lowerer.typeOf(access.expression)); - if (receiverType?.kind !== "classval" && isJsSourceFile(call.getSourceFile())) { + if (receiverType?.kind !== "classval" && isJsSourceFile(ts.getSourceFileOfNode(call))) { const receiver = tryLowerExpression(lowerer, access.expression); if (receiver?.type.kind === "classval") receiverType = receiver.type; } @@ -6182,7 +6226,7 @@ export function lowerStaticMethodCall( loc: locOf(call), }; } - const whole = cjsClassExprWholeExportOf(access.getSourceFile()); + const whole = cjsClassExprWholeExportOf(ts.getSourceFileOfNode(access)); if (!whole) return null; return staticCallOn( lowerer, @@ -6479,9 +6523,9 @@ function hasLexicalSuper(root: ts.Node): boolean { ts.isClassExpression(node) ) return; - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - root.forEachChild(visit); + ts.forEachChild(root, visit); return found; } @@ -6510,9 +6554,9 @@ function capturesEnclosingScope( if (decl && decl !== scope && inside(decl, scope) && !inside(decl, member)) captured = true; return; } - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - member.forEachChild(visit); + ts.forEachChild(member, visit); return captured; } @@ -6635,7 +6679,10 @@ export function lowerClassValueProperty( // `module.exports.label` in a class-replaced CJS module: the // receiver is the exact exported class, and the read is // side-effect-free — as bindable as an identifier. - !(isModuleExportsAccess(expr.expression) && isCjsJsFile(expr.getSourceFile(), lowerer.program)) + !( + isModuleExportsAccess(expr.expression) && + isCjsJsFile(ts.getSourceFileOfNode(expr), lowerer.program) + ) ) { // Devirtualized reads DISCARD the receiver value, so only // side-effect-free receivers are claimed (the instanceOf fold @@ -6730,14 +6777,22 @@ function thunkAsyncSlot( ): void { if (overridden.sig.async !== true) return; const declarer = overridden.declarer; + // The slot answers the body's promise: an untyped async method's `dyn` + // result is the promise's payload, so the slot's ABI is that promise. An + // async generator method's slot keeps its generator object. + const ret: IrType = + overridden.sig.gen || overridden.sig.ret.kind === "promise" + ? overridden.sig.ret + : { kind: "promise", inner: overridden.sig.ret }; declarer.returnAdapters ??= new Map(); if (!declarer.returnAdapters.has(mName)) { declarer.returnAdapters.set(mName, { params: [...overridden.sig.params], - ret: overridden.sig.ret, + ret, async: true, }); } + overridden.sig.ret = ret; delete overridden.sig.async; } @@ -6899,7 +6954,7 @@ export function collectInheritedJsFieldWrites( if (name !== null) addWrite(name, part.right); } } - part.forEachChild(scan); + ts.forEachChild(part, scan); }; for (const member of node.members.nodes) { if ( @@ -6940,7 +6995,7 @@ export function collectInheritedJsFieldWrites( if ( !owner || (!ts.isClassDeclaration(owner) && !ts.isClassExpression(owner)) || - !isJsSourceFile(owner.getSourceFile()) + !isJsSourceFile(ts.getSourceFileOfNode(owner)) ) continue; let fields = lowerer.inheritedJsFieldWrites.get(owner); @@ -6978,7 +7033,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou if ( node.heritageClauses?.nodes.length && accessors.length && - isJsSourceFile(node.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(node)) ) { const symbol = lowerer.typeOf(node).getSymbol(); const instance = symbol ? lowerer.checker.getDeclaredTypeOfSymbol(symbol) : null; @@ -6995,7 +7050,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou if ( (ts.isGetAccessorDeclaration(declaration) || ts.isSetAccessorDeclaration(declaration)) && - isJsSourceFile(declaration.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(declaration)) ) { lowerer.checkedVirtualJsAccessors.add(declaration); lowerer.checkedVirtualJsAccessors.add(member); @@ -7037,12 +7092,12 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou for (const declaration of property ? property.declarations : []) { if ( ts.isMethodDeclaration(declaration) && - isJsSourceFile(declaration.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(declaration)) && !checkedJsOverride(lowerer, declaration, member) ) { lowerer.virtualJsMethods.add(declaration); if ( - isJsSourceFile(member.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(member)) && !member.typeParameters?.nodes && !member.asteriskToken && !member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) && @@ -7088,7 +7143,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou if (node !== member && ts.isFunctionLike(node)) return; if (ts.isReturnStatement(node) && node.expression) lowerer.checkedVirtualJsValueReturns.add(member); - node.forEachChild(scan); + ts.forEachChild(node, scan); }; scan(member); } @@ -7121,7 +7176,7 @@ function checkedJsOverride( ): boolean { return [base, member].every( (method) => - lowerer.checkedJsFile(method.getSourceFile()) && + lowerer.checkedJsFile(ts.getSourceFileOfNode(method)) && method.parameters.nodes.every(documentedParameter), ); } @@ -7269,7 +7324,7 @@ export function lowerClassGenericMethodCall( result.type.kind === "object" && !ts.isExpressionStatement(call.parent) && ts.isMethodDeclaration(info.decl) && - isJsSourceFile(info.decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(info.decl)) && returnsOnlyThisValue(info.decl) ) { return lowerer.maybeNarrow(result, call); @@ -7374,7 +7429,7 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction out.push(lowerer.lowerClassCtor(info)); } catch (e) { if (!(e instanceof PoisonError)) throw e; - if (info.decl && isJsSourceFile(info.decl.getSourceFile())) { + if (info.decl && isJsSourceFile(ts.getSourceFileOfNode(info.decl))) { const types: IrType[] = [ { kind: "object", className }, ...info.ctorParams.map((param) => param.type), @@ -8287,7 +8342,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL } catch (e) { if (!(e instanceof PoisonError)) throw e; lowerer.countStatementFailed(f.initializer, locOf(f.initializer).file); - if (isJsSourceFile(f.initializer.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(f.initializer))) { const fence = lowerer.diagSink === null ? lowerer.deferToRuntimeFence(diagsBefore, f.initializer, { kind: "statement" }) @@ -8786,7 +8841,7 @@ export function lowerSuperMethodCall( kind: "dynCall", callee, receiver: lowerer.coerceToExpected(thisRef, DYN), - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), type: DYN, loc, @@ -8961,7 +9016,7 @@ export function inheritedBuiltinCtorFence( lowerer.runtimeFences.push(diag); const line = info.decl !== null - ? ts.getLineAndCharacterOfPosition(info.decl.getSourceFile(), at.start).line + 1 + ? ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(info.decl), at.start).line + 1 : null; const where = line !== null ? ` [${diag.code} at ${at.file}:${line}]` : ""; return { kind: "runtimeFence", code: diag.code, message: `${diag.message}${where}`, loc }; @@ -9164,6 +9219,11 @@ function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean { export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const loc = locOf(expr); + // A dependency's deprecated `new Buffer(...)` fallback (containers/bytes.ts). + { + const buffer = lowerNodeModulesBufferConstruct(lowerer, expr); + if (buffer) return buffer; + } // new Function(...): Node's EvalError under // --disallow-code-generation-from-strings (code-generation.ts). if (codeGenerationCalleeOf(lowerer, expr.expression) === "Function") @@ -9409,7 +9469,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // lowering ahead of the assignment statement). if ( !expr.expression.questionDotToken && - ((isCjsJsFile(expr.getSourceFile(), lowerer.program) && + ((isCjsJsFile(ts.getSourceFileOfNode(expr), lowerer.program) && (isModuleExportsAccess(expr.expression.expression) || (ts.isIdentifier(expr.expression.expression) && expr.expression.expression.text === "exports" && @@ -9425,7 +9485,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { memberSym, resolved, lowerer.resolveValueSymbol(expr.expression.name) ?? undefined, - lowerer.cjsModuleExportSymbol(expr.getSourceFile(), expr.expression.name.text), + lowerer.cjsModuleExportSymbol(ts.getSourceFileOfNode(expr), expr.expression.name.text), ]; const exportSym = candidates.find((s) => s !== undefined); const g = candidates @@ -9640,7 +9700,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // libregexp engine the literals ride. The pattern compiles EAGERLY, // so bad input throws Node's catchable SyntaxError at construction. // Checked arguments retain native regexes and perform runtime coercion. - if (symbol && symbol.name === "RegExp" && lowerer.isStdlibSymbol(symbol)) { + if (symbol && ts.symbolName(symbol) === "RegExp" && lowerer.isStdlibSymbol(symbol)) { const args = expr.arguments?.nodes ?? []; if (args.length > 2) { lowerer.noLowering(`new RegExp with ${args.length} arguments`, expr); @@ -9708,19 +9768,19 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { loc, }; } - if (symbol && symbol.name === "URL" && lowerer.isStdlibSymbol(symbol)) { + if (symbol && ts.symbolName(symbol) === "URL" && lowerer.isStdlibSymbol(symbol)) { return lowerUrlNew(lowerer, expr); } // `new URLSearchParams(init?)`: the WHATWG list (stdlib provenance — // see lowerSearchParamsNew for the lowered init shapes). - if (symbol && symbol.name === "URLSearchParams" && lowerer.isStdlibSymbol(symbol)) { + if (symbol && ts.symbolName(symbol) === "URLSearchParams" && lowerer.isStdlibSymbol(symbol)) { return lowerSearchParamsNew(lowerer, expr, loc); } // Date's read-only value slice: store the constructor's TimeClip'd // epoch milliseconds as the scalar date kind. That is sufficient // for every getter and toISOString; identity and setters stay // fenced, so copying the scalar cannot create an observable lie. - if (symbol && symbol.name === "Date" && lowerer.isStdlibSymbol(symbol)) { + if (symbol && ts.symbolName(symbol) === "Date" && lowerer.isStdlibSymbol(symbol)) { const args = expr.arguments?.nodes ?? []; if (args.some(ts.isSpreadElement)) { lowerer.noLowering( @@ -9737,7 +9797,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // pass numbers, completed with the spec's defaults (date 1, time // parts 0) for the seven-f64 ABI. if (args.length > 1) { - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { return { kind: "libCall", fn: "date.nativeNewFields", @@ -9768,7 +9828,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { } return { kind: "libCall", fn: "date.newLocal", args: fields, type: DATE_T, loc }; } - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { return { kind: "libCall", // One argument: a string parses with the full date parser. @@ -9813,7 +9873,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // normalized at run time (Node's normalizeEncoding, which throws // ERR_UNKNOWN_ENCODING for an unknown name). Omitted means utf8, // Node's default. - if (symbol && symbol.name === "StringDecoder" && lowerer.isStdlibSymbol(symbol)) { + if (symbol && ts.symbolName(symbol) === "StringDecoder" && lowerer.isStdlibSymbol(symbol)) { const args = expr.arguments?.nodes ?? []; if (args.length > 1) { lowerer.noLowering("new StringDecoder with 2 arguments", expr, undefined, symbol); @@ -9847,12 +9907,13 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { loc, }; } + const codec = symbol ? ts.symbolName(symbol) : undefined; if ( symbol && - (symbol.name === "TextDecoder" || symbol.name === "TextEncoder") && + (codec === "TextDecoder" || codec === "TextEncoder") && lowerer.isStdlibSymbol(symbol) ) { - return lowerTextCodecNew(lowerer, expr, symbol.name); + return lowerTextCodecNew(lowerer, expr, codec); } // `new Uint8Array(...)` / `new Uint32Array(...)` / `new // Float32Array(...)` / `new Float64Array(...)`: the typed-array @@ -9886,11 +9947,11 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // types get their half named specifically instead of the component // fence (SC2009, which names Map slots at value positions elsewhere). // Both Array() and new Array() share the elements/count lowering. - if (symbol?.name === "Object" && lowerer.isStdlibSymbol(symbol)) { + if (symbol !== null && ts.symbolName(symbol) === "Object" && lowerer.isStdlibSymbol(symbol)) { const object = lowerObjectConstructor(lowerer, expr.arguments?.nodes ?? [], loc); if (object) return object; } - if (symbol?.name === "Array" && lowerer.isStdlibSymbol(symbol)) { + if (symbol !== null && ts.symbolName(symbol) === "Array" && lowerer.isStdlibSymbol(symbol)) { return lowerArrayConstructor(lowerer, expr, expr.arguments?.nodes ?? []); } // A const alias (`const M = Map`) constructs the same Map. @@ -9906,7 +9967,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { : null; let tsType = lowerer.typeOf(expr); let mapped = lowerer.mapTypeOf(tsType); - if (mapped === null && isJsSourceFile(expr.getSourceFile())) mapped = lowerer.irTypeOf(expr); + if (mapped === null && isJsSourceFile(ts.getSourceFileOfNode(expr))) + mapped = lowerer.irTypeOf(expr); const fieldType = assignedThisFieldType(lowerer, expr); if (mapped?.kind !== "map" && fieldType?.kind === "map") mapped = fieldType; // A fresh map built from pair literals can use its destination's @@ -9968,7 +10030,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // unconditionally; the format path never touches them. TypeScript // keeps the compile fence. if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && (expr.arguments?.nodes.length ?? 0) === 0 && targs.length > 0 && targs.every((t) => (t.flags & ts.TypeFlags.Any) !== 0) @@ -10004,7 +10066,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { } // Weak containers share checked native key identities and lifetime hooks. if ( - (symbol?.name === "WeakMap" || symbol?.name === "WeakSet") && + ((symbol !== null && ts.symbolName(symbol) === "WeakMap") || + (symbol !== null && ts.symbolName(symbol) === "WeakSet")) && lowerer.isStdlibSymbol(symbol) ) { const args = expr.arguments?.nodes ?? []; @@ -10012,13 +10075,13 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { lowerer.unsupported( "SC1090", expr, - `${symbol.name} construction with surplus or spread arguments`, + `${ts.symbolName(symbol)} construction with surplus or spread arguments`, ); } return { kind: "libCall", - fn: symbol.name === "WeakMap" ? "weakMap.new" : "weakSet.new", + fn: ts.symbolName(symbol) === "WeakMap" ? "weakMap.new" : "weakSet.new", args: [args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc)], type: DYN, loc, @@ -10037,7 +10100,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { } if ( !mapped && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && (expr.arguments?.nodes.length ?? 0) <= 1 && !expr.arguments?.nodes.some(ts.isSpreadElement) ) { @@ -10060,7 +10123,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // the honest construction is a Set of those scalars. if ( !mapped && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && (expr.arguments?.nodes.length ?? 0) === 1 && ts.isArrayLiteralExpression(expr.arguments!.nodes[0]!) && !(expr.arguments!.nodes[0] as ts.ArrayLiteralExpression).elements.nodes.some( @@ -10103,7 +10166,11 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // `new AsyncLocalStorage()` (node:async_hooks): a fresh store id — // an f64 handle (type-mapper.ts), the Channel story. Construction options // ({ defaultValue, name }) have no lowering yet. - if (symbol?.name === "AsyncLocalStorage" && lowerer.isStdlibSymbol(symbol)) { + if ( + symbol !== null && + ts.symbolName(symbol) === "AsyncLocalStorage" && + lowerer.isStdlibSymbol(symbol) + ) { if ((expr.arguments?.nodes.length ?? 0) > 0) { lowerer.noLowering( "new AsyncLocalStorage(options)", @@ -10114,7 +10181,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { return { kind: "libCall", fn: "als.new", args: [], type: F64, loc }; } // `new Promise((resolve) => ...)`: the ambient Promise constructor. - if (symbol?.name === "Promise" && lowerer.isStdlibSymbol(symbol)) { + if (symbol !== null && ts.symbolName(symbol) === "Promise" && lowerer.isStdlibSymbol(symbol)) { const type = lowerer.irTypeOf(expr); if (type.kind !== "promise") lowerer.badType(expr, lowerer.typeOf(expr)); const args = expr.arguments?.nodes ?? []; @@ -10130,7 +10197,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if (ts.isIdentifier(a0) && a0.text === "setImmediate") { const sym = lowerer.checker.getSymbolAtLocation(a0); 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))) + ) { // The settled value is the undefined dyn value — the result // is promise whatever T the checker inferred for the // unusual executor (Promise in the suite's shape). @@ -10151,7 +10221,9 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // catch-side instanceof and the uncaught printer working). First // settle wins, exactly JS: reject-after-resolve and double-reject // are no-ops, and an executor throw after any settle is swallowed. - const executor = lowerer.lowerExpr(args[0]!); + let executor = lowerer.lowerExpr(args[0]!); + if (executor.type.kind !== "func") lowerer.badType(args[0]!, lowerer.typeOf(args[0]!)); + executor = adaptPromiseExecutor(lowerer, executor, type.inner, loc) ?? executor; if (executor.type.kind !== "func") lowerer.badType(args[0]!, lowerer.typeOf(args[0]!)); if (executor.type.params.length > 1) { const rj = executor.type.params[1]!; @@ -10459,7 +10531,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { if ( def.arms.every( (arm) => - arm.kind === "classval" || (isJsSourceFile(expr.getSourceFile()) && isUnitType(arm)), + arm.kind === "classval" || + (isJsSourceFile(ts.getSourceFileOfNode(expr)) && isUnitType(arm)), ) && lowerer.dynConvertible(calleeT) ) { @@ -10492,7 +10565,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const sym = raw && raw.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(raw) : raw; const hint = - sym && lowerer.isStdlibSymbol(sym) ? own(STDLIB_CTOR_HINTS, sym.name) : undefined; + sym && lowerer.isStdlibSymbol(sym) ? own(STDLIB_CTOR_HINTS, ts.symbolName(sym)) : undefined; if (hint !== undefined) { lowerer.unsupported("SC1090", expr, hint); } diff --git a/packages/compiler/src/frontend/lowering/lower-comptime.ts b/packages/compiler/src/frontend/lowering/lower-comptime.ts index f239eea82..e442b34a5 100644 --- a/packages/compiler/src/frontend/lowering/lower-comptime.ts +++ b/packages/compiler/src/frontend/lowering/lower-comptime.ts @@ -86,7 +86,7 @@ export function lowerComptime(lowerer: Lowerer, expr: ts.CallExpression): IrExpr try { if (lowerer.frontendServices === undefined) throw new Error("compile-time evaluation requires frontend services"); - result = lowerer.frontendServices.evaluateComptime(cb.getText(), COMPTIME_TIMEOUT_MS); + result = lowerer.frontendServices.evaluateComptime(ts.getTextOfNode(cb), COMPTIME_TIMEOUT_MS); } catch (e) { const detail = comptimeFailureDetail(e); lowerer.pushDiag(comptimeFailedDiag(detail, locOf(cb))); @@ -226,7 +226,7 @@ export function rejectComptimeCaptures( lowerer: Lowerer, cb: ts.ArrowFunction | ts.FunctionExpression, ): void { - const sf = cb.getSourceFile(); + const sf = ts.getSourceFileOfNode(cb); const inTypePosition = (node: ts.Node): boolean => { for (let p: ts.Node | undefined = node.parent; p && p !== cb; p = p.parent) { if (ts.isTypeNode(p)) return true; @@ -249,7 +249,7 @@ export function rejectComptimeCaptures( const decls = symbol ? symbol.declarations : undefined; if (symbol && decls && decls.length > 0) { const ownLocal = decls.every( - (d) => d.getSourceFile() === sf && d.pos >= cb.pos && d.end <= cb.end, + (d) => ts.getSourceFileOfNode(d) === sf && d.pos >= cb.pos && d.end <= cb.end, ); if (!ownLocal) { if (node.text === "comptime" && lowerer.isStdlibSymbol(symbol)) { diff --git a/packages/compiler/src/frontend/lowering/lower-console.ts b/packages/compiler/src/frontend/lowering/lower-console.ts index a7fac4f51..eec308342 100644 --- a/packages/compiler/src/frontend/lowering/lower-console.ts +++ b/packages/compiler/src/frontend/lowering/lower-console.ts @@ -209,7 +209,7 @@ export function lowerConsoleInstanceCall( if (access.questionDotToken) return null; const recvType = lowerer.typeOf(access.expression); const sym = recvType.getAliasSymbol() ?? recvType.getSymbol(); - if (!sym || sym.name !== "Console" || !lowerer.isStdlibSymbol(sym)) return null; + if (!sym || ts.symbolName(sym) !== "Console" || !lowerer.isStdlibSymbol(sym)) return null; const toStderr = OUTPUT_MEMBERS.get(access.name.text); if (toStderr === undefined) { lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index 17522971d..4cb238352 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -195,7 +195,7 @@ export function lowerDgramDnsModuleCall( sawType = true; } else if (name === "reuseAddr") { reuseAddr = lowerer.lowerExprExpecting(prop.initializer, BOOL); - } else if (name === "signal" && isJsSourceFile(expr.getSourceFile())) { + } else if (name === "signal" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { // A provably-non-AbortSignal signal (the invalid-input probes: // strings, numbers, plain records) throws Node's // validateAbortSignal ladder; plausible signal values keep the @@ -578,7 +578,7 @@ export function lowerDgramMethodCall( return { kind: "libCall", fn, args: [receiver, data, port, host], type: VOID, loc }; } if ( - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && args.length <= 5 && !args.some(ts.isSpreadElement) ) { diff --git a/packages/compiler/src/frontend/lowering/lower-enums.ts b/packages/compiler/src/frontend/lowering/lower-enums.ts index cc2797fb9..e72dd2780 100644 --- a/packages/compiler/src/frontend/lowering/lower-enums.ts +++ b/packages/compiler/src/frontend/lowering/lower-enums.ts @@ -36,14 +36,14 @@ import { nodeThrowExpr } from "../../ir/build.js"; * the only name forms tsc allows on enum members). */ function memberNameText(member: ts.EnumMember): string { const n = member.name; - return ts.isIdentifier(n) || ts.isStringLiteralLike(n) ? n.text : n.getText(); + return ts.isIdentifier(n) || ts.isStringLiteralLike(n) ? n.text : ts.getTextOfNode(n); } /** Ambient: a `declare`d declaration, or one living in a .d.ts. */ function isAmbientEnumDecl(decl: ts.EnumDeclaration): boolean { return ( (ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Ambient) !== 0 || - decl.getSourceFile().isDeclarationFile + ts.getSourceFileOfNode(decl).isDeclarationFile ); } @@ -126,7 +126,7 @@ function lowerEnumMemberRead( // runtime values, so SC2020-family member fences own those. A user's // `declare enum` follows the // `declare const __VERSION__` stance: ReferenceError at the access. - const sf = enumDecl.getSourceFile(); + const sf = ts.getSourceFileOfNode(enumDecl); if (lowerer.isStdlibFile(sf)) return null; const declared = lowerer.mapTypeOf(lowerer.typeOf(expr)); if (!declared || declared.kind === "void") return null; @@ -165,10 +165,10 @@ function lowerEnumReverseRead( key: ts.Expression, ): IrExpr | null { const loc = locOf(expr); - const enumName = recvSym.name; + const enumName = ts.symbolName(recvSym); // Ambient: no object exists — the member-read stance. if (decls.every((d) => isAmbientEnumDecl(d))) { - if (decls.some((d) => lowerer.isStdlibFile(d.getSourceFile()))) { + if (decls.some((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d)))) { return null; } return { diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index 4b8b9ea14..f8588fe48 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -72,6 +72,8 @@ import { STREAM_FORCED_EVENT_NAMES, streamForcedTuple, streamSidesOf } from "./l import { boolLit, strLit, varRef } from "../../ir/build.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; import { lowerAddAbortListener } from "./builtin-calls.js"; +import { requestGeneralStreamSurface } from "./general-streams.js"; +import { nodeStreamInterfaceSide } from "./dyn-streams.js"; /** True when the class descends from (or is) the runtime emitter. */ export function emitterRooted(lowerer: Lowerer, info: ClassInfo | undefined | null): boolean { @@ -412,7 +414,11 @@ function isErrorMonitorRef(lowerer: Lowerer, arg: ts.Expression): boolean { const isMonitorType = (t: ts.Type): boolean => { if (!(t.flags & ts.TypeFlags.UniqueESSymbol)) return false; const symbol = t.getSymbol(); - return symbol?.name === "errorMonitor" && lowerer.isStdlibSymbol(symbol); + return ( + symbol !== undefined && + ts.symbolName(symbol) === "errorMonitor" && + lowerer.isStdlibSymbol(symbol) + ); }; // The unique symbol TYPE names its declaration through any alias. if (isMonitorType(lowerer.typeOf(arg))) return true; @@ -1695,7 +1701,7 @@ export function emitOverrideShapeReason( const p0T = lowerer.mapTypeOf(lowerer.typeOf(p0.name)); const jsUnannotated = p0.type === undefined && - isJsSourceFile(member.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(member)) && (p0T === null || p0T.kind === "dyn"); if (p0T?.kind !== "string" && !jsUnannotated) { return "the event parameter must be typed 'string' (symbol event names have no lowering)"; @@ -1729,9 +1735,9 @@ export function emitOverrideShapeReason( "the event parameter cannot be reassigned (the forward's event name must stay pinned)"; } } - n.forEachChild(visit); + ts.forEachChild(n, visit); }; - member.body.forEachChild(visit); + ts.forEachChild(member.body, visit); return reason; } @@ -2204,36 +2210,90 @@ function eventsOnceBoxer( return { kind: "closure", fnName: name, captures: [], type: fnType, loc }; } -/** events.once(emitter, name): a promise of the next `name` event's - * arguments as an array, rejected by an earlier 'error' event (unless the - * awaited event is 'error'). The emitter's listeners are typed by the - * program-wide event table, so the runtime registers one-shot typed - * listeners whose compiler-built boxers turn the event's tuple into the - * array. EventTarget and stream receivers, computed names and options are - * refused. */ +/** The `signal` of an events.once options object literal (a checked- + * dynamic AbortSignal or undefined), or null when the literal names none. + * Other options forms are refused. */ +function eventsOnceSignal(lowerer: Lowerer, node: ts.Expression): IrExpr | null { + if (!ts.isObjectLiteralExpression(node)) { + lowerer.noLowering( + "events.once options that are not an object literal", + node, + "pass the options as an object literal: { signal }", + ); + return null; + } + let signal: IrExpr | null = null; + for (const prop of node.properties.nodes) { + const name = prop.name && ts.isIdentifier(prop.name) ? prop.name.text : null; + if ( + name === "signal" && + signal === null && + (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) + ) { + signal = lowerer.lowerExprExpecting( + ts.isPropertyAssignment(prop) ? prop.initializer : (prop.name as ts.Identifier), + DYN, + ); + } else { + lowerer.noLowering( + "events.once options other than one signal", + prop, + "the lowered options object takes signal: AbortSignal", + ); + } + } + return signal; +} + +/** events.once(emitter, name[, { signal }]): a promise of the next `name` + * event's arguments as an array, rejected by an earlier 'error' event + * (unless the awaited event is 'error') or by the signal's abort with an + * AbortError. The emitter's listeners are typed by the program-wide event + * table (a stream's runtime-emitted events by their fixed tuples), so the + * runtime registers one-shot typed listeners whose compiler-built boxers + * turn the event's tuple into the array. EventTarget receivers, a stream's + * 'data' event, computed names and options other than a literal { signal } + * are refused. */ export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr { const args = call.arguments.nodes; - if (args.length < 2 || args.some(ts.isSpreadElement)) { - lowerer.noLowering("events.once without an emitter and an event name", call); - } - if (args.length > 2) { - lowerer.noLowering( - "events.once with options", - args[2]!, - "the lowered form is events.once(emitter, name); AbortSignal options are not supported yet", - ); + if (args.length < 2 || args.length > 3 || args.some(ts.isSpreadElement)) { + lowerer.noLowering("events.once without an emitter, an event name and options", call); } // Node's Promise: the arguments array of checked-dynamic values. const arrayT = arrayOf(DYN); const resultT: IrType = { kind: "promise", inner: arrayT }; - const receiver = lowerer.lowerExpr(args[0]!); + let receiver = lowerer.lowerExpr(args[0]!); + // A checked-dynamic NodeJS stream slot (dyn-streams.ts): a native stream + // unboxes by identity; any other value throws the not-supported Error. + if ( + receiver.type.kind === "dyn" && + lowerer.dynNodeStreams && + nodeStreamInterfaceSide(lowerer, lowerer.typeOf(args[0]!)) !== null + ) { + receiver = { + kind: "libCall", + fn: "stream.unboxDyn", + args: [ + receiver, + strLit( + "events.once over stream objects other than native streams is not supported yet", + loc, + ), + ], + type: { kind: "object", className: "%Duplex" }, + loc, + }; + } const info = receiver.type.kind === "object" ? lowerer.classes.get(receiver.type.className) : undefined; - if (!emitterRooted(lowerer, info) || streamSidesOf(lowerer, info) !== null) { + if (!emitterRooted(lowerer, info)) { + // A NodeJS stream interface slot that holds a stdio handle here: the + // program lowers again with the runtime stream classes. + requestGeneralStreamSurface(lowerer, args[0]!); lowerer.noLowering( "events.once over this value", args[0]!, - "the emitter must be an EventEmitter instance; EventTarget and stream receivers are not supported yet", + "the emitter must be an EventEmitter or stream instance; EventTarget receivers are not supported yet", ); } const name = literalEventName(lowerer, args[1]!); @@ -2245,6 +2305,14 @@ export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc: ); } const event = name ?? ""; + const sides = streamSidesOf(lowerer, info); + if ((sides === "r" || sides === "rw") && (event === "data" || event === "keypress")) { + lowerer.noLowering( + `events.once of a stream's '${event}' event`, + args[1]!, + `the chunk has no checked-dynamic boxing here; register stream.once('${event}', listener) instead`, + ); + } const table = emitterEvents(lowerer); if (event !== "error" && flexibleJsEvent(event, table.get(event))) { lowerer.noLowering( @@ -2252,14 +2320,22 @@ export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc: args[1]!, ); } - const tuple = tupleOf(lowerer, table, event, call); + const tuple = streamForcedTuple(lowerer, info, event) ?? tupleOf(lowerer, table, event, call); const errorTuple: IrType[] = [{ kind: "object", className: "%Error" }]; const boxer = eventsOnceBoxer(lowerer, call, tuple, arrayT, loc); const errorBoxer = eventsOnceBoxer(lowerer, call, errorTuple, arrayT, loc); + const signal = args.length > 2 ? eventsOnceSignal(lowerer, args[2]!) : null; + const common = [ + receiver, + eventNameExpr(event, loc), + boxer, + errorBoxer, + boolLit(event !== "error", loc), + ]; return { kind: "libCall", - fn: "events.once", - args: [receiver, eventNameExpr(event, loc), boxer, errorBoxer, boolLit(event !== "error", loc)], + fn: signal === null ? "events.once" : "events.onceSignal", + args: signal === null ? common : [...common, signal], type: resultT, loc, }; diff --git a/packages/compiler/src/frontend/lowering/lower-expando.ts b/packages/compiler/src/frontend/lowering/lower-expando.ts index 5ed97d4d7..6adce8f67 100644 --- a/packages/compiler/src/frontend/lowering/lower-expando.ts +++ b/packages/compiler/src/frontend/lowering/lower-expando.ts @@ -98,8 +98,8 @@ function expandoFnSymbolOf(lowerer: Lowerer, recv: ts.Expression): ts.Symbol | n const sym = lowerer.resolveValueSymbol(recv); if (!sym) return null; const decl = sym.valueDeclaration; - if (!decl || decl.getSourceFile().isDeclarationFile) return null; - if (isJsSourceFile(decl.getSourceFile())) return null; + if (!decl || ts.getSourceFileOfNode(decl).isDeclarationFile) return null; + if (isJsSourceFile(ts.getSourceFileOfNode(decl))) return null; if (ts.isFunctionDeclaration(decl)) { return ts.isSourceFile(decl.parent) ? sym : null; } @@ -177,7 +177,7 @@ export function collectExpandoMembers(lowerer: Lowerer, sf: ts.SourceFile): void } const existing = memberOf(members, w.key); if (existing) { - existing.firstWriteStart = Math.min(existing.firstWriteStart, node.getStart()); + existing.firstWriteStart = Math.min(existing.firstWriteStart, ts.tokenStartOfNode(node)); } else if ( !(typeof w.key === "string" && READONLY_FN_MEMBERS.has(w.key)) && !nsOwnedMember(lowerer, w.access) @@ -198,18 +198,22 @@ export function collectExpandoMembers(lowerer: Lowerer, sf: ts.SourceFile): void } if (type?.kind === "void" && isUnitOnlyTsType(tsType)) type = unitOnlyUnion(lowerer.unions); if (type && type.kind !== "void") { - const fnName = w.fnSym.name; + const fnName = ts.symbolName(w.fnSym); const memberName = typeof w.key === "string" ? w.key - : `sym%${w.key.name}%${w.key.declarations[0]?.getStart() ?? 0}`; + : `sym%${ts.symbolName(w.key)}%${(w.key.declarations.length === 0 ? undefined : ts.tokenStartOfNode(w.key.declarations[0]!)) ?? 0}`; const g: IrGlobal = { id: `%g.${tag}${fnName}%.${memberName}`, name: `${fnName}.${memberName}`, type, mutable: true, }; - const entry: ExpandoMember = { global: g, firstWriteStart: node.getStart(), file: sf }; + const entry: ExpandoMember = { + global: g, + firstWriteStart: ts.tokenStartOfNode(node), + file: sf, + }; if (typeof w.key === "string") members.names.set(w.key, entry); else members.symbols.set(w.key, entry); lowerer.globalsList.push(g); @@ -248,7 +252,7 @@ function nsOwnedMember( if (ts.isModuleDeclaration(p)) { return !( p.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword) || - p.getSourceFile().isDeclarationFile + ts.getSourceFileOfNode(p).isDeclarationFile ); } } @@ -308,11 +312,11 @@ export function expandoMemberRead( const member = memberOf(lowerer.expandoMembers.get(fnSym), key); if (!member) return null; if ( - access.getSourceFile() === member.file && + ts.getSourceFileOfNode(access) === member.file && initExecutingPosition(access) && - access.getStart() < member.firstWriteStart + ts.tokenStartOfNode(access) < member.firstWriteStart ) { - const name = typeof key === "string" ? key : key.name; + const name = typeof key === "string" ? key : ts.symbolName(key); lowerer.unsupported( "SC1090", access, diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 9047cf2c5..4fb7350a0 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1,3 +1,5 @@ +import { lowerDynObjectLiteral } from "./expressions/object-literals.js"; +import { fallbackTainted } from "./checked-fallback.js"; import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; @@ -194,9 +196,9 @@ export function templateRawTextOf( ): string { const own = (node as { rawText?: string }).rawText; if (own !== undefined) return own; - const sf = node.getSourceFile(); - const start = node.getStart(sf); - const end = node.getEnd(); + const sf = ts.getSourceFileOfNode(node); + const start = ts.tokenStartOfNode(node); + const end = node.end; const tailTrim = node.kind === ts.SyntaxKind.TemplateHead || node.kind === ts.SyntaxKind.TemplateMiddle ? 2 : 1; return sf.text.slice(start + 1, end - tailTrim); @@ -270,6 +272,25 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const chainRecv = lowerer.chainRecvByNode.get(expr); if (chainRecv) return { ...chainRecv, loc }; + // A failed checked value in a fallback tail: assertions check nothing + // (checked-fallback.ts). + if ( + lowerer.fallbackTaint !== null && + (ts.isAsExpression(expr) || + ts.isTypeAssertion(expr) || + ts.isNonNullExpression(expr) || + ts.isSatisfiesExpression(expr)) && + fallbackTainted(lowerer, expr.expression) + ) + return lowerer.lowerExpr(expr.expression); + if (lowerer.fallbackTaint !== null && fallbackTainted(lowerer, expr)) { + if (ts.isObjectLiteralExpression(expr)) return lowerDynObjectLiteral(lowerer, expr); + if (ts.isArrayLiteralExpression(expr) && !expr.elements.nodes.some(ts.isSpreadElement)) { + const elems = expr.elements.nodes.map((el) => lowerer.lowerExprExpecting(el, DYN)); + return { kind: "dynArrLit", elems, type: DYN, loc }; + } + } + // --external-types supplies CHECKER truth only. A value rooted in one // of those declarations has no runtime implementation in scriptc, so // every use fences at its expression instead of accidentally lowering @@ -297,14 +318,14 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // integer within ±(2^53−1)) and non-integer spellings carry // nothing, so the IR is unchanged for programs that held their // numbers. - const spelled = expr.getText().replace(/_/g, ""); + const spelled = ts.getTextOfNode(expr).replace(/_/g, ""); if (/^\d+$/.test(spelled) && String(Number(spelled)) !== spelled) { return { kind: "numLit", value: Number(spelled), spelling: spelled, type: F64, loc }; } return { kind: "numLit", value, type: F64, loc }; } if (ts.isBigIntLiteral(expr)) { - const spelling = expr.getText().replace(/_/g, "").replace(/n$/i, ""); + const spelling = ts.getTextOfNode(expr).replace(/_/g, "").replace(/n$/i, ""); return { kind: "libCall", fn: "bigint.parse", @@ -550,7 +571,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // the read answers the strict-mode plain-call undefined, the old // constant. TypeScript keeps the fence (noImplicitThis makes it a // compile-time story there). - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { // Module scope of a CommonJS file: `this` is module.exports. const exportsThis = lowerCjsTopLevelThis(lowerer, expr); if (exportsThis) return exportsThis; @@ -1554,7 +1575,7 @@ export function lowerAbsenceProbe(lowerer: Lowerer, node: ts.Expression): IrExpr // statement ruled non-null without narrowing it (jose's secs()). if ( receiverT?.kind === "union" && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && !expr.questionDotToken && !hasOptionalChainGuard(expr.expression) ) { @@ -1744,7 +1765,8 @@ export function stringWrapperToString(lowerer: Lowerer, node: ts.Expression): Ir while (ts.isParenthesizedExpression(e)) e = e.expression; if (!ts.isNewExpression(e) || !ts.isIdentifier(e.expression)) return null; const symbol = lowerer.resolveValueSymbol(e.expression); - if (symbol?.name !== "String" || !lowerer.isStdlibSymbol(symbol)) return null; + if (symbol === null || ts.symbolName(symbol) !== "String" || !lowerer.isStdlibSymbol(symbol)) + return null; const args = e.arguments?.nodes ?? []; if (args.length > 1 || args.some(ts.isSpreadElement)) return null; if (args.length === 0) return { kind: "strLit", value: "", type: STRING, loc: locOf(e) }; @@ -2033,7 +2055,7 @@ export function aliasTypeofNarrows( const leaf = aliasTypeofArmOf(lowerer, e, strip); if (leaf) out.push(leaf); }; - if (implicitMonoFile(cond.getSourceFile())) walk(cond, polarity); + if (implicitMonoFile(ts.getSourceFileOfNode(cond))) walk(cond, polarity); return out; } @@ -2133,7 +2155,7 @@ export function cjsAccessorSnapshotOf( ): { id: string; type: IrType } | null { const hit = lowerer.cjsAccessorSnapshots.get(getter); if (hit !== undefined) return hit; - if (!lexerVisibleAccessors(getter.getSourceFile()).includes(getter)) return null; + if (!lexerVisibleAccessors(ts.getSourceFileOfNode(getter)).includes(getter)) return null; const call = cjsAccessorCall(lowerer, getter, locOf(getter)); if (call === null) return null; const snapshot = { id: `%g.cjsSnapshot.${lowerer.cjsAccessorSnapshots.size}`, type: call.type }; @@ -2582,7 +2604,7 @@ export function lowerOptionalArrayIndexRead( key: IrExpr, ): IrExpr | null { if ( - !isJsSourceFile(expr.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(expr)) || expr.questionDotToken || hasOptionalChainGuard(expr.expression) ) diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index f0f2c45ea..0ddfa0ac4 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -2,6 +2,7 @@ import { declareTypedArguments } from "./typed-arguments.js"; import { varRef } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; +import { isRebindableFunctionSymbol } from "./rebindable-functions.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import type { Lowerer } from "./lowerer.js"; import { @@ -59,6 +60,8 @@ function declareCallReceiver( * same collection lists top-level declarations ride. */ export function isTopLevelFnSymbol(lowerer: Lowerer, ident: ts.Identifier): boolean { const symbol = lowerer.resolveValueSymbol(ident); + // A reassigned JavaScript declaration is read through its module global. + if (isRebindableFunctionSymbol(lowerer, symbol ?? undefined)) return false; if (symbol && lowerer.staticCallables.get(symbol)?.kind === "declared-function") return true; const decl = symbol ? symbol.declarations[0] : undefined; return ( @@ -130,7 +133,7 @@ function declareFunctionArguments( lowerer.ctx.argumentsLocal = local; } if ( - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && !ts.isArrowFunction(node) && shapes.length === 1 && shapes[0]?.mode === "dynRest" && @@ -204,7 +207,7 @@ export function lowerLambda( // Fluent methods must return that receiver without a class cast. if ( ts.isMethodDeclaration(node) && - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && !node.type && !node.asteriskToken && !hasExplicitJsDocReturn(node) && @@ -215,6 +218,19 @@ export function lowerLambda( ret: funcType.ret.kind === "promise" ? { ...funcType.ret, inner: DYN } : DYN, }; } + // An ASYNC lambda whose signature answered a bare checked-dynamic result + // (`retry(async () => loose())` against a library slot typed + // `(...) => A | Promise` with A = any — the union collapses to `any`): + // calling it still produces a promise, so the value's result is + // Promise, the payload the dyn the body returns. + if ( + funcType.ret.kind === "dyn" && + !ts.isAccessor(node) && + node.asteriskToken === undefined && + node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true + ) { + funcType = { ...funcType, ret: { kind: "promise", inner: DYN } }; + } // A lambda IS a value: the completed-ABI rule applies at birth. The // contextual (target) type decides — `(x?: number) => void` may flow // into a slot annotated `(x: number | undefined) => void` (same ABI @@ -223,7 +239,7 @@ export function lowerLambda( if ( !( ts.isMethodDeclaration(node) && - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && canBoxFuncIntoDyn( funcType, (id) => lowerer.shapes.get(id), @@ -274,14 +290,14 @@ export function lowerLambda( if (lowerer.methodLambdasInProgress.has(node)) lowerer.methodLambdasInProgress.set(node, { fnName, type: funcType, selfReads: 0 }); const inferredSignature = - isJsSourceFile(node.getSourceFile()) && !node.type + isJsSourceFile(ts.getSourceFileOfNode(node)) && !node.type ? lowerer.checker.getSignatureFromDeclaration(node) : undefined; const returnsConstructor = inferredSignature && producesConstructor(lowerer, lowerer.checker.getReturnTypeOfSignature(inferredSignature)); if ( - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && !node.type && !isAsync && !isGenerator && @@ -303,7 +319,7 @@ export function lowerLambda( try { const receiver = (checkedReceiver || - isJsSourceFile(node.getSourceFile()) || + isJsSourceFile(ts.getSourceFileOfNode(node)) || node.parameters.nodes.some(isThisParameter)) && !ts.isArrowFunction(node) ? declareCallReceiver(lowerer, node, isGenerator) @@ -448,7 +464,7 @@ export function lowerLambda( // position. ICEs (SC9001) stay compile errors, exactly like // lowerStmts; probe mode (diagSink) keeps the poison. if (!(e instanceof PoisonError)) throw e; - if (!isJsSourceFile(node.getSourceFile())) throw e; + if (!isJsSourceFile(ts.getSourceFileOfNode(node))) throw e; const params: IrParam[] = funcType.params.map((t, i) => ({ localId: `%pf${i}`, name: `%pf${i}`, @@ -497,7 +513,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I lowerer.fnStack.push(ctx); try { const receiver = - decl.parameters.nodes.some(isThisParameter) || isJsSourceFile(decl.getSourceFile()) + decl.parameters.nodes.some(isThisParameter) || isJsSourceFile(ts.getSourceFileOfNode(decl)) ? declareCallReceiver(lowerer, decl, sig.generator !== undefined) : null; const { params, prologue } = lowerer.declareParams(decl.parameters.nodes, sig.params); @@ -550,7 +566,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I // at the declaration's position — so a reachable-but-broken // signature stops the RUN at its own site instead of the build. // ICEs (SC9001) stay compile errors, exactly like lowerStmts. - if (isJsSourceFile(decl.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(decl))) { // An ABI type naming a class that never REGISTERED (the sentence- // walker idiom's path type — the #private fence) is fine to emit: // callers CAN lower calls to this symbol (a same-typed param diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index b1d0a1aa5..19a75a0b4 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -207,7 +207,7 @@ function isGeneratorMember(lowerer: Lowerer, access: ts.PropertyAccessExpression (d) => ts.isInterfaceDeclaration(d.parent) && (d.parent.name.text === "Generator" || d.parent.name.text === "AsyncGenerator") && - lowerer.isStdlibFile(d.getSourceFile()), + lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ) ); } @@ -272,7 +272,7 @@ export function isSettledIteratorValueRead( ts.isInterfaceDeclaration(d.parent) && (d.parent.name.text === "IteratorYieldResult" || d.parent.name.text === "IteratorReturnResult") && - lowerer.isStdlibFile(d.getSourceFile()), + lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) return false; @@ -282,13 +282,13 @@ export function isSettledIteratorValueRead( sym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) && - lowerer.isStdlibFile(d.getSourceFile()), + lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ); const alias = receiver.getAliasSymbol(); const symbol = receiver.getSymbol(); if ( - !(alias?.name === "IteratorResult" && stdlib(alias)) && - !(symbol?.name === "IteratorReturnResult" && stdlib(symbol)) + !(alias !== undefined && ts.symbolName(alias) === "IteratorResult" && stdlib(alias)) && + !(symbol !== undefined && ts.symbolName(symbol) === "IteratorReturnResult" && stdlib(symbol)) ) return false; const checked = lowerer.mapTypeOf(lowerer.typeOf(node)); diff --git a/packages/compiler/src/frontend/lowering/lower-global-value.ts b/packages/compiler/src/frontend/lowering/lower-global-value.ts index d0b251e84..4b8e7b10a 100644 --- a/packages/compiler/src/frontend/lowering/lower-global-value.ts +++ b/packages/compiler/src/frontend/lowering/lower-global-value.ts @@ -14,7 +14,7 @@ export function lowerGlobalValue(lowerer: Lowerer, expr: ts.Expression): IrExpr // lib slice) but absent in Node 24: reads answer undefined. const names = lowerer.checker .getPropertiesOfType(lowerer.typeOf(expr)) - .map((property) => property.name) + .map((property) => ts.symbolName(property)) .filter((name) => name !== TEMPORAL_GLOBAL); return { kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 1310469fb..a8657f978 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -493,7 +493,7 @@ function classInspectsThroughView(lowerer: Lowerer, info: ClassInfo | undefined) if (info.subclasses.length > 0) return true; return ( info.decl != null && - isJsSourceFile(info.decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(info.decl)) && classDynViewSupported( info.def.fields, (id) => lowerer.shapes.get(id), @@ -2660,7 +2660,7 @@ function lowerUtilCallableCall( * a statically typed callback parameter cannot receive a value its type * excludes. Untyped (JavaScript or unknown) slots keep the wrapper dyn. */ function callbackifyErrorExcludesNull(lowerer: Lowerer, expr: ts.CallExpression): boolean { - if (isJsSourceFile(expr.getSourceFile())) return false; + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) return false; const checker = lowerer.checker; const slot = checker.getContextualType(expr) ?? lowerer.typeOf(expr); const wrapper = checker.getCallSignatures(slot)[0]; @@ -2766,7 +2766,7 @@ function lowerGetCallSitesCall( loc: SrcLoc, ): IrExpr | null { if (expr.arguments.nodes.length !== 0) return null; - if (!isJsSourceFile(expr.getSourceFile())) return null; + if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null; const frame = (): IrExpr => { const field = (name: string, value: IrExpr): { key: IrExpr; value: IrExpr } => ({ key: { kind: "strLit", value: name, type: STRING, loc }, @@ -2778,7 +2778,7 @@ function lowerGetCallSitesCall( field("functionName", { kind: "strLit", value: "", type: STRING, loc }), field("scriptName", { kind: "strLit", - value: expr.getSourceFile().fileName, + value: ts.getSourceFileOfNode(expr).fileName, type: STRING, loc, }), diff --git a/packages/compiler/src/frontend/lowering/lower-mixins.ts b/packages/compiler/src/frontend/lowering/lower-mixins.ts index 90789b4a0..766059dfb 100644 --- a/packages/compiler/src/frontend/lowering/lower-mixins.ts +++ b/packages/compiler/src/frontend/lowering/lower-mixins.ts @@ -174,7 +174,7 @@ function mixinFnShapeInner( if (n === t.expression) return undefined; if ( ts.isIdentifier(n) && - n.text === p.name.getText() && + n.text === ts.getTextOfNode(p.name) && lowerer.checker.getSymbolAtLocation(n) === paramSym ) { extraRef = true; @@ -451,8 +451,8 @@ function instantiateMixinCall( const base = mixinBaseClassOf(lowerer, call.arguments.nodes[0]!); const instName = lowerer.qualify( - call.getSourceFile(), - `%mx${call.getStart()}.${shape.classNode.name?.text ?? ""}`, + ts.getSourceFileOfNode(call), + `%mx${ts.tokenStartOfNode(call)}.${shape.classNode.name?.text ?? ""}`, ); const ordinal = lowerer.mixinOrdinals.get(shape.classNode) ?? 0; lowerer.mixinOrdinals.set(shape.classNode, ordinal + 1); @@ -513,7 +513,10 @@ function instantiateMixinCall( ); } if (info.mixinInstance) { - info.mixinInstance.statics = { sf: call.getSourceFile(), pos: holder.getStart() }; + info.mixinInstance.statics = { + sf: ts.getSourceFileOfNode(call), + pos: ts.tokenStartOfNode(holder), + }; } } return info; diff --git a/packages/compiler/src/frontend/lowering/lower-mock.ts b/packages/compiler/src/frontend/lowering/lower-mock.ts index be68abaca..fcc68c787 100644 --- a/packages/compiler/src/frontend/lowering/lower-mock.ts +++ b/packages/compiler/src/frontend/lowering/lower-mock.ts @@ -797,7 +797,7 @@ export function lowerMockMethod( const objShape = objType?.kind === "record" ? lowerer.shapes.get(objType.shapeId) : undefined; const fn = objShape?.fields.find((f) => f.name === field)?.type; if (!objType || !objShape || fn?.kind !== "func") { - const typeName = lowerer.typeOf(args[0]!).getSymbol()?.name; + const typeName = ts.symbolNameOf(lowerer.typeOf(args[0]!).getSymbol()); lowerer.noLowering( typeName && !typeName.startsWith("__") ? `${what} on '${typeName}'` diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index bea6c7f7d..6682ffb23 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -8,6 +8,7 @@ import { InternalCompilerError } from "../../errors.js"; * seeds), npm/JSON import collection, per-file %init functions, %main, and * the module artifacts (globals, embedded npm tables) the IR module carries. */ import * as ts from "../ts7/adapter.js"; +import { collectRebindableFunctions, isRebindableFunction } from "./rebindable-functions.js"; import { bindingSource } from "../binding-source.js"; import type { Lowerer } from "./lowerer.js"; import { dirname as dirnamePath, resolve as resolvePath } from "node:path"; @@ -295,8 +296,12 @@ export function splitFiles(lowerer: Lowerer): FileParts[] { const files = lowerer.moduleOrder.length > 0 ? lowerer.moduleOrder : [lowerer.entry]; return files.map((sf) => { const fp: FileParts = { sf, fnDecls: [], classDecls: [], topStmts: [] }; + const rebindable = collectRebindableFunctions(lowerer, sf); for (const stmt of sf.statements.nodes) { - if (ts.isFunctionDeclaration(stmt)) fp.fnDecls.push(stmt); + // A reassigned JavaScript declaration lives in a module global that + // the init body assigns first (rebindable-functions.ts). + if (ts.isFunctionDeclaration(stmt) && rebindable.has(stmt)) fp.topStmts.push(stmt); + else if (ts.isFunctionDeclaration(stmt)) fp.fnDecls.push(stmt); else if (ts.isClassDeclaration(stmt)) fp.classDecls.push(stmt); // Namespaces: ambient/type-only ones are zero-runtime and skip; // instantiated bodies FLATTEN into this file's parts (functions/ @@ -745,7 +750,10 @@ export function collectJsonImports(lowerer: Lowerer, parts: FileParts[]): void { const nameSym = lowerer.checker.getSymbolAtLocation(clause.name); if (!nameSym || !(nameSym.flags & ts.SymbolFlags.Alias)) continue; const target = lowerer.checker.getAliasedSymbol(nameSym); - const jsonSf = target.declarations[0]?.getSourceFile(); + const jsonSf = + target.declarations.length === 0 + ? undefined + : ts.getSourceFileOfNode(target.declarations[0]!); if (!jsonSf || !jsonSf.fileName.endsWith(".json")) continue; try { const tsType = lowerer.typeOf(clause.name); @@ -1228,7 +1236,7 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi if ( (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) && !e.typeParameters?.nodes.length && - isJsSourceFile(e.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(e)) ) return true; if (ts.isIdentifier(e) || ts.isArrowFunction(e) || ts.isFunctionExpression(e)) { @@ -1279,6 +1287,36 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi return false; } +/** A JavaScript default-export object literal holding a class the program + * could not lower (`export default { parse, TomlDate }` where TomlDate + * extends an unsupported base): the literal narrows that member away, its + * diagnostics deferred to the reads that reach it, so the binding's type + * drops it too. Otherwise the module's own initialization would fail the + * width check against members no build can supply. */ +function withoutRejectedClassMembers( + lowerer: Lowerer, + expression: ts.Expression, + type: IrType, +): IrType { + let literal = expression; + while (ts.isParenthesizedExpression(literal)) literal = literal.expression; + if (!ts.isObjectLiteralExpression(literal) || type.kind !== "record") return type; + const shape = lowerer.shapes.get(type.shapeId); + if (!shape || shape.tuple) return type; + const rejected = (t: IrType): boolean => + t.kind === "classval" && + !lowerer.classes.has(t.className) && + lowerer.deferredClassByName.has(t.className); + if (!shape.fields.some((f) => rejected(f.type))) return type; + const kept = shape.fields.filter((f) => !rejected(f.type)); + const shapeId = lowerer.shapes.intern( + kept.map((f) => ({ name: f.name, type: f.type })), + false, + shape.indexValue, + ); + return { kind: "record", shapeId }; +} + export function collectGlobals( lowerer: Lowerer, sf: ts.SourceFile, @@ -1294,6 +1332,21 @@ export function collectGlobals( // collected earlier in this pass must resolve them (lower-expando.ts). collectExpandoMembers(lowerer, sf); for (const stmt of topStmts) { + if (ts.isFunctionDeclaration(stmt) && stmt.name && isRebindableFunction(lowerer, stmt)) { + const symbol = lowerer.checker.getSymbolAtLocation(stmt.name); + if (symbol && !lowerer.globalsBySymbol.has(symbol)) { + const g: IrGlobal = { + id: `%g.${tag}${stmt.name.text}`, + name: stmt.name.text, + type: DYN, + mutable: true, + }; + lowerer.globalsBySymbol.set(symbol, g); + lowerer.globalsByDeclNode.set(stmt, g); + lowerer.globalsList.push(g); + } + continue; + } // `export default `: the module's `default` binding is a const // module global (registered under the checker's default-export // symbol — the symbol a default IMPORT's alias chain terminates at, @@ -1327,7 +1380,7 @@ export function collectGlobals( const mutableVar = vd !== undefined && ts.isVariableDeclaration(vd) && - !vd.getSourceFile().isDeclarationFile && + !ts.getSourceFileOfNode(vd).isDeclarationFile && (ts.getCombinedNodeFlags(vd) & ts.NodeFlags.Const) === 0; // Immutable targets: alias plumbing — importers resolve to the // target's storage (the alias-resolved read IS the snapshot when @@ -1381,6 +1434,7 @@ export function collectGlobals( type = unitOnlyUnion(lowerer.unions); } if (type.kind === "void") lowerer.badType(stmt.expression, tsType); + if (isJsSourceFile(sf)) type = withoutRejectedClassMembers(lowerer, stmt.expression, type); const g: IrGlobal = { id: `%g.${tag}default`, name: "default", type, mutable: false }; lowerer.globalsBySymbol.set(symbol, g); lowerer.globalsList.push(g); @@ -1599,7 +1653,7 @@ export function collectGlobals( const mutableVar = vd !== undefined && ts.isVariableDeclaration(vd) && - !vd.getSourceFile().isDeclarationFile && + !ts.getSourceFileOfNode(vd).isDeclarationFile && (ts.getCombinedNodeFlags(vd) & ts.NodeFlags.Const) === 0; if (!mutableVar) continue; const diagsBefore = lowerer.diags.length; @@ -1610,7 +1664,7 @@ export function collectGlobals( } if (type.kind === "void") lowerer.badType(stmt.name, lowerer.typeOf(stmt.name)); const g: IrGlobal = { - id: `%g.${tag}${nsPathPrefix(stmt)}%alias.${stmt.name.text}%${stmt.getStart()}`, + id: `%g.${tag}${nsPathPrefix(stmt)}%alias.${stmt.name.text}%${ts.tokenStartOfNode(stmt)}`, name: stmt.name.text, type, mutable: false, @@ -1660,43 +1714,9 @@ export function collectGlobals( ) ) ) { - if (ts.isVariableStatement(stmt) && (stmt.declarationList.flags & ts.NodeFlags.Const) !== 0) { - for (const decl of stmt.declarationList.declarations.nodes) { - stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer); - } - } - // `const { codes: { ERR_X } } = require("./errors")`: the outer - // keys stay alias plumbing, but a nested pattern destructures the - // export's value at the require, so its bindings own storage like - // any destructured declaration (lowerVarDeclarator assigns them). - if (ts.isVariableStatement(stmt)) { - const isLetList = - (stmt.declarationList.flags & ts.NodeFlags.Let) !== 0 || - (stmt.declarationList.flags & ts.NodeFlags.BlockScoped) === 0; - for (const decl of stmt.declarationList.declarations.nodes) { - for (const element of requireNestedPatternsOf(decl)) { - for (const nameNode of boundIdentifiersOf(element.name!)) { - const symbol = lowerer.checker.getSymbolAtLocation(nameNode); - if (!symbol || lowerer.globalsBySymbol.has(symbol)) continue; - const mapped = lowerer.mapTypeOf(lowerer.typeOf(nameNode)); - const type = mapped && mapped.kind !== "void" ? mapped : DYN; - const g: IrGlobal = { - id: `%g.${tag}${nameNode.text}`, - name: nameNode.text, - type, - mutable: isLetList, - source: bindingSource(nameNode), - ...((type.kind === "func" || type.kind === "dyn") && !isVarDeclared(decl) - ? { tdz: true as const } - : {}), - }; - lowerer.globalsBySymbol.set(symbol, g); - lowerer.globalsList.push(g); - if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g); - } - } - } - } + if (ts.isVariableStatement(stmt)) + for (const decl of stmt.declarationList.declarations.nodes) + registerRequireDeclBindings(lowerer, sf, stmt.declarationList, decl, tag); continue; } // `declare const __VERSION__: string` — AMBIENT: no storage exists @@ -1730,6 +1750,16 @@ export function collectGlobals( // probes below would otherwise claim the call-initializer shape // and put its fences on the build. if (provenanceElidedConstDecl(lowerer, decl)) continue; + // A require declarator beside other declarators (`var EventEmitter = + // require("events"), R = class extends EventEmitter {}`): the + // statement lowering treats it as the alias plumbing of a whole + // require statement (lowerVarDeclarator), so it owns no storage + // either. A global here would never be assigned, and a read through + // it would see null. + if (requireAliasDecl(lowerer, decl)) { + registerRequireDeclBindings(lowerer, sf, list, decl, tag); + continue; + } // A stored built-in numeric value iterator has no first-class // global representation. Its same-%init for-of uses are backed by // hidden source/cursor/done locals registered when the declaration @@ -2335,12 +2365,12 @@ export function collectGlobals( : undefined; const callDeclaration = callSignature && callSignature.declaration; const callType = - callDeclaration && isJsSourceFile(callDeclaration.getSourceFile()) + callDeclaration && isJsSourceFile(ts.getSourceFileOfNode(callDeclaration)) ? lowerer.mapTypeOf(lowerer.typeOf(nameNode)) : null; const inferredJsCall = callDeclaration && - isJsSourceFile(callDeclaration.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(callDeclaration)) && (!callType || (unannotatedCall && callType.kind === "union" && @@ -2648,6 +2678,61 @@ function noteVarGlobalEntryInit(lowerer: Lowerer, sf: ts.SourceFile, g: IrGlobal lowerer.varGlobalEntryInits.set(sf, inits); } +/** A JavaScript require declaration the statement lowering treats as alias + * plumbing (lowerVarDeclarator): no storage, the reads resolve through to + * the module. JSON documents and native FFI requires keep their storage. */ +function requireAliasDecl(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { + return ( + decl.initializer !== undefined && + requireSpecOf(decl.initializer) !== null && + !isNativeFfiRequire(lowerer, decl.initializer) && + !isJsonRequireDecl(lowerer, decl) && + isJsSourceFile(decl.getSourceFile()) + ); +} + +/** The registrations a top-level require declaration does make. `const + * process = require('node:process')` aliases the global process + * (stdlibGlobalAliasDecl's require form) so reads through the binding + * lower via the process surface. In `const { codes: { ERR_X } } = + * require("./errors")` the outer keys stay alias plumbing, but a nested + * pattern destructures the export's value at the require, so its bindings + * own storage like any destructured declaration (lowerVarDeclarator + * assigns them). */ +function registerRequireDeclBindings( + lowerer: Lowerer, + sf: ts.SourceFile, + list: ts.VariableDeclarationList, + decl: ts.VariableDeclaration, + tag: string, +): void { + if ((list.flags & ts.NodeFlags.Const) !== 0) + stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer); + const isLetList = + (list.flags & ts.NodeFlags.Let) !== 0 || (list.flags & ts.NodeFlags.BlockScoped) === 0; + for (const element of requireNestedPatternsOf(decl)) { + for (const nameNode of boundIdentifiersOf(element.name!)) { + const symbol = lowerer.checker.getSymbolAtLocation(nameNode); + if (!symbol || lowerer.globalsBySymbol.has(symbol)) continue; + const mapped = lowerer.mapTypeOf(lowerer.typeOf(nameNode)); + const type = mapped && mapped.kind !== "void" ? mapped : DYN; + const g: IrGlobal = { + id: `%g.${tag}${nameNode.text}`, + name: nameNode.text, + type, + mutable: isLetList, + source: bindingSource(nameNode), + ...((type.kind === "func" || type.kind === "dyn") && !isVarDeclared(decl) + ? { tdz: true as const } + : {}), + }; + lowerer.globalsBySymbol.set(symbol, g); + lowerer.globalsList.push(g); + if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g); + } + } +} + /** The nested half of module-scope `var` hoisting: a `var` inside a * top-level BLOCK (an if arm, a loop body, a try) is module-scoped in * JS — other functions in the file read the same binding — so it must @@ -2692,6 +2777,14 @@ function collectNestedVarGlobals( }); for (const list of lists) { for (const decl of list.declarations.nodes) { + // A nested builtin require (`if (c) { var fs = require("fs"); }`) + // lowers to nothing: its bindings are alias plumbing wherever the + // statement sits (lowerVarDeclarator), so they own no storage. + if ( + requireAliasDecl(lowerer, decl) && + canonicalBuiltinModule(requireSpecOf(decl.initializer!)!) !== null + ) + continue; for (const nameNode of boundIdentifiersOf(decl.name)) { const diagsBefore = lowerer.diags.length; try { @@ -2763,7 +2856,7 @@ export function declSymbolOf( ts.canHaveModifiers(decl) && ts.getModifiers(decl)?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) === true; if (!isDefault) return undefined; - const sym = defaultExportSymbolOf(lowerer, decl.getSourceFile()); + const sym = defaultExportSymbolOf(lowerer, ts.getSourceFileOfNode(decl)); return sym ?? undefined; } @@ -2781,7 +2874,7 @@ export function declSymbolOf( * expression so the expression's OWN diagnostic wins when it has one, * then poisons. */ export function lowerDefaultExport(lowerer: Lowerer, stmt: ts.ExportAssignment): IrStmt | null { - const symbol = defaultExportSymbolOf(lowerer, stmt.getSourceFile()); + const symbol = defaultExportSymbolOf(lowerer, ts.getSourceFileOfNode(stmt)); if (symbol && symbol.flags & ts.SymbolFlags.Alias && !lowerer.globalsBySymbol.has(symbol)) return null; const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined; @@ -2794,7 +2887,7 @@ export function lowerDefaultExport(lowerer: Lowerer, stmt: ts.ExportAssignment): while (ts.isParenthesizedExpression(inner)) inner = inner.expression; if ( ts.isClassExpression(inner) && - !isJsSourceFile(stmt.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(stmt)) && inner.typeParameters === undefined && (decoratorNodesOf(inner).length > 0 || inner.members.nodes.some((m) => decoratorNodesOf(m).length > 0)) && @@ -2855,6 +2948,23 @@ export function lowerFileInit( loc: loc0, }); } + // Reassigned function declarations are initialized when the module + // starts, as declarations are hoisted: before its dependencies run, so + // a module of an import cycle can call one while it initializes. Their + // statements then lower to nothing (rebindable-functions.ts). + for (const stmt of stmts) { + if (!ts.isFunctionDeclaration(stmt) || !stmt.name || !isRebindableFunction(lowerer, stmt)) + continue; + const g = lowerer.globalsByDeclNode.get(stmt); + if (!g) continue; + header.push({ + kind: "assign", + localId: g.id, + value: lowerer.coerceToExpected(lowerer.lowerLambda(stmt), DYN), + loc: locOf(stmt), + }); + ctx.hoistedFnDecls.add(stmt); + } for (const { stmt, dep } of orderedImportsOf(lowerer.program, sf)) { const npm = lowerer.npmInitActions.get(sf)?.get(stmt); if (npm !== undefined && npm.length > 0) { @@ -3014,7 +3124,7 @@ export function lowerFileInit( c.decl && ts.isClassDeclaration(c.decl) && !c.mixinInstance && - c.decl.getSourceFile() === sf && + ts.getSourceFileOfNode(c.decl) === sf && (lowerer.classInitFlags.has(c.def.name) || c.callableBase !== undefined || c.def.baseValueGlobal !== undefined || @@ -3023,7 +3133,7 @@ export function lowerFileInit( (c.staticBlocks?.length ?? 0) > 0 || c.classDecorators !== undefined), ) - .map((c) => ({ pos: c.decl!.getStart(), info: c })); + .map((c) => ({ pos: ts.tokenStartOfNode(c.decl!), info: c })); // Statics-bearing MIXIN instantiations whose call evaluates in THIS // file: their declaration-time code runs when the call does — the // top-level statement lower-mixins recorded. Insertion order is @@ -3043,7 +3153,7 @@ export function lowerFileInit( // anything the statement's lowering emits. Hoisted class // declarations never share a kept statement's start, so `<=` // changes nothing for them. - while (at < statics.length && statics[at]!.pos <= stmt.getStart()) { + while (at < statics.length && statics[at]!.pos <= ts.tokenStartOfNode(stmt)) { body.push( ...withClassInitFlag( lowerer, diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index 9e2903593..dca835d77 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -179,10 +179,10 @@ export function nsPathPrefix(node: ts.Node, modifierDecl?: ts.Node): string { const nested = p.parent !== undefined && ts.isModuleBlock(p.parent); const nonExported = nested && (ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Export) === 0; - segs.unshift(p.name.text + (nonExported ? `%${p.getStart()}` : "")); + segs.unshift(p.name.text + (nonExported ? `%${ts.tokenStartOfNode(p)}` : "")); } } - return `${segs.join("%")}${exported ? "" : `%${parent.getStart()}`}.`; + return `${segs.join("%")}${exported ? "" : `%${ts.tokenStartOfNode(parent)}`}.`; } /** The registered namespace-block kind a symbol's declaration sits in @@ -280,7 +280,11 @@ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile stmt.moduleSpecifier !== undefined && ts.isStringLiteral(stmt.moduleSpecifier) ) { - const dep = resolveImport(lowerer.program, d.getSourceFile(), stmt.moduleSpecifier.text); + const dep = resolveImport( + lowerer.program, + ts.getSourceFileOfNode(d), + stmt.moduleSpecifier.text, + ); if (dep && !dep.isDeclarationFile && lowerer.fileTag.has(dep)) return dep; } } @@ -294,7 +298,7 @@ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile !ts.isStringLiteral(stmt.moduleSpecifier) ) continue; - const from = d.getSourceFile(); + const from = ts.getSourceFileOfNode(d); if (packageShimFor(from.fileName, stmt.moduleSpecifier.text) === null) continue; const dep = resolveImport(lowerer.program, from, stmt.moduleSpecifier.text); if (dep && !dep.isDeclarationFile && lowerer.fileTag.has(dep)) return dep; @@ -353,7 +357,7 @@ export function ambientNsRootOf(lowerer: Lowerer, e: ts.Expression): ts.Identifi return null; } if (ts.isModuleDeclaration(d) && !ts.isIdentifier(d.name)) return null; - if (d.getSourceFile().isDeclarationFile) return null; // lib/@types keep their own fences + if (ts.getSourceFileOfNode(d).isDeclarationFile) return null; // lib/@types keep their own fences if ( ts.isModuleDeclaration(d) ? !isAmbientModuleDecl(d) @@ -451,7 +455,7 @@ export function ambientUndefVarRootOf(lowerer: Lowerer, e: ts.Expression): ts.Id if (decls.length === 0) return null; for (const d of decls) { if (!ts.isVariableDeclaration(d) || d.initializer !== undefined) return null; - if (d.getSourceFile().isDeclarationFile) return null; // lib/@types keep their own fences + if (ts.getSourceFileOfNode(d).isDeclarationFile) return null; // lib/@types keep their own fences if (!(ts.getCombinedModifierFlags(d) & ts.ModifierFlags.Ambient)) return null; } return root; @@ -510,7 +514,7 @@ export function trapDeclRootOf( if (root === null) return null; for (const nameNode of boundIdentifiersOf(decl.name)) { const sym = lowerer.checker.getSymbolAtLocation(nameNode); - if (sym && bindingEverWritten(lowerer, sym, decl.getSourceFile())) { + if (sym && bindingEverWritten(lowerer, sym, ts.getSourceFileOfNode(decl))) { const mapped = lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(sym)); if (mapped !== null && mapped.kind !== "void") return null; } @@ -535,7 +539,7 @@ export function trapDeclRootOf( * initializer into an ordinary lowering that fences). ambientUndefVarRootOf * carries its own collect-guard, so the extra question stays a probe. */ function bindingEverWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): boolean { - const symText = sym.name; + const symText = ts.symbolName(sym); const namesSym = (e: ts.Node): boolean => ts.isIdentifier(e) && e.text === symText && lowerer.checker.getSymbolAtLocation(e) === sym; const patternWrites = (target: ts.Node): boolean => { @@ -544,9 +548,9 @@ function bindingEverWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile) const walk = (m: ts.Node): void => { if (hit) return; if (namesSym(m)) hit = true; - else m.forEachChild(walk); + else ts.forEachChild(m, walk); }; - target.forEachChild(walk); + ts.forEachChild(target, walk); return hit; }; let written = false; @@ -591,9 +595,9 @@ function bindingEverWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile) written = true; } } - n.forEachChild(visit); + ts.forEachChild(n, visit); }; - sf.forEachChild(visit); + ts.forEachChild(sf, visit); return written; } @@ -654,7 +658,7 @@ export function ambientUndefinedFnSymbolOf( } else if (!ts.isModuleDeclaration(d) && !ts.isClassDeclaration(d)) { return null; } - if (d.getSourceFile().isDeclarationFile) return null; + if (ts.getSourceFileOfNode(d).isDeclarationFile) return null; if (!(ts.getCombinedModifierFlags(d) & ts.ModifierFlags.Ambient)) return null; } return sawFn ? sym : null; @@ -695,7 +699,7 @@ function initExecutingPosition(node: ts.Node): boolean { function earliestSameFileDeclStart(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): number { let earliest = Infinity; for (const d of sym.declarations) { - if (d.getSourceFile() === sf) earliest = Math.min(earliest, d.getStart()); + if (ts.getSourceFileOfNode(d) === sf) earliest = Math.min(earliest, ts.tokenStartOfNode(d)); } return earliest; } @@ -710,10 +714,10 @@ export function fenceEarlyAliasUse(lowerer: Lowerer, ident: ts.Identifier, refNo const aliasDecl = sym.declarations.find(ts.isImportEqualsDeclaration); if (!aliasDecl || ts.isExternalModuleReference(aliasDecl.moduleReference)) return; if (!initExecutingPosition(refNode)) return; - const sf = refNode.getSourceFile(); - if (aliasDecl.getSourceFile() !== sf) return; // cross-file: module init order applies - const aliasStart = aliasDecl.getStart(); - if (refNode.getStart() < aliasStart) { + const sf = ts.getSourceFileOfNode(refNode); + if (ts.getSourceFileOfNode(aliasDecl) !== sf) return; // cross-file: module init order applies + const aliasStart = ts.tokenStartOfNode(aliasDecl); + if (ts.tokenStartOfNode(refNode) < aliasStart) { lowerer.unsupported( "SC1090", refNode, @@ -749,8 +753,8 @@ export function fenceEarlyNsMemberRef( memberSym: ts.Symbol, ): void { if (!initExecutingPosition(access)) return; - const sf = access.getSourceFile(); - const refStart = access.getStart(); + const sf = ts.getSourceFileOfNode(access); + const refStart = ts.tokenStartOfNode(access); const memberStart = earliestSameFileDeclStart(lowerer, memberSym, sf); if (memberStart !== Infinity && refStart < memberStart) { lowerer.unsupported( diff --git a/packages/compiler/src/frontend/lowering/lower-node-module.ts b/packages/compiler/src/frontend/lowering/lower-node-module.ts index fee25b10f..37675777c 100644 --- a/packages/compiler/src/frontend/lowering/lower-node-module.ts +++ b/packages/compiler/src/frontend/lowering/lower-node-module.ts @@ -45,7 +45,7 @@ function cjsGlobal(lowerer: Lowerer, node: ts.Expression, name: "module" | "requ const decls = symbol ? symbol.declarations : []; const sourceShadow = decls.some( (decl) => - !decl.getSourceFile().isDeclarationFile && + !ts.getSourceFileOfNode(decl).isDeclarationFile && (ts.isVariableDeclaration(decl) || ts.isParameter(decl) || ts.isBindingElement(decl) || @@ -55,7 +55,7 @@ function cjsGlobal(lowerer: Lowerer, node: ts.Expression, name: "module" | "requ ts.isNamespaceImport(decl) || ts.isImportEqualsDeclaration(decl)), ); - return !sourceShadow && !isNodeEsmFile(node.getSourceFile(), lowerer.program); + return !sourceShadow && !isNodeEsmFile(ts.getSourceFileOfNode(node), lowerer.program); } function graphSyntax(lowerer: Lowerer, sf: ts.SourceFile): boolean { @@ -174,7 +174,7 @@ export function cjsModuleRegistryPrelude(lowerer: Lowerer, loc: IrExpr["loc"]): } export function cjsModuleRef(lowerer: Lowerer, node: ts.Node): IrExpr | null { - const id = lowerer.cjsModuleIdOf.get(node.getSourceFile()); + const id = lowerer.cjsModuleIdOf.get(ts.getSourceFileOfNode(node)); return id === undefined ? null : numLit(id, locOf(node)); } @@ -203,8 +203,8 @@ function nodeModuleType(lowerer: Lowerer, node: ts.Expression): boolean { const symbol = type.getSymbol(); if ( symbol && - (symbol.name === "Module" || symbol.name === "ScriptcModule") && - symbol.declarations.some((decl) => lowerer.isStdlibFile(decl.getSourceFile())) + (ts.symbolName(symbol) === "Module" || ts.symbolName(symbol) === "ScriptcModule") && + symbol.declarations.some((decl) => lowerer.isStdlibFile(ts.getSourceFileOfNode(decl))) ) { return true; } @@ -217,7 +217,7 @@ function nodeModuleType(lowerer: Lowerer, node: ts.Expression): boolean { (ts.isInterfaceDeclaration(parent) || ts.isClassDeclaration(parent)) && parent.name && (parent.name.text === "ScriptcModule" || parent.name.text === "Module") && - lowerer.isStdlibFile(parent.getSourceFile()) + lowerer.isStdlibFile(ts.getSourceFileOfNode(parent)) ) { return true; } @@ -237,7 +237,7 @@ function nodeModuleType(lowerer: Lowerer, node: ts.Expression): boolean { * file assigns an object or function. */ function moduleExportsAlwaysTruthy(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean { if (!truthinessOnly(expr)) return false; - const sf = expr.getSourceFile(); + const sf = ts.getSourceFileOfNode(expr); if (isNodeEsmFile(sf, lowerer.program)) return false; let truthy = true; ts.walkPreorder(sf, (node) => { @@ -307,7 +307,7 @@ export function isCjsTopLevelThis(lowerer: Lowerer, node: ts.Node): boolean { ) return false; } - return isCjsJsFile(node.getSourceFile(), lowerer.program); + return isCjsJsFile(ts.getSourceFileOfNode(node), lowerer.program); } export function usesCjsTopLevelThis(lowerer: Lowerer, sf: ts.SourceFile): boolean { @@ -341,7 +341,7 @@ export function usesCjsTopLevelThis(lowerer: Lowerer, sf: ts.SourceFile): boolea * behave as in Node. */ export function lowerCjsTopLevelThis(lowerer: Lowerer, expr: ts.Expression): IrExpr | null { if (!isCjsTopLevelThis(lowerer, expr)) return null; - const global = lowerer.cjsExportsThis.get(expr.getSourceFile()); + const global = lowerer.cjsExportsThis.get(ts.getSourceFileOfNode(expr)); if (!global) return null; let receiver: ts.Node = expr; while (ts.isParenthesizedExpression(receiver.parent)) receiver = receiver.parent; @@ -433,7 +433,7 @@ export function lowerNodeModuleIdentifier(lowerer: Lowerer, expr: ts.Identifier) if (!isCjsModuleGlobal(lowerer, expr)) return null; const ref = cjsModuleRef(lowerer, expr); if (ref) return ref; - if (isCjsJsFile(expr.getSourceFile(), lowerer.program)) { + if (isCjsJsFile(ts.getSourceFileOfNode(expr), lowerer.program)) { lowerer.unsupported( "SC1090", expr, diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index b4e64c9d0..afae7dd7b 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -719,7 +719,7 @@ export function lowerNetModuleCall( // ERR_INVALID_ARG_TYPE on 'options' before any server exists. DYN // result — the checked-dynamic lane's concise arrows box it, and // the throw means it never materializes. - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { const t = lowerer.mapTypeOf(lowerer.typeOf(args[0]!)); if (t !== null && (t.kind === "string" || t.kind === "f64" || t.kind === "bool")) { const raw = lowerer.lowerExpr(args[0]!); @@ -770,7 +770,7 @@ export function lowerNetModuleCall( // invalid-input probes build theirs with computed keys): the // checked-dynamic walk validates Node-order and the compiler-rendered // fence is the post-validation tail. - if (args.length === 1 && isJsSourceFile(expr.getSourceFile())) { + if (args.length === 1 && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const t = lowerer.mapTypeOf(lowerer.typeOf(args[0]!)); if (t !== null && (t.kind === "dyn" || t.kind === "record")) { const raw = lowerer.lowerExpr(args[0]!); @@ -885,7 +885,7 @@ function lowerNetConnectOptions( ): IrExpr { const args = expr.arguments.nodes; const optsNode = args[0] as ts.ObjectLiteralExpression; - const isJs = isJsSourceFile(expr.getSourceFile()); + const isJs = isJsSourceFile(ts.getSourceFileOfNode(expr)); // The checked-dynamic option-bag route (JS sources): a literal with // computed keys (the invalid-input probes' spelling) or an objectMode- // trio member rides WHOLE to the runtime walk — Node's Socket-ctor @@ -1524,7 +1524,7 @@ function lowerNetServerMethodCall( } else if ( key === "signal" && ts.isPropertyAssignment(prop) && - isJsSourceFile(call.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(call)) ) { // A provably-non-AbortSignal signal (the invalid-input probes: // strings, numbers, plain records) throws Node's @@ -2334,7 +2334,7 @@ function lowerNetSocketMethodCall( if ( encT.value === "buffer" && chunkT.kind === "string" && - isJsSourceFile(call.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(call)) ) { lowerer.lowerExpr(args[0]!); // evaluation order (effect-free in practice) return nodeThrowExpr(1, "ERR_UNKNOWN_ENCODING", "Unknown encoding: buffer", VOID, loc); @@ -4851,7 +4851,7 @@ function tlsLiteralNeedsRuntimeWalk( node: ts.ObjectLiteralExpression, includeHttpTimeoutOptions = false, ): boolean { - const jsSource = isJsSourceFile(node.getSourceFile()); + const jsSource = isJsSourceFile(ts.getSourceFileOfNode(node)); return node.properties.nodes.some( (p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) && @@ -5071,7 +5071,7 @@ function lowerTlsModuleCall( bi.member === "getCACertificates" && args.length === 1 && !args.some(ts.isSpreadElement) && - isJsSourceFile(expr.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(expr)) ) { // The type-argument ladder (validateString + the documented name // set); the real CA list has no lowering, so a valid name meets the @@ -5737,7 +5737,7 @@ function lowerHttp2ModuleCall( if (ts.isConditionalExpression(inner)) { const whenTrue = stripParensAndCasts(inner.whenTrue); const whenFalse = stripParensAndCasts(inner.whenFalse); - const condText = inner.condition.getText(); + const condText = ts.getTextOfNode(inner.condition); const ok = ts.isObjectLiteralExpression(whenTrue) && ts.isObjectLiteralExpression(whenFalse) && @@ -5748,7 +5748,7 @@ function lowerHttp2ModuleCall( ts.isStringLiteral(whenTrue.properties.nodes[0]!.name)) && (whenTrue.properties.nodes[0]!.name as ts.Identifier | ts.StringLiteral).text === "SNICallback" && - whenTrue.properties.nodes[0]!.initializer.getText() === condText; + ts.getTextOfNode(whenTrue.properties.nodes[0]!.initializer) === condText; if (!ok) { lowerer.noLowering( "createSecureServer options with a conditional spread", @@ -7533,7 +7533,7 @@ function lowerHttpResMethodCall( } if (name === "writeContinue" || name === "writeProcessing" || name === "writeEarlyHints") { requireStatementPosition(lowerer, call, `res.${name}(...)`); - if (lowerer.typeOf(access.expression).getSymbol()?.name === "Http2ServerResponse") { + if (ts.symbolNameOf(lowerer.typeOf(access.expression).getSymbol()) === "Http2ServerResponse") { lowerer.noLowering( `Http2ServerResponse.${name}`, call, @@ -7642,7 +7642,7 @@ function lowerHttpResMethodCall( } if (name === "addTrailers") { requireStatementPosition(lowerer, call, "res.addTrailers(...)"); - if (lowerer.typeOf(access.expression).getSymbol()?.name === "Http2ServerResponse") { + if (ts.symbolNameOf(lowerer.typeOf(access.expression).getSymbol()) === "Http2ServerResponse") { lowerer.noLowering( "Http2ServerResponse.addTrailers", call, diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 78d690c6b..bc8955a81 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -667,7 +667,7 @@ export function lowerStaticResponseCall(lowerer: Lowerer, call: ts.CallExpressio } const recvType = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression)); const sym = recvType.getAliasSymbol() ?? recvType.getSymbol(); - if (!sym || sym.name !== "Response" || !lowerer.isStdlibSymbol(sym)) return null; + if (!sym || ts.symbolName(sym) !== "Response" || !lowerer.isStdlibSymbol(sym)) return null; const recv = lowerer.lowerExpr(access.expression); if (recv.type.kind !== "dyn") return null; return lowerStaticFixedFetchMethodCall(lowerer, call, access, member, recv, [], (receiver) => @@ -753,7 +753,7 @@ function lowerStaticFixedFetchMethodCall( loc, }, then: answer, - else_: nodeThrowExpr(1, "", `${access.getText()} is not a function`, answer.type, loc), + else_: nodeThrowExpr(1, "", `${ts.getTextOfNode(access)} is not a function`, answer.type, loc), type: answer.type, loc, }; @@ -927,7 +927,7 @@ export function fenceStaticResponseMember( ): IrExpr | null { const recvType = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression)); const sym = recvType.getAliasSymbol() ?? recvType.getSymbol(); - if (!sym || sym.name !== "Response" || !lowerer.isStdlibSymbol(sym)) return null; + if (!sym || ts.symbolName(sym) !== "Response" || !lowerer.isStdlibSymbol(sym)) return null; const members = fetchAccessMemberNames(lowerer, access); if (members === null) return null; const status = (member: string) => @@ -969,7 +969,7 @@ export function fenceStaticHeadersMember( ): IrExpr | null { const recvType = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression)); const sym = recvType.getAliasSymbol() ?? recvType.getSymbol(); - if (!sym || sym.name !== "Headers" || !lowerer.isStdlibSymbol(sym)) return null; + if (!sym || ts.symbolName(sym) !== "Headers" || !lowerer.isStdlibSymbol(sym)) return null; const members = fetchAccessMemberNames(lowerer, access); if (members === null) return null; const status = (member: string) => @@ -1026,7 +1026,8 @@ function isStdlibFetchInterface(lowerer: Lowerer, node: ts.Node, owner: string): for (const symbol of [type.getSymbol(), type.getAliasSymbol()]) { // NodeJS.ReadableStream shares the WHATWG interface's name. if ( - symbol?.name === owner && + symbol !== undefined && + ts.symbolName(symbol) === owner && lowerer.isStdlibSymbol(symbol) && !symbol.declarations.some( (d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS"), @@ -1093,10 +1094,10 @@ function fetchInterfaceInventoryOwner( const symbol = [type.getSymbol(), type.getAliasSymbol()].find( (candidate) => candidate !== undefined && - NODE24_FETCH_COMPAT_PROFILE.inventory.interfaces.includes(candidate.name) && + NODE24_FETCH_COMPAT_PROFILE.inventory.interfaces.includes(ts.symbolName(candidate)) && lowerer.isStdlibSymbol(candidate), ); - return symbol ? { owner: symbol.name, symbol } : null; + return symbol ? { owner: ts.symbolName(symbol), symbol } : null; } function fenceFetchObjectMembers( @@ -1308,7 +1309,7 @@ export function lowerFetchElementMethodCall( kind: "dynInvoke", recv: receiverRef, method: member, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: argumentRefs, type: DYN, loc, @@ -1321,7 +1322,7 @@ export function lowerFetchElementMethodCall( let result: IrExpr = nodeThrowExpr( 1, "", - `${access.getText()} is not a function`, + `${ts.getTextOfNode(access)} is not a function`, invocationType, loc, ); @@ -1627,9 +1628,10 @@ export function lowerStaticAbortControllerCall( const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression)); const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( - symbol?.name !== "AbortController" || + symbol === undefined || + ts.symbolName(symbol) !== "AbortController" || !symbol.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return null; @@ -1652,7 +1654,7 @@ export function lowerStaticAbortControllerCall( kind: "dynInvoke", recv: receiverRef, method: "abort", - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: argumentRefs, type: DYN, loc, @@ -1681,9 +1683,10 @@ export function lowerStaticReadableStreamControllerCall( const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression)); const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( - symbol?.name !== "ReadableStreamDefaultController" || + symbol === undefined || + ts.symbolName(symbol) !== "ReadableStreamDefaultController" || !symbol.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return null; @@ -1707,7 +1710,7 @@ export function lowerStaticReadableStreamControllerCall( kind: "dynInvoke", recv: receiver, method: member, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: argumentRefs, type: DYN, loc, @@ -1756,7 +1759,7 @@ export function lowerStaticReadableStreamCancelCall( kind: "dynInvoke", recv: receiverRef, method: "cancel", - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: argumentRefs, type: DYN, loc, @@ -1789,9 +1792,10 @@ export function lowerStaticAbortSignalListenerCall( const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression)); const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol(); if ( - (symbol?.name !== "AbortSignal" && symbol?.name !== "EventTarget") || + ((symbol === undefined || ts.symbolName(symbol) !== "AbortSignal") && + (symbol === undefined || ts.symbolName(symbol) !== "EventTarget")) || !symbol.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return null; @@ -1814,7 +1818,7 @@ export function lowerStaticAbortSignalListenerCall( kind: "dynInvoke", recv: receiver, method: member, - calleeName: access.getText(), + calleeName: ts.getTextOfNode(access), args: argumentRefs, type: DYN, loc, @@ -2146,9 +2150,10 @@ export function lowerStaticReadableStreamReaderCall( const receiverTs = lowerer.typeOf(access.expression); const symbol = receiverTs.getSymbol(); if ( - symbol?.name !== "ReadableStreamDefaultReader" || + symbol === undefined || + ts.symbolName(symbol) !== "ReadableStreamDefaultReader" || !symbol.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return null; @@ -2189,7 +2194,7 @@ export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression if (call.arguments.nodes.length !== 1) { lowerer.unsupported("SC1090", call, "dynamic import() with import attributes"); } - const res = lowerer.dynImports.get(`${call.getSourceFile().fileName}\u0000${arg.text}`); + const res = lowerer.dynImports.get(`${ts.getSourceFileOfNode(call).fileName}\u0000${arg.text}`); if (res === undefined) lowerer.refuseUnsupportedApi("'import()'", call); if (res.kind === "program-module") { return lowerOwnStaticModuleImport(lowerer, call, arg); @@ -2569,6 +2574,6 @@ export function npmMemberFence(lowerer: Lowerer, access: ts.PropertyAccessExpres export function npmPackageOfSymbol(lowerer: Lowerer, sym: ts.Symbol | undefined): string | null { const decls = sym ? sym.declarations : undefined; if (!decls || decls.length === 0) return null; - if (!decls.every((d) => lowerer.isNpmFile(d.getSourceFile()))) return null; - return npmPackageNameOf(decls[0]!.getSourceFile().fileName); + if (!decls.every((d) => lowerer.isNpmFile(ts.getSourceFileOfNode(d)))) return null; + return npmPackageNameOf(ts.getSourceFileOfNode(decls[0]!).fileName); } diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index ed5d97480..efa2813f1 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1,7 +1,15 @@ +import { + armFallbackSite, + fallbackSplitsAt, + fallbackTainted, + lowerFallbackTail, + openFallbackSite, +} from "./checked-fallback.js"; import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js"; import { declaredMemberOf } from "./expressions/property-access.js"; import { hostsAll } from "./expressions/hosted-narrowing.js"; import { requestGeneralStreamSurface } from "./general-streams.js"; +import { lowerForAwaitDynStream } from "./dyn-streams.js"; import { matchAllRowsReadByElements, storedMatchAllRowsReadByElements, @@ -256,8 +264,8 @@ function npmStaticReturnedEmptyArrayType( decl.initializer === undefined || !ts.isArrayLiteralExpression(decl.initializer) || decl.initializer.elements.nodes.length !== 0 || - !isJsSourceFile(decl.getSourceFile()) || - npmStaticPackageOfPath(decl.getSourceFile().fileName) === null || + !isJsSourceFile(ts.getSourceFileOfNode(decl)) || + npmStaticPackageOfPath(ts.getSourceFileOfNode(decl).fileName) === null || lowerer.ctx.returnType.kind !== "array" ) { return null; @@ -296,8 +304,8 @@ function npmStaticOptionalSelfWriteType( ): (IrType & { kind: "union" }) | null { if ( !ts.isIdentifier(decl.name) || - !isJsSourceFile(decl.getSourceFile()) || - npmStaticPackageOfPath(decl.getSourceFile().fileName) === null || + !isJsSourceFile(ts.getSourceFileOfNode(decl)) || + npmStaticPackageOfPath(ts.getSourceFileOfNode(decl).fileName) === null || initial.kind === "union" ) { return null; @@ -359,7 +367,9 @@ function stdlibTypeNamed(lowerer: Lowerer, node: ts.Node, name: string): boolean if (t.flags & ts.TypeFlags.Union) return constituentTypes(t).some(visit); const sym = t.getAliasSymbol() ?? t.getSymbol(); return ( - sym?.name === name && sym.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile())) + sym !== undefined && + ts.symbolName(sym) === name && + sym.declarations.some((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) ); }; return visit(lowerer.typeOf(node)); @@ -613,7 +623,7 @@ function pureAnnotatedDeadConst(stmt: ts.Statement, sf: ts.SourceFile): boolean return decls.every((d) => { let init = d.initializer; if (init === undefined) return false; - const trivia = sf.text.slice(init.pos, init.getStart(sf)); + const trivia = sf.text.slice(init.pos, ts.tokenStartOfNode(init)); if (!trivia.includes("@__PURE__") && !trivia.includes("#__PURE__")) return false; // `as` casts are type-world wrappers (`(() => {...})() as unknown as // { new (): any }` — cookie's NullObject shape); the runtime @@ -640,7 +650,7 @@ function pureAnnotatedDeadConst(stmt: ts.Statement, sf: ts.SourceFile): boolean * probes that would otherwise claim (and diagnose) the call-initializer * shape. */ export function provenanceElidedConstDecl(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { - const sf = decl.getSourceFile(); + const sf = ts.getSourceFileOfNode(decl); if (!isProvenanceSourceFile(sf.fileName)) return false; const stmt = decl.parent?.parent; if (!ts.isVariableStatement(stmt) || !pureAnnotatedDeadConst(stmt, sf)) return false; @@ -683,8 +693,11 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir // runtimeFence — a catchable, SC-coded throw at the statement's // position — instead of failing the whole build). TypeScript keeps // compile fences. ICEs (SC9001) always stay compile errors. - const sf = stmts[0]?.getSourceFile(); - const deferFences = sf !== undefined && isJsSourceFile(sf); + const sf = stmts.length === 0 ? undefined : ts.getSourceFileOfNode(stmts[0]!); + // The dynamic lowering of a checked-cast fallback tail fences like + // JavaScript: a statement with no `any` lowering throws when a failed + // value reaches it (checked-fallback.ts). + const deferFences = sf !== undefined && (isJsSourceFile(sf) || lowerer.fallbackTaint !== null); // Register this list as open for the forward-capture machinery: a // nested function lowering mid-list may pre-declare a LATER const of // this list as a TDZ box (predeclareForwardCapture), pushing the @@ -731,6 +744,7 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir lowered = accounting; appended = true; } else { + openFallbackSite(lowerer, stmt, out); lowered = lowerer.lowerStmt(stmt); } // The lib-boundary chokepoint (lib-boundary.ts): checked-dynamic @@ -745,8 +759,17 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir else body.push(lowered); const rewritten = lowerSurplusCalls(lowerer, body); enforceLibBoundary(lowerer, rewritten); + armFallbackSite(lowerer, stmt, rewritten); if (!appended) out.push(...rewritten); } + // A checked-cast fallback site's tail starts after this + // statement: the rest of the list lowers statically and + // dynamically, selected by the site's flag. + const splits = fallbackSplitsAt(lowerer, stmt); + if (splits.length > 0) { + out.push(...lowerFallbackTail(lowerer, stmts.slice(i + 1), splits)); + break; + } // A JS statement that lowered CLEAN can still have flushed // DEFERRED collection diagnostics (a symbol lookup on an alias // resolves through flushDeferred — informational here, not a @@ -959,7 +982,7 @@ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): b return false; let type = lowerer.irTypeOf(decl.name); if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasJsTypeAnnotation(decl) && (ts.isObjectLiteralExpression(decl.initializer) || @@ -1107,7 +1130,7 @@ function jsBindingKeepsCheckedObject( nameNode: ts.Identifier, type: IrType, ): boolean { - if (!isJsSourceFile(nameNode.getSourceFile())) return false; + if (!isJsSourceFile(ts.getSourceFileOfNode(nameNode))) return false; const host = hostVariableDeclarationOf(nameNode); if (!host || host.type || hasJsTypeAnnotation(host)) return false; const arms = type.kind === "union" ? (lowerer.unions.get(type.unionId)?.arms ?? []) : [type]; @@ -1122,8 +1145,9 @@ function jsBindingKeepsCheckedObject( function mentionsSymbol(lowerer: Lowerer, node: ts.Node, symbol: ts.Symbol): boolean { if (ts.isIdentifier(node)) return lowerer.checker.getSymbolAtLocation(node) === symbol; return ( - node.forEachChild((child) => (mentionsSymbol(lowerer, child, symbol) ? true : undefined)) === - true + ts.forEachChild(node, (child) => + mentionsSymbol(lowerer, child, symbol) ? true : undefined, + ) === true ); } @@ -1169,7 +1193,8 @@ function lowerJsVarFromExisting( symbol: ts.Symbol, ): IrStmt | null { if (!ts.isIdentifier(decl.name) || !decl.initializer) return null; - if (!isJsSourceFile(decl.getSourceFile()) || lowerer.ctx.hoistedVars.has(symbol)) return null; + if (!isJsSourceFile(ts.getSourceFileOfNode(decl)) || lowerer.ctx.hoistedVars.has(symbol)) + return null; if (lowerer.bindingIn(lowerer.ctx, symbol)) return null; let node: ts.Expression = decl.initializer; while (ts.isParenthesizedExpression(node) || ts.isSatisfiesExpression(node)) @@ -1212,13 +1237,13 @@ function varBindingType(lowerer: Lowerer, nameNode: ts.Identifier): IrType | nul ) return DYN; if ( - isJsSourceFile(nameNode.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(nameNode)) && ts.isVariableDeclaration(nameNode.parent) && isNativeBuiltinValueInitializer(lowerer, nameNode.parent.initializer) ) return DYN; let type = lowerer.mapTypeOf(lowerer.typeOf(nameNode)); - if (type?.kind === "record" && isJsSourceFile(nameNode.getSourceFile())) { + if (type?.kind === "record" && isJsSourceFile(ts.getSourceFileOfNode(nameNode))) { const shape = lowerer.shapes.get(type.shapeId); if (shape && shape.fields.length === 0 && !shape.indexValue && !shape.tuple) type = DYN; } @@ -1835,7 +1860,7 @@ function lowerVarDeclarator( requireSpecOf(decl.initializer) !== null && !isNativeFfiRequire(lowerer, decl.initializer) && !isJsonRequireDecl(lowerer, decl) && - isJsSourceFile(decl.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(decl)) ) { const spec = requireSpecOf(decl.initializer)!; if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, decl, false); @@ -2084,7 +2109,7 @@ function lowerMatchAllDrainDecl( export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): boolean { const widened = lowerer.checker.getBaseTypeOfLiteralType(type); const declarationBelongs = (d: ts.Node): boolean => { - if (!lowerer.isStdlibFile(d.getSourceFile())) return false; + if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) return false; let inParseArgsType = false; for (let node: ts.Node | undefined = d.parent; node; node = node.parent) { if ( @@ -2100,7 +2125,11 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool return false; }; const sym = widened.getAliasSymbol() ?? widened.getSymbol(); - if (sym && isParseArgsDynTypeName(sym.name) && sym.declarations.some(declarationBelongs)) { + if ( + sym && + isParseArgsDynTypeName(ts.symbolName(sym)) && + sym.declarations.some(declarationBelongs) + ) { return true; } // A discriminant check erases the token alias and leaves one anonymous @@ -2111,43 +2140,6 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool .some((p) => p.declarations.some(declarationBelongs)); } -/** True when an `any` value comes from a JavaScript function's result: - * a call (possibly awaited) whose callee is declared in a JavaScript file, - * or a const bound to one. A TypeScript program importing a JavaScript - * package sees the package's inferred types (`jwtVerify` from jose - * answers `any`), and its object destructuring reads the members of that - * checked-dynamic value as JavaScript does — member reads on the same - * value already compile. Other `any` sources keep the fence. */ -function javaScriptAnyOrigin(lowerer: Lowerer, node: ts.Expression, depth: number): boolean { - let expr = node; - while (ts.isParenthesizedExpression(expr) || ts.isAwaitExpression(expr)) expr = expr.expression; - if (ts.isCallExpression(expr)) { - const callee = ts.isPropertyAccessExpression(expr.expression) - ? expr.expression.name - : expr.expression; - if (!ts.isIdentifier(callee)) return false; - const symbol = lowerer.resolveValueSymbol(callee); - const declarations = symbol ? symbol.declarations : []; - return ( - declarations.length > 0 && - declarations.every((d) => isJsSourceFile(d.getSourceFile()) && ts.isFunctionLike(d)) - ); - } - if (ts.isIdentifier(expr) && depth === 0) { - const symbol = lowerer.resolveValueSymbol(expr); - const declaration = symbol ? symbol.valueDeclaration : undefined; - return ( - declaration !== undefined && - ts.isVariableDeclaration(declaration) && - ts.isIdentifier(declaration.name) && - declaration.initializer !== undefined && - (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0 && - javaScriptAnyOrigin(lowerer, declaration.initializer, depth + 1) - ); - } - return false; -} - /** Destructuring declarations, desugared as sugar over indexed/field * reads: the initializer evaluates ONCE into a hidden temp, then each * bound name declares (or assigns its pre-registered module global) from @@ -2186,18 +2178,18 @@ export function lowerDestructuringDecl( const parseArgsDynObject = init.type.kind === "dyn" && isParseArgsDynCheckerType(lowerer, lowerer.typeOf(decl.initializer)); - // An `any`-origin source needs a represented record or array shape for - // destructuring. JS files retain their per-site runtime fences. + // An `any`-origin source destructures through a represented record or + // array shape, or through the checked-dynamic value it lowered to: the + // pattern reads members (or iterates) the dyn value exactly as a + // JavaScript file's pattern does, with the same per-site runtime fences + // (null/undefined sources throw Node's TypeError). Primitive and unit + // sources keep their own fences below; other carriers keep this one. if ( lowerer.anyOrigin(decl.initializer) && - !isJsSourceFile(decl.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(decl)) && init.type.kind !== "record" && init.type.kind !== "array" && - !( - init.type.kind === "dyn" && - ts.isObjectBindingPattern(decl.name) && - javaScriptAnyOrigin(lowerer, decl.initializer, 0) - ) + init.type.kind !== "dyn" ) { lowerer.anyOpFence("destructuring", decl); } @@ -2210,7 +2202,7 @@ export function lowerDestructuringDecl( // the checker's own type instead of the carrier. if ( init.type.kind === "string" && - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !checkerStringSource(lowerer, decl.initializer) ) { lowerer.unsupported( @@ -2228,7 +2220,7 @@ export function lowerDestructuringDecl( init.type.kind === "bool" || init.type.kind === "string" || isUnitType(init.type)) && - !isJsSourceFile(decl.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(decl)) && !( init.type.kind === "string" && staticStringPattern(lowerer, decl.name as ts.ArrayBindingPattern | ts.ObjectBindingPattern) @@ -2252,7 +2244,7 @@ export function lowerDestructuringDecl( // (the message is the fence's — a unit value here is the failure // path either way). TypeScript keeps the fence: narrowing first is // the annotated fix. - if (isJsSourceFile(decl.getSourceFile()) && init.type.kind === "union") { + if (isJsSourceFile(ts.getSourceFileOfNode(decl)) && init.type.kind === "union") { const def = lowerer.unions.get(init.type.unionId); const dataArms = (def?.arms ?? []).filter((a) => !isUnitType(a)); const arm = dataArms[0]; @@ -2302,6 +2294,50 @@ export function lowerDestructuringDecl( if (groupsSource !== null) init = groupsSource; const tmp = lowerer.declareHiddenLocal("%destr", init.type); out.push({ kind: "varDecl", localId: tmp.id, init, loc }); + // A TypeScript `any` source destructured through its checked-dynamic + // value: RequireObjectCoercible with V8's own TypeError ("Cannot + // destructure property 'a' of 'f(...)' as it is null.") for the source + // spellings the assignment form reproduces; other sources are refused, + // as in the assignment form. JavaScript files keep their existing + // per-read errors. + if ( + init.type.kind === "dyn" && + ts.isObjectBindingPattern(decl.name) && + !isJsSourceFile(ts.getSourceFileOfNode(decl)) && + lowerer.anyOrigin(decl.initializer) + ) { + const spelling = destrSpellingOf(decl.initializer); + if (spelling === null) { + lowerer.unsupported( + "SC1031", + decl.initializer, + "destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call)", + ); + } + const first = decl.name.elements.nodes[0]; + const firstKey = + first === undefined || first.dotDotDotToken !== undefined + ? undefined + : (first.propertyName ?? first.name); + // V8 names the first property when its key is an identifier, a string + // or a number; a computed first key takes the bare (source-only) form. + const firstProp = + firstKey === undefined || ts.isComputedPropertyName(firstKey) + ? undefined + : (patternKeyNameOf(lowerer, firstKey as ts.PropertyName) ?? undefined); + out.push({ + kind: "exprStmt", + expr: { + kind: "dynDestrCheck", + value: { kind: "varRef", localId: tmp.id, type: init.type, loc }, + spelling, + ...(firstProp !== undefined ? { firstProp } : {}), + type: init.type, + loc, + }, + loc, + }); + } // V8's destructuring TypeError spells STATIC sources: an identifier // source reads " is not iterable", an identifier-callee call // " is not a function or its return value is not iterable"; @@ -2348,7 +2384,7 @@ export function matchGroupsPatternSource( !ts.isObjectBindingPattern(pattern) || pattern.elements.nodes.length === 0 || !(row.type.kind === "dyn" - ? isJsSourceFile(pattern.getSourceFile()) + ? isJsSourceFile(ts.getSourceFileOfNode(pattern)) : isMatchSliceType(lowerer, row.type)) || !pattern.elements.nodes.every((el) => { if (el.dotDotDotToken !== undefined || el.name === undefined) return false; @@ -2391,7 +2427,9 @@ export function lowerBindingPattern( const patternType = lowerer.typeOf(pattern); const patternSymbol = patternType.getSymbol(); allowDynObject ||= - patternSymbol?.name === "SegmentData" && lowerer.isStdlibSymbol(patternSymbol); + patternSymbol !== undefined && + ts.symbolName(patternSymbol) === "SegmentData" && + lowerer.isStdlibSymbol(patternSymbol); allowDynObject ||= (patternType.flags & ts.TypeFlags.Object) !== 0 && lowerer.mapTypeOf(patternType)?.kind === "dyn"; @@ -2849,7 +2887,7 @@ export function lowerBindingPattern( // object patterns use the same exact keyed-read desugar. if ( srcType.kind === "union" && - isJsSourceFile(pattern.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(pattern)) && lowerer.dynConvertible(srcType) ) { lowerer.lowerBindingPattern( @@ -3133,6 +3171,11 @@ export function lowerBindingPattern( lowerArrayPositionObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out); return; } + const nsSource = lowerer.sourceFileOfModuleNamespace(srcType); + if (nsSource !== null) { + lowerModuleNamespacePattern(lowerer, pattern, nsSource, isLet, out); + return; + } if (srcType.kind !== "record") { lowerer.unsupported( "SC1031", @@ -3252,7 +3295,7 @@ export function lowerBindingPattern( lowerer.bindPatternTarget(el.name, value, isLet, out); continue; } - if (!fieldType && isJsSourceFile(el.getSourceFile())) { + if (!fieldType && isJsSourceFile(ts.getSourceFileOfNode(el))) { // Inferred JavaScript shapes can omit optional own properties and // inherit values from their prototype. Read the live checked view. const loc = locOf(el); @@ -3386,10 +3429,73 @@ function patternBindingType(lowerer: Lowerer, name: ts.BindingName): IrType | nu const type = lowerer.typeOf(name); return ( lowerer.mapTypeOf(type) ?? - (isJsSourceFile(name.getSourceFile()) ? dynFallbackType(lowerer, name, type) : null) + (isJsSourceFile(ts.getSourceFileOfNode(name)) ? dynFallbackType(lowerer, name, type) : null) ); } +/** `const { a, b: c } = ns` / `const { f } = await import("./m")` over + * a program module's namespace: each element reads the export exactly + * like `ns.a` does — the member resolves to the exporter's own binding + * (nsMemberIdentOf's rule), read once at the pattern's position, so the + * bound name holds the export's value at that moment (destructuring + * copies; it does not alias the live binding). The namespace value + * itself was already evaluated into the pattern's temp, so its side + * effects (the dynamic import) keep their order. Rest elements would + * materialize the namespace object and keep their fence. The exports + * come from the source's module, not from the pattern's type: an + * annotation can name another module with a compatible shape. */ +function lowerModuleNamespacePattern( + lowerer: Lowerer, + pattern: ts.ObjectBindingPattern, + source: ts.SourceFile, + isLet: boolean, + out: IrStmt[], +): void { + const moduleSym = lowerer.checker.getSymbolAtLocation(source); + if (moduleSym === undefined) { + lowerer.unsupported( + "SC1031", + pattern, + "destructuring a module namespace without a module symbol", + ); + } + const nsType = lowerer.checker.getTypeOfSymbol(moduleSym); + for (const el of pattern.elements.nodes) { + if (el.name === undefined) continue; + if (el.dotDotDotToken) { + lowerer.unsupported( + "SC1031", + el, + "rest bindings over module namespaces (the namespace object is not materialized — bind the exports you need by name)", + ); + } + const prop = el.propertyName ?? el.name; + const propName = + ts.isIdentifier(prop) || + ts.isComputedPropertyName(prop) || + ts.isStringLiteralLike(prop) || + ts.isNumericLiteral(prop) + ? patternKeyNameOf(lowerer, prop as ts.PropertyName) + : null; + let exported = + propName === null ? undefined : lowerer.checker.getPropertyOfType(nsType, propName); + if (exported !== undefined && exported.flags & ts.SymbolFlags.Alias) { + exported = lowerer.checker.getAliasedSymbol(exported); + } + const declared = exported?.valueDeclaration?.name as ts.Node | undefined; + if (declared === undefined || !ts.isIdentifier(declared)) { + lowerer.unsupported( + "SC1031", + el, + "destructuring module namespace members that are not named value exports", + ); + } + let value = lowerer.lowerExpr(declared); + if (el.initializer) value = applyBindingDefault(lowerer, el, value); + lowerer.bindPatternTarget(el.name, value, isLet, out); + } +} + function patternKeyNameOf(lowerer: Lowerer, prop: ts.PropertyName): string | null { if (ts.isIdentifier(prop) || ts.isPrivateIdentifier(prop)) return prop.text; if (ts.isComputedPropertyName(prop)) return lowerer.foldedStringKeyOf(prop.expression); @@ -3416,7 +3522,7 @@ function isRegexMatchResultPattern(lowerer: Lowerer, pattern: ts.ObjectBindingPa const symbol = type.getAliasSymbol() ?? type.getSymbol(); return ( symbol !== undefined && - (symbol.name === "RegExpExecArray" || symbol.name === "RegExpMatchArray") && + (ts.symbolName(symbol) === "RegExpExecArray" || ts.symbolName(symbol) === "RegExpMatchArray") && lowerer.isStdlibSymbol(symbol) ); } @@ -3727,7 +3833,7 @@ function stdlibGlobalTokenDestructure( decl: ts.VariableDeclaration, isLet: boolean, ): IrStmt[] | null { - if (decl.initializer === undefined || !isJsSourceFile(decl.getSourceFile())) return null; + if (decl.initializer === undefined || !isJsSourceFile(ts.getSourceFileOfNode(decl))) return null; const globalName = stdlibGlobalNameOf(lowerer, decl.initializer); if (globalName === null) return null; if (!ts.isObjectBindingPattern(decl.name)) { @@ -3803,7 +3909,7 @@ function stdlibGlobalTokenDestructure( `destructuring the non-builtin member '${propName}' of 'globalThis'`, ); } - const canonical = member.name === "global" ? "globalThis" : member.name; + const canonical = ts.symbolName(member) === "global" ? "globalThis" : ts.symbolName(member); token = `[builtin ${canonical}]`; alias = canonical; // Builtin constructors read as their interned function values, the @@ -4407,7 +4513,7 @@ function classInstanceRestValue( const deferOrderCheck = lowerer.instantiationContext !== null && lowerer.onEdge !== null && - !isJsSourceFile(blame.getSourceFile()); + !isJsSourceFile(ts.getSourceFileOfNode(blame)); if (!deferOrderCheck) { if (!orderMatches(emitOrder)) { lowerer.unsupported( @@ -4425,7 +4531,7 @@ function classInstanceRestValue( // matters. const context = lowerer.instantiationContext; const countFailure = !lowerer.suppressStats; - const sf = blame.getSourceFile(); + const sf = ts.getSourceFileOfNode(blame); lowerer.shapeOrderMetadataFinalizers.push(() => { const current = lowerer.shapes.get(restT.shapeId) ?? restShape; const currentOrder = current.declaredOrder ?? current.fields.map((f) => f.name); @@ -4686,7 +4792,7 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration) if ( ts.isCallExpression(node.right) && (implicitMethodCallInfersReturn(lowerer, node.right) || - isJsSourceFile(node.right.getSourceFile())) + isJsSourceFile(ts.getSourceFileOfNode(node.right))) ) { invalid = true; return "stop"; @@ -4807,9 +4913,9 @@ export function lowerVarDecl( (decl.name.text === "module" || decl.name.text === "exports") && ts.isVariableStatement(decl.parent?.parent) && ts.isSourceFile(decl.parent?.parent?.parent) && - !isJsSourceFile(decl.getSourceFile()) && - !decl - .getSourceFile() + !isJsSourceFile(ts.getSourceFileOfNode(decl)) && + !ts + .getSourceFileOfNode(decl) .statements.nodes.some( (s) => ts.isImportDeclaration(s) || @@ -5014,7 +5120,7 @@ export function lowerVarDecl( : undefined, ); if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasJsTypeAnnotation(decl) && raw.type.kind === "dyn" && @@ -5086,7 +5192,7 @@ export function lowerVarDecl( // immediately (the type covers the unassigned state), so they // initialize to the interned undefined arm like an omitted optional. let type = lowerer.irTypeOf(decl.name); - if (type.kind === "dyn" && isLet && isJsSourceFile(decl.getSourceFile())) { + if (type.kind === "dyn" && isLet && isJsSourceFile(ts.getSourceFileOfNode(decl))) { type = inferredEvolvingLetType(lowerer, decl) ?? type; } // `var v: void;` / `let x: undefined;` — a unit-only binding rides @@ -5113,7 +5219,7 @@ export function lowerVarDecl( // function (with its hint), not the unmappable declared type. If it // poisons, still declare the local when its type is representable, so // later references to this name don't produce cascading errors. - const inferredObjectType = isJsSourceFile(decl.getSourceFile()) + const inferredObjectType = isJsSourceFile(ts.getSourceFileOfNode(decl)) ? lowerer.mapTypeOf(lowerer.typeOf(decl.name)) : null; const inferredObjectArms = @@ -5130,7 +5236,7 @@ export function lowerVarDecl( inferredObjectArms.some( (arm) => arm.kind === "record" && jsObjectBindingExpands(lowerer, decl, arm), ) || - (isJsSourceFile(decl.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasJsTypeAnnotation(decl) && ts.isObjectLiteralExpression(decl.initializer) && @@ -5146,7 +5252,7 @@ export function lowerVarDecl( try { init = expandsObject || - (isJsSourceFile(decl.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && isNativeBuiltinValueInitializer(lowerer, decl.initializer)) ? lowerer.lowerExprExpecting(decl.initializer, DYN) @@ -5183,7 +5289,7 @@ export function lowerVarDecl( (init.type.kind === "moduleNs" ? init.type : null) ?? (init.type.kind === "dyn" ? DYN : null); if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasJsTypeAnnotation(decl) && init.type.kind === "func" @@ -5214,8 +5320,8 @@ export function lowerVarDecl( if ( lowerer.implicitParamTypes !== null && init.type.kind === "dyn" && - isJsSourceFile(decl.getSourceFile()) && - npmStaticPackageOfPath(decl.getSourceFile().fileName) !== null + isJsSourceFile(ts.getSourceFileOfNode(decl)) && + npmStaticPackageOfPath(ts.getSourceFileOfNode(decl).fileName) !== null ) { type = DYN; } @@ -5223,17 +5329,17 @@ export function lowerVarDecl( init.type.kind === "dyn" && (inferredObjectType?.kind === "record" || inferredObjectType?.kind === "array" || - (isJsSourceFile(decl.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(decl)) && inferredObjectArms.some((arm) => arm.kind === "record") && inferredObjectArms.every((arm) => isUnitType(arm) || arm.kind === "record")) || - (isJsSourceFile(decl.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(decl)) && inferredObjectType !== null && (inferredObjectType.kind === "date" || inferredObjectType.kind === "func" || inferredObjectType.kind === "object" || jsOpenObjectType(decl, inferredObjectType, lowerer.shapes, lowerer.unions).kind === "dyn")) || - (isJsSourceFile(decl.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(decl)) && inferredObjectType !== null && (inferredObjectType.kind === "void" || isUnitType(inferredObjectType) || @@ -5251,14 +5357,14 @@ export function lowerVarDecl( expandsObject || preservesObjectIdentity || keepsAssertedObject || - (isJsSourceFile(decl.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && isNativeBuiltinValueInitializer(lowerer, decl.initializer)) ) { type = DYN; } if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && init.type.kind === "array" && init.type.elem.kind === "dyn" && @@ -5274,14 +5380,14 @@ export function lowerVarDecl( // cannot hold the binding's future. The binding is checked-dynamic // instead (irTypeOf's JS story): writes dynFrom, typed exits dynCheck. // Const keeps the static empty record — no reassignment exists. - if (isLet && type?.kind === "record" && isJsSourceFile(decl.getSourceFile())) { + if (isLet && type?.kind === "record" && isJsSourceFile(ts.getSourceFileOfNode(decl))) { const shape = lowerer.shapes.get(type.shapeId); if (shape && shape.fields.length === 0 && !shape.indexValue && !shape.tuple) type = DYN; } if ( isLet && type !== null && - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasJsTypeAnnotation(decl) && (type.kind === "object" || type.kind === "symbol" || jsBindingHasOpenWrites(lowerer, decl)) @@ -5305,7 +5411,7 @@ export function lowerVarDecl( if ( returnedEmptyArrayType === null && (type === null || type.kind === "array") && - isJsSourceFile(decl.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(decl)) ) { let initExpr: ts.Expression = decl.initializer; while (ts.isParenthesizedExpression(initExpr)) initExpr = initExpr.expression; @@ -5368,7 +5474,7 @@ export function lowerVarDecl( if ( type?.kind === "func" && init.type.kind === "dyn" && - (isJsSourceFile(decl.getSourceFile()) || + (isJsSourceFile(ts.getSourceFileOfNode(decl)) || (!decl.type && !hasJsTypeAnnotation(decl) && type.rest === true && @@ -5377,7 +5483,7 @@ export function lowerVarDecl( type = DYN; } if ( - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !hasJsTypeAnnotation(decl) && init.type.kind === "dyn" && @@ -5404,7 +5510,7 @@ export function lowerVarDecl( // type (a union with a fence-folded arm — the typeof-'bigint' // dual-mode ternary) adopts the initializer's static type, the // same rule genuine `any` consts take. - (type === null && isJsSourceFile(decl.getSourceFile()))) && + (type === null && isJsSourceFile(ts.getSourceFileOfNode(decl)))) && init.type.kind !== "void" && init.type.kind !== "caught" && !isUnitType(init.type) @@ -5425,7 +5531,7 @@ export function lowerVarDecl( let settledType: IrType = type; if ( !isLet && - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !hasJsTypeAnnotation(decl) && decl.initializer && ts.isNewExpression(decl.initializer) @@ -5697,7 +5803,7 @@ function jsObjectBindingExpands( decl.type || !decl.initializer || !ts.isIdentifier(decl.name) || - !isJsSourceFile(decl.getSourceFile()) + !isJsSourceFile(ts.getSourceFileOfNode(decl)) ) return false; let initializer = decl.initializer; @@ -5807,7 +5913,10 @@ function jsObjectBindingExpands( export function hasJsTypeAnnotation(decl: ts.VariableDeclaration): boolean { const statement = decl.parent?.parent; return ( - !!statement && /@type\b/.test(decl.getSourceFile().text.slice(statement.pos, decl.getStart())) + !!statement && + /@type\b/.test( + ts.getSourceFileOfNode(decl).text.slice(statement.pos, ts.tokenStartOfNode(decl)), + ) ); } @@ -5815,7 +5924,7 @@ export function hasJsTypeAnnotation(decl: ts.VariableDeclaration): boolean { * an any-valued callback. Keep those bindings open, including captured writes. */ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { if ( - !isJsSourceFile(decl.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(decl)) || decl.type || hasJsTypeAnnotation(decl) || !ts.isIdentifier(decl.name) @@ -5843,11 +5952,44 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar writes = indexed; lowerer.jsBindingWritesByOwner.set(owner, writes); } - return (writes.get(decl.name.text) ?? []).some( - (node) => - lowerer.resolveValueSymbol(node.left as ts.Identifier) === symbol && - (lowerer.typeOf(node.right).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0, + const candidates = (writes.get(decl.name.text) ?? []).filter( + (node) => lowerer.resolveValueSymbol(node.left as ts.Identifier) === symbol, ); + if ( + candidates.some( + (node) => + (lowerer.typeOf(node.right).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0, + ) + ) + return true; + // A plain JavaScript write of a value the declared type cannot hold (the + // checker does not report it): buffer-crc32 replaces its number[] table + // with `new Int32Array(table)`. Only a different representation counts; + // writes that coerce into the declared type keep it. + // A write whose checker type is the declaration's own maps to the same + // representation; the declared type is mapped only for the others. + const declaredType = lowerer.typeOf(decl.name); + const differing = candidates.filter( + (node) => + node.operatorToken.kind === ts.SyntaxKind.EqualsToken && + lowerer.typeOf(node.right) !== declaredType, + ); + if (differing.length === 0) return false; + const declared = lowerer.mapTypeOf(declaredType); + if (declared === null || declared.kind === "dyn" || declared.kind === "union") return false; + return differing.some((node) => { + const written = lowerer.mapTypeOf(lowerer.typeOf(node.right)); + return ( + written !== null && + written.kind !== "dyn" && + written.kind !== "union" && + !isUnitType(written) && + lowerer.dynConvertible(written) && + lowerer.dynConvertible(declared) && + !typeEquals(written, declared) && + (written.kind !== declared.kind || written.kind === "bytes") + ); + }); } function immediatelyGuardedAbsenceProbe(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { @@ -6317,7 +6459,7 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt } if (accessor && errorBase) return { kind: "exprStmt", expr: obj, loc }; } - if (isJsSourceFile(expr.getSourceFile()) && obj.type.kind === "object") { + if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && obj.type.kind === "object") { const info = lowerer.classes.get(obj.type.className); const keyType = ts.isElementAccessExpression(target) ? lowerer.typeOf(target.argumentExpression) @@ -6338,7 +6480,7 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt } } if ( - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && obj.type.kind === "func" && lowerer.dynConvertible(obj.type) ) { @@ -6449,7 +6591,7 @@ function lowerCjsExportStatement( const symbol = lowerer.checker.getSymbolAtLocation(nameNode) ?? (ts.isIdentifier(nameNode) || ts.isPrivateIdentifier(nameNode) - ? lowerer.cjsModuleExportSymbol(cjs.expr.getSourceFile(), nameNode.text) + ? lowerer.cjsModuleExportSymbol(ts.getSourceFileOfNode(cjs.expr), nameNode.text) : undefined); const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined; if (!g) { @@ -6524,7 +6666,7 @@ function lowerCjsExportStatement( // later real assignment keep the ordinary path (the export IS // undefined then). if (cjsVoidInitPreambleReals(cjs) !== null) return { kind: "block", body: [], loc }; - const sf = cjs.expr.getSourceFile(); + const sf = ts.getSourceFileOfNode(cjs.expr); let root: IrGlobal | undefined; for (const stmt of sf.statements.nodes) { const whole = cjsExportAssignmentOf(stmt); @@ -6587,7 +6729,7 @@ function lowerCjsExportStatement( // were already evaluated by the const's own declaration, so this // statement may not RE-evaluate anything: expression-valued entries // (which the direct-table lowering assigns here) keep a fence. - tableObj = cjsExportTargetLiteral(cjs.expr.right, cjs.expr.getSourceFile()); + tableObj = cjsExportTargetLiteral(cjs.expr.right, ts.getSourceFileOfNode(cjs.expr)); resolvedTarget = tableObj !== null; if (!tableObj) { const single = lowerCjsSingleValueExport(lowerer, cjs.expr, loc); @@ -6649,7 +6791,7 @@ function lowerCjsExportStatement( lowerer.rejectThisInObjectMethod(method.body); const symbol = lowerer.checker.getSymbolAtLocation(method.name) ?? - lowerer.cjsModuleExportSymbol(cjs.expr.getSourceFile(), method.name.text); + lowerer.cjsModuleExportSymbol(ts.getSourceFileOfNode(cjs.expr), method.name.text); const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined; if (g) { const fn = lowerer.lowerLambda(method); @@ -6817,6 +6959,36 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm ); return { kind: "if", cond, then, else_, loc: locOf(expr) }; } + // A property write to a failed checked value in a fallback tail writes + // the dynamic object (checked-fallback.ts). + if ( + lowerer.fallbackTaint !== null && + ts.isBinaryExpression(expr) && + expr.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(expr.left) && + !expr.left.questionDotToken && + fallbackTainted(lowerer, expr.left.expression) + ) { + const loc = locOf(expr); + const recv = lowerer.lowerExpr(expr.left.expression); + if (recv.type.kind === "dyn") { + return { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "dyn.keySet", + args: [ + recv, + { kind: "strLit", value: expr.left.name.text, type: STRING, loc }, + lowerer.lowerExprExpecting(expr.right, DYN), + ], + type: VOID, + loc, + }, + loc, + }; + } + } // Assignment statements over the no-storage binding families: // - `f1 = f2` where the RHS roots at an ambient-undefined name: // Node evaluates the RHS first and dies on the root's @@ -6924,7 +7096,7 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm // global — the expression path owns it (lowerCreateRequireCall). createRequireCalleeFileOf(lowerer, expr.expression) === null ) { - const sf = expr.getSourceFile(); + const sf = ts.getSourceFileOfNode(expr); const spec = requireSpecOf(expr)!; if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, expr, false); // A bare require of a package NOTHING INSTALLED resolves, in ANY @@ -6966,7 +7138,7 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm // environment-dependent branch); the static export table has one shape. if ( !topLevelJs && - isJsSourceFile(expr.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.EqualsToken && (isCjsModuleExports(lowerer, expr.left) || isCjsExportsIdentifier(lowerer, expr.left)) @@ -7470,7 +7642,7 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm if ( recv && (recv.type.kind === "dyn" || - (isJsSourceFile(expr.getSourceFile()) && + (isJsSourceFile(ts.getSourceFileOfNode(expr)) && recv.type.kind === "union" && lowerer.dynConvertible(recv.type))) ) { @@ -7500,7 +7672,7 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm // through the checked-dynamic node and keep the dynamic write, as // do JavaScript files, whose reads use that node too. if ( - !isJsSourceFile(expr.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(expr)) && recv?.type.kind === "object" && lowerer.isErrorRootedClass(recv.type.className) && expr.left.name.text === "code" @@ -7773,8 +7945,8 @@ function lowerDiscardedLogical(lowerer: Lowerer, expr: ts.BinaryExpression): IrS // picks another lowering, exactly as before. if (!(e instanceof PoisonError) || lowerer.diagSink !== null) throw e; const added = lowerer.diags.slice(diagsBefore); - const file = expr.getSourceFile().fileName; - const start = expr.getStart(); + const file = ts.getSourceFileOfNode(expr).fileName; + const start = ts.tokenStartOfNode(expr); const local = added.length > 0 && added.every( @@ -7924,16 +8096,42 @@ export function lowerDestructuringAssign( /** V8's spelling of a destructuring RHS in its TypeError message * ("Cannot destructure 'a' as it is undefined.") — plain identifiers - * spell their name (parens unwrap), and calls append an ellipsis to a - * supported callee spelling. Other CallPrinter forms keep a fence. */ + * spell their name (parens unwrap), `this`, property chains and + * identifier or numeric element reads spell their source, awaited and + * yielded values spell "(intermediate value)", and calls append an + * ellipsis to a supported callee spelling. Other CallPrinter forms keep + * a fence. */ function destrSpellingOf(rhs: ts.Expression): string | null { let e = rhs; - while (ts.isParenthesizedExpression(e)) e = e.expression; + // Parentheses and the type-only wrappers are not part of the emitted + // JavaScript expression V8 prints. + while ( + ts.isParenthesizedExpression(e) || + ts.isAsExpression(e) || + ts.isSatisfiesExpression(e) || + ts.isTypeAssertion(e) || + ts.isNonNullExpression(e) + ) { + e = e.expression; + } if (ts.isIdentifier(e)) return e.text; + if (e.kind === ts.SyntaxKind.ThisKeyword) return "this"; + // V8 prints an awaited or yielded operand as "(intermediate value)". + if (ts.isAwaitExpression(e) || ts.isYieldExpression(e)) return "(intermediate value)"; if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) { const base = destrSpellingOf(e.expression); return base === null ? null : `${base}.${e.name.text}`; } + // `table[key]` and `list[0]`; V8 prints a numeric key in its canonical + // number form (`o[1.50]` reads "o[1.5]"). + if (ts.isElementAccessExpression(e) && !e.questionDotToken) { + const base = destrSpellingOf(e.expression); + const key = e.argumentExpression; + if (base === null) return null; + if (ts.isIdentifier(key)) return `${base}[${key.text}]`; + if (ts.isNumericLiteral(key)) return `${base}[${String(Number(key.text))}]`; + return null; + } if (ts.isCallExpression(e) && !e.questionDotToken) { const callee = destrSpellingOf(e.expression); return callee === null ? null : `${callee}(...)`; @@ -8019,7 +8217,7 @@ function destructuringAssignInto( const patternEmpty = ts.isObjectLiteralExpression(target) ? target.properties.nodes.length === 0 : target.elements.nodes.length === 0; - if (!patternEmpty && rhs !== null && isJsSourceFile(rhs.getSourceFile())) { + if (!patternEmpty && rhs !== null && isJsSourceFile(ts.getSourceFileOfNode(rhs))) { const globalName = stdlibGlobalNameOf(lowerer, rhs); if (globalName !== null) { lowerer.unsupported( @@ -9036,6 +9234,11 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { ) { return lowerForAwaitStdin(lowerer, stmt); } + // A checked-dynamic NodeJS.ReadableStream iterates its native stream. + const dynStream = lowerForAwaitDynStream(lowerer, stmt, () => + lowerForAwaitBuiltin(lowerer, stmt), + ); + if (dynStream) return dynStream; const builtin = lowerForAwaitBuiltin(lowerer, stmt); if (builtin) return builtin; // A NodeJS.ReadableStream iterates as the general Readable. @@ -10809,7 +11012,7 @@ function lowerForIn(lowerer: Lowerer, stmt: ts.ForInStatement): IrStmt { while (root?.base) root = root.base; if ( info?.decl && - isJsSourceFile(info.decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(info.decl)) && !info.def.runtime && !root?.def.runtime && !info.builtinError && diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index eb919909b..b9add95a6 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -49,7 +49,8 @@ import { sideEffectFreeOptionValue } from "./surfaces.js"; import { lowerDynObjectLiteral } from "./expressions/object-literals.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { genResultRecord } from "../type-mapper.js"; -import { requestGeneralStreamSurface } from "./general-streams.js"; +import { noteUntypedStreamFlow, requestGeneralStreamSurface } from "./general-streams.js"; +import { dynNativeBranch } from "./dyn-streams.js"; import { extractIteratorValue, genStep, genValue, stepDrivable } from "./lower-generators.js"; import { BOOL, @@ -170,6 +171,16 @@ export const UNDERSCORE_METHODS: ReadonlyMap = new Map([ ["flush", "_flush"], ]); +/** The completion callback's parameter position in each underscore + * method that takes one (cbTuples' "fn" positions). */ +export const STREAM_COMPLETION_CALLBACK_INDEX: ReadonlyMap = new Map([ + ["_write", 2], + ["_final", 0], + ["_destroy", 1], + ["_transform", 2], + ["_flush", 0], +]); + /** The accepted option-callback names (canonical flag order) and the * duplex-shaped-head answer for one runtime stream class. */ export function streamCtorShape(cls: string): { @@ -1464,7 +1475,7 @@ export function lowerStreamStaticCall( member === "toWeb" && cls === "%Readable" && call.arguments.nodes.length === 2 && - isJsSourceFile(call.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(call)) && ts.isObjectLiteralExpression(call.arguments.nodes[1]!) ) { for (const p of call.arguments.nodes[1]!.properties.nodes) { @@ -1498,7 +1509,7 @@ export function lowerStreamStaticCall( ); } const type: IrType = { kind: "object", className: "%Readable" }; - const js = isJsSourceFile(call.getSourceFile()); + const js = isJsSourceFile(ts.getSourceFileOfNode(call)); const srcNode = args[0]; const optsNode = args[1]; const srcT = srcNode ? lowerer.mapTypeOf(lowerer.typeOf(srcNode)) : null; @@ -2081,13 +2092,16 @@ function lowerStreamArg( // a boxed native stream unwraps at run time; other values (iterables, // functions) throw the not-supported Error there. if (v.type.kind === "dyn" && what === "pipeline") { + noteUntypedStreamFlow(lowerer, null); return { kind: "libCall", fn: "stream.unboxDyn", args: [ v, strLit( - "pipeline stages other than native streams (iterables, generators, plain functions, and { readable, writable } pairs) are not supported yet", + lowerer.dynNodeStreams + ? "pipeline stages other than native streams (JavaScript stream objects, iterables, generators, plain functions, and { readable, writable } pairs) are not supported yet" + : "pipeline stages other than native streams (iterables, generators, plain functions, and { readable, writable } pairs) are not supported yet", locOf(node), ), ], @@ -2113,7 +2127,7 @@ function lowerStreamArg( loc: locOf(node), }); }; - if (isJsSourceFile(node.getSourceFile()) && lowerer.dynConvertible(v.type)) { + if (isJsSourceFile(ts.getSourceFileOfNode(node)) && lowerer.dynConvertible(v.type)) { if (what === "pipeline") { // Strings and arrays are iterables — VALID pipeline stages in // Node — and a record carrying a readable/writable member may be @@ -3127,7 +3141,7 @@ function lowerOperatorCallback( if (v.type.kind === "dyn") return v; if ( v.type.kind !== "func" && - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && lowerer.dynConvertible(v.type) ) { // JavaScript passes any value; Node's validateFunction throws. @@ -3403,6 +3417,63 @@ function pipeIntoStdio( }; } +/** `readable.unpipe(untyped)`: a native destination (boxed) unpipes as a + * typed one does; any other value unpipes the JavaScript destination a + * pipe into it registered (scr_stream_dyn.c), or does nothing when there + * is none, as Node's unpipe does for a destination it is not piped into. + * The call answers the source. */ +function unpipeDyn(lowerer: Lowerer, receiver: IrExpr, dst: IrExpr, loc: SrcLoc): IrExpr { + noteUntypedStreamFlow(lowerer, null); + const source = lowerer.declareHiddenLocal("%unpipeSource", receiver.type); + const destination = lowerer.declareHiddenLocal("%unpipeDestination", dst.type); + const sourceRef = varRef(source.id, receiver.type, loc); + const destinationRef = varRef(destination.id, dst.type, loc); + const stmts: IrStmt[] = [ + { kind: "varDecl", localId: source.id, init: receiver, loc }, + { kind: "varDecl", localId: destination.id, init: dst, loc }, + ]; + const unpipeNative = (native: IrExpr): IrStmt => ({ + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "readable.unpipe", + args: [sourceRef, native], + type: receiver.type, + loc, + }, + loc, + }); + if (lowerer.dynNodeStreams) { + stmts.push( + dynNativeBranch(lowerer, destinationRef, "w", unpipeNative, { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "stream.unpipeDyn", + args: [sourceRef, destinationRef], + type: VOID, + loc, + }, + loc, + }), + ); + } else { + stmts.push( + unpipeNative({ + kind: "libCall", + fn: "stream.unboxDyn", + args: [ + destinationRef, + strLit("unpipe destinations other than native streams are not supported yet", loc), + ], + type: { kind: "object", className: "%Duplex" }, + loc, + }), + ); + } + return { kind: "seqExpr", stmts, result: sourceRef, type: receiver.type, loc }; +} + /** The pipe() libCall for a destination of type `dst`. Under the general * stream representation a Writable slot can hold the converted * process.stdout / process.stderr, which a pipe never ends (as in Node), so @@ -3423,6 +3494,7 @@ function pipeIntoDyn( target: IrExpr, ): IrExpr { const loc = locOf(call); + noteUntypedStreamFlow(lowerer, null); const receiverValue = lowerer.lowerExpr(access.expression); const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type); const destination = lowerer.declareHiddenLocal("%pipeDestination", target.type); @@ -3432,6 +3504,59 @@ function pipeIntoDyn( ]; const end = pipeEndOption(lowerer, call.arguments.nodes[1], loc); const duplex: IrType = { kind: "object", className: "%Duplex" }; + // Checked-dynamic stream slots (dyn-streams.ts): a JavaScript stream + // object is a destination too, piped with Node's listener protocol. + if (lowerer.dynNodeStreams) { + const sourceRef: IrExpr = { kind: "varRef", localId: source.id, type: receiverValue.type, loc }; + const destinationRef: IrExpr = { + kind: "varRef", + localId: destination.id, + type: target.type, + loc, + }; + const ending = lowerer.declareHiddenLocal("%pipeEnd", BOOL); + stmts.push({ kind: "varDecl", localId: ending.id, init: end, loc }); + const endRef: IrExpr = { kind: "varRef", localId: ending.id, type: BOOL, loc }; + stmts.push( + dynNativeBranch( + lowerer, + destinationRef, + "w", + (native) => ({ + kind: "exprStmt", + expr: { + kind: "libCall", + // Checked-dynamic slots come with the general representation, + // where a boxed native destination can be the converted + // process.stdout (generalPipe). + fn: "readable.pipeGeneral", + args: [sourceRef, native, endRef], + type: native.type, + loc, + }, + loc, + }), + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "stream.pipeDyn", + args: [sourceRef, lowerer.coerceToExpected(sourceRef, DYN), destinationRef, endRef], + type: VOID, + loc, + }, + loc, + }, + ), + ); + return { + kind: "seqExpr", + stmts, + result: destinationRef, + type: target.type, + loc, + }; + } stmts.push({ kind: "exprStmt", expr: { @@ -3746,6 +3871,7 @@ export function lowerStreamMethodCall( const receiver = lowerer.lowerExpr(access.expression); if (args[0] !== undefined) { const dst = lowerer.lowerExpr(args[0]); + if (dst.type.kind === "dyn") return unpipeDyn(lowerer, receiver, dst, loc); const dstInfo = dst.type.kind === "object" ? lowerer.classes.get(dst.type.className) : undefined; const dstSides = streamSidesOf(lowerer, dstInfo); @@ -3760,6 +3886,44 @@ export function lowerStreamMethodCall( loc, }; } + // Checked-dynamic slots: unpipe() also unpipes the JavaScript + // destinations stream.pipeDyn registered (stream.unpipeDyn with an + // undefined destination), as Node's unpipe() empties every pipe. + if (lowerer.dynNodeStreams) { + const source = lowerer.declareHiddenLocal("%unpipeSource", receiver.type); + const sourceRef = varRef(source.id, receiver.type, loc); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: source.id, init: receiver, loc }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "readable.unpipe", + args: [sourceRef], + type: receiver.type, + loc, + }, + loc, + }, + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "stream.unpipeDyn", + args: [sourceRef, dynUndefinedExpr(loc)], + type: VOID, + loc, + }, + loc, + }, + ], + result: sourceRef, + type: receiver.type, + loc, + }; + } return { kind: "libCall", fn: "readable.unpipe", args: [receiver], type: receiver.type, loc }; } diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index 32870c2cf..391a77245 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -86,8 +86,8 @@ function requireStatementPosition(lowerer: Lowerer, call: ts.CallExpression, wha * reports the `test` property's column), at the call for plain ones. */ function atStringOf(expr: ts.CallExpression): string { const target = ts.isPropertyAccessExpression(expr.expression) ? expr.expression.name : expr; - const sf = expr.getSourceFile(); - const pos = sf.getLineAndCharacterOfPosition(target.getStart()); + const sf = ts.getSourceFileOfNode(expr); + const pos = sf.getLineAndCharacterOfPosition(ts.tokenStartOfNode(target)); return `${sf.fileName}:${pos.line + 1}:${pos.character + 1}`; } diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts index 22983fbb4..6b6a1b5d1 100644 --- a/packages/compiler/src/frontend/lowering/lower-timers.ts +++ b/packages/compiler/src/frontend/lowering/lower-timers.ts @@ -420,7 +420,7 @@ function lowerTimerCall( lowerer.noLowering(`clearTimeout with ${expr.arguments.nodes.length} arguments`, expr); } let handle = lowerer.lowerExpr(expr.arguments.nodes[0]!); - if (handle.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) { + if (handle.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { const stored = lowerer.declareHiddenLocal("%timer.clear", DYN); const ref = varRef(stored.id, DYN, loc); handle = { diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ecd511495..3915ee9e1 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,5 +1,9 @@ import { onlyForwarded } from "./typed-arguments.js"; -import { needsGeneralNodeStreams, stdioIntoGeneralStream } from "./general-streams.js"; +import { + needsGeneralNodeStreams, + noteUntypedStreamFlow, + stdioIntoGeneralStream, +} from "./general-streams.js"; import { extendAssertedRecordMembers } from "./assertion-members.js"; import { jsDynamicDeclarations } from "./js-dynamic-objects.js"; import { builtinClassValue, builtinMemberClassKind } from "./builtins/builtin-class-dyn.js"; @@ -28,8 +32,12 @@ import { scanDivergences } from "./divergences.js"; import { applyKeyOrderWitnesses, KeyOrderNotes } from "./key-order-witness.js"; import { checkedClassAssertion } from "./class-assertions.js"; import { narrowStoredClassValue, narrowGenericClassValue } from "./class-unions.js"; -import { isOptionalProcessStreamProperty } from "./builtins/process.js"; +import { + isOptionalProcessStreamProperty, + isOptionalProcessVersionsRead, +} from "./builtins/process.js"; import { isSettledIteratorValueRead } from "./lower-generators.js"; +import { isRebindableFunctionSymbol } from "./rebindable-functions.js"; import { concreteCollectionNarrow } from "./collection-narrowing.js"; import { lowerWorkerMetadata } from "./builtins/workers.js"; import { fenceStaticBlobMember, lowerBlobNew, lowerStaticBlobCall } from "./builtins/blob.js"; @@ -131,6 +139,7 @@ import { earlyReadVarSymbols } from "./var-hoisting.js"; import { recordTextCodecClass } from "../../ir/ir.js"; import { assignLegacyTiers, type DynamicFunction } from "../../ir/tiers.js"; import { promoteJsvals } from "../../ir/jsval.js"; +import { fallbackTainted, type FallbackSite, type FallbackTaint } from "./checked-fallback.js"; import { analyzeTiers, dynamicStatementCount, @@ -894,6 +903,11 @@ export interface LowererMode { * generalNodeStreams). lowerToIr turns it on only after a pass without * it saw a stream flow that needs it (requiresGeneralNodeStreams). */ generalNodeStreams?: boolean; + /** With generalNodeStreams, the checked-dynamic stream slots + * (TypeMapperCtx.dynNodeStreams). lowerToIr turns it on only after a + * pass saw an untyped value flow into a stream slot + * (requiresDynNodeStreams). */ + dynNodeStreams?: boolean; /** LoadResult.inlineRequireCycleCalls. */ inlineRequireCycleCalls?: ReadonlySet | undefined; /** LowerOptions.libraryCallbacks (see there). */ @@ -1063,7 +1077,11 @@ export function lowerToIr( // a general stream into a stdio-represented stream slot lowers again // with it. Every other program keeps the single pass above unchanged. const generalNodeStreams = reachableEmit.requiresGeneralNodeStreams; - if (generalNodeStreams) { + // Checked-dynamic stream slots (dyn-streams.ts) are a further per-program + // decision under the general representation: only a program that moves an + // untyped value into a stream slot lowers again with them. + let dynNodeStreams = reachableEmit.requiresDynNodeStreams; + const streamPass = (dyn: boolean): Lowerer => { const general = new Lowerer(program, entry, moduleOrder, { frontendServices: options.frontendServices, projectCheckJs: options.projectCheckJs ?? false, @@ -1079,6 +1097,7 @@ export function lowerToIr( tierMode, ffiBindingSymbols: ffiValidation.symbolsByName, generalNodeStreams: true, + ...(dyn ? { dynNodeStreams: true } : {}), }); for (const d of extraModuleCycleDiags) general.pushDiag(d); for (const d of ffiValidation.diagnostics) general.pushDiag(d); @@ -1086,7 +1105,15 @@ export function lowerToIr( reachable = generalEmitted.reachable; result = generalEmitted.result; resultLowerer = general; - timing("general-streams-relower", { reachable: reachable.size }); + timing(dyn ? "dyn-streams-relower" : "general-streams-relower", { reachable: reachable.size }); + return general; + }; + if (generalNodeStreams) { + const general = streamPass(dynNodeStreams); + if (!dynNodeStreams && general.requiresDynNodeStreams) { + dynNodeStreams = true; + streamPass(true); + } } // A generic class-rest support decision can depend on record metadata // whose historical owner is discovered only while retained bodies lower. @@ -1100,6 +1127,7 @@ export function lowerToIr( frontendServices: options.frontendServices, projectCheckJs: options.projectCheckJs ?? false, ...(generalNodeStreams ? { generalNodeStreams: true } : {}), + ...(dynNodeStreams ? { dynNodeStreams: true } : {}), reachable, targetPlatform, startupCrash, @@ -1191,11 +1219,12 @@ export function lowerToIr( timing("wasm-discovery", { modules: wasmModules.length }); } if (options.coverage !== true) return result; - const remainderPass = (general: boolean): Lowerer => + const remainderPass = (general: boolean, dyn = dynNodeStreams): Lowerer => new Lowerer(program, entry, moduleOrder, { frontendServices: options.frontendServices, projectCheckJs: options.projectCheckJs ?? false, ...(general ? { generalNodeStreams: true } : {}), + ...(general && dyn ? { dynNodeStreams: true } : {}), reachable, remainder: true, alreadyFlushed: resultLowerer.flushedSymbols, @@ -1216,7 +1245,11 @@ export function lowerToIr( // stream flow there that needs the general representation lowers the // remainder again with it, as reachable emit would. if (!generalNodeStreams && remainder.requiresGeneralNodeStreams) { - remainder = remainderPass(true); + remainder = remainderPass(true, remainder.requiresDynNodeStreams); + rem = remainder.run(); + } + if (!dynNodeStreams && remainder.generalNodeStreams && remainder.requiresDynNodeStreams) { + remainder = remainderPass(true, true); rem = remainder.run(); } timing("coverage-remainder"); @@ -1329,7 +1362,7 @@ export function uncheckedOverloadHandleCall( * `never` at the ROOT stays out (`for (const v of [])`'s loop var — the * dead read the f64 mapping is FOR). */ export function neverTaintedJsType(lowerer: Lowerer, node: ts.Node, t: ts.Type): boolean { - if (!isJsSourceFile(node.getSourceFile())) return false; + if (!isJsSourceFile(ts.getSourceFileOfNode(node))) return false; const walk = (x: ts.Type, depth: number): boolean => { if (depth === 0) return false; if (x.isUnionType()) return ts.constituentTypes(x).some((a) => walk(a, depth - 1)); @@ -1359,7 +1392,7 @@ export function ladderFenceExpr( const loc = locOf(node); const d = noLoweringDiag(surface, loc, hint); lowerer.runtimeFences.push(d); - const sf = node.getSourceFile(); + const sf = ts.getSourceFileOfNode(node); const pos = ts.getLineAndCharacterOfPosition(sf, loc.start); return { kind: "strLit", @@ -1371,7 +1404,7 @@ export function ladderFenceExpr( export function dynFallbackType(lowerer: Lowerer, node: ts.Node, t: ts.Type): IrType | null { if (t.flags & ts.TypeFlags.Void) return null; - if (!isJsSourceFile(node.getSourceFile())) { + if (!isJsSourceFile(ts.getSourceFileOfNode(node))) { if (t.flags & ts.TypeFlags.Any) return DYN; const symbol = t.getSymbol(); const args = @@ -1386,16 +1419,17 @@ export function dynFallbackType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir ts.TypeFlags.Any) !== 0; if ( - symbol?.name === "Promise" && + symbol !== undefined && + ts.symbolName(symbol) === "Promise" && args[0] && (args[0].flags & ts.TypeFlags.Any || anyArray(args[0])) ) - return symbol.name === "Promise" && anyArray(args[0]) + return ts.symbolName(symbol) === "Promise" && anyArray(args[0]) ? { kind: "promise", inner: DYN } : jsFallbackPromiseType(lowerer, t); if ( symbol && - ["Generator", "AsyncGenerator", "IterableIterator"].includes(symbol.name) && + ["Generator", "AsyncGenerator", "IterableIterator"].includes(ts.symbolName(symbol)) && args.some((arg) => arg.flags & ts.TypeFlags.Any) ) return jsFallbackGeneratorType(lowerer, t); @@ -1417,7 +1451,8 @@ export function dynFallbackType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir if (generator) return generator; const symbol = t.getSymbol(); if ( - (symbol?.name === "Map" || symbol?.name === "ReadonlyMap") && + ((symbol !== undefined && ts.symbolName(symbol) === "Map") || + (symbol !== undefined && ts.symbolName(symbol) === "ReadonlyMap")) && lowerer.isStdlibSymbol(symbol) ) { const args = lowerer.checker.getTypeArguments(t as ts.TypeReference); @@ -1506,7 +1541,12 @@ export function dynFallbackType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir * has no fixed shape. The settled value crosses the checked native boundary. */ function jsFallbackPromiseType(lowerer: Lowerer, t: ts.Type): IrType | null { const symbol = t.getSymbol(); - if (symbol?.name !== "Promise" || !lowerer.isStdlibSymbol(symbol)) return null; + if ( + symbol === undefined || + ts.symbolName(symbol) !== "Promise" || + !lowerer.isStdlibSymbol(symbol) + ) + return null; const inner = lowerer.checker.getTypeArguments(t as ts.TypeReference)[0]; return inner ? { kind: "promise", inner: lowerer.mapTypeOf(inner) ?? DYN } : null; } @@ -1517,7 +1557,7 @@ function jsFallbackGeneratorType(lowerer: Lowerer, t: ts.Type): IrType | null { const symbol = t.getSymbol(); if ( !symbol || - !["Generator", "AsyncGenerator", "IterableIterator"].includes(symbol.name) || + !["Generator", "AsyncGenerator", "IterableIterator"].includes(ts.symbolName(symbol)) || !lowerer.isStdlibSymbol(symbol) ) return null; @@ -1525,7 +1565,7 @@ function jsFallbackGeneratorType(lowerer: Lowerer, t: ts.Type): IrType | null { const unit = ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Never; return { kind: "generator", - ...(symbol.name === "AsyncGenerator" ? { async: true as const } : {}), + ...(ts.symbolName(symbol) === "AsyncGenerator" ? { async: true as const } : {}), yieldT: yielded && yielded.flags & ts.TypeFlags.Never ? VOID : DYN, retT: !returned || returned.flags & unit ? VOID : DYN, nextT: !resumed || resumed.flags & unit ? UNDEFINED_T : DYN, @@ -1559,7 +1599,7 @@ function jsFallbackFunctionType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir const sigDecl = sig.declaration; const jsUnitReturn = sigDecl !== undefined && - isJsSourceFile(sigDecl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(sigDecl)) && (retT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0; const ret: IrType = jsUnitReturn ? DYN @@ -1593,7 +1633,7 @@ function jsArgumentsFunctionType(lowerer: Lowerer, t: ts.Type): IrType | null { ts.isFunctionExpression(decl) || ts.isArrowFunction(decl) ) || - !isJsSourceFile(decl.getSourceFile()) + !isJsSourceFile(ts.getSourceFileOfNode(decl)) ) return null; const hasRest = decl.parameters.nodes.some((p) => p.dotDotDotToken !== undefined); @@ -1601,7 +1641,8 @@ function jsArgumentsFunctionType(lowerer: Lowerer, t: ts.Type): IrType | null { !hasRest && (ts.isArrowFunction(decl) || !bodyReadsArguments(decl) || - (decl.parameters.nodes.length > 0 && !isNodeEsmFile(decl.getSourceFile(), lowerer.program))) + (decl.parameters.nodes.length > 0 && + !isNodeEsmFile(ts.getSourceFileOfNode(decl), lowerer.program))) ) return null; const shapes = paramShapes(lowerer, decl.parameters.nodes); @@ -1820,6 +1861,9 @@ export class Lowerer { /** Syntactic writes indexed once per JS scope. Symbol and RHS type * checks remain contextual to each binding probe. */ readonly jsBindingWritesByOwner = new Map>(); + /** JavaScript function declarations their own module reassigns + * (rebindable-functions.ts). */ + readonly rebindableFunctions = new Set(); /** TRAP bindings: declarations whose initializer provably throws before * producing a value (its chain roots at an ambient-undefined name — * ambientUndefVarRootOf). Module init unwinds at the declaration, so no @@ -2181,6 +2225,10 @@ export class Lowerer { runtimeOptionalBindingType(node: ts.Node): IrType | null; runtimeOptionalBindingType(node: ts.Node, fallback: IrType): IrType; runtimeOptionalBindingType(node: ts.Node, fallback?: IrType): IrType | null { + // A binding holding a failed checked value is dynamic: undefined is one + // of its values already (checked-fallback.ts). + if (this.fallbackTaint !== null && ts.isIdentifier(node) && fallbackTainted(this, node)) + return fallback ?? null; const patternType = this.runtimeOptionalPatternTypes.get(node); if (patternType) return patternType; const symbol = ts.isIdentifier(node) @@ -2245,7 +2293,7 @@ export class Lowerer { return ( sym !== undefined && sym.declarations.some((d) => { - if (!isJsSourceFile(d.getSourceFile())) return false; + if (!isJsSourceFile(ts.getSourceFileOfNode(d))) return false; // The pattern itself, or the `= {}` default of a pattern parameter. const pattern = ts.isObjectLiteralExpression(d) && @@ -2361,7 +2409,7 @@ export class Lowerer { // A checked JavaScript indexed read can already be boxed. The checker // calls the element present, but that inference is not a runtime guard. if ( - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && value.kind === "dynCheck" && value.value.type.kind === "dyn" ) @@ -2580,6 +2628,14 @@ export class Lowerer { /** True while re-lowering a base function's 2nd+ instance: the same source * statements were already counted for the first instance. */ suppressStats = false; + /** Checked-cast fallback (checked-fallback.ts): sites opened by their + * root's declaration, by root; armed sites whose tails have not split + * yet; the taint of the dynamic tail being lowered; and the number of + * splits whose tails are being lowered (each lowers its tail twice). */ + fallbackSites = new Map(); + fallbackArmed: FallbackSite[] = []; + fallbackTaint: FallbackTaint | null = null; + fallbackSplitDepth = 0; /** Comparisons interned by operand representations and equality semantics. */ readonly equalityHelpers = new Map(); /** Synthetic array-HOF loop functions (map/filter/forEach desugar), @@ -2603,6 +2659,15 @@ export class Lowerer { * a stdio handle reached an 'unknown' slot. lowerToIr then lowers the * program again under the general stream representation. */ requiresGeneralNodeStreams = false; + /** An untyped value reached a stream slot (a checked unwrap into a + * runtime stream class, or an untyped pipe/pipeline stage). lowerToIr + * then lowers the program again with checked-dynamic stream slots + * (dyn-streams.ts). */ + requiresDynNodeStreams = false; + /** Receiver representations forced by the per-value stream dispatch + * (dyn-streams.ts): the native arm lowers a member call on the unboxed + * stream through the ordinary class-method path. */ + readonly receiverTypeOverrides = new Map(); /** Helpers that snapshot shape declaration order into their bodies. * Reachability lowers inits before the declarations they discover, so * these rebuild after the worklist restores historical shape metadata. */ @@ -3065,6 +3130,7 @@ export class Lowerer { * committed refusals in "auto" and keeps them refusals in "static-only". */ readonly tierMode: TierMode; readonly generalNodeStreams: boolean; + readonly dynNodeStreams: boolean; /** Functions finishModule placed in the dynamic tier (ir/tiers.ts). */ dynamicFunctions: DynamicFunction[] = []; /** Their IR, for coverage's dynamic statement count. */ @@ -3194,6 +3260,7 @@ export class Lowerer { this.targetPlatform = mode.targetPlatform ?? process.platform; this.tierMode = mode.tierMode ?? "auto"; this.generalNodeStreams = mode.generalNodeStreams ?? false; + this.dynNodeStreams = this.generalNodeStreams && (mode.dynNodeStreams ?? false); this.workerTargets = mode.workerTargets ?? []; this.workerTargets.forEach((sf, id) => { this.workerTargetIdByPath.set(tsgoPath(resolve(sf.fileName)), id); @@ -3274,7 +3341,8 @@ export class Lowerer { if (this.localClassInstantiations.some((entry) => entry.owner === parent)) break; if ( parent.typeParameters?.nodes.length || - (implicitMonoFile(parent.getSourceFile()) && monomorphizedImplicit(this, parent)) + (implicitMonoFile(ts.getSourceFileOfNode(parent)) && + monomorphizedImplicit(this, parent)) ) return null; } @@ -3291,6 +3359,7 @@ export class Lowerer { this.externalTypeSpecifiersByFile.has(tsgoPath(resolve(sf.fileName))), typeMemo: this.typeMemo, ...(this.generalNodeStreams ? { generalNodeStreams: true } : {}), + ...(this.dynNodeStreams ? { dynNodeStreams: true } : {}), canMemoizeType: () => this.typeParamBindings === null && this.typeParamTsBindings === null && @@ -3299,15 +3368,15 @@ export class Lowerer { // lowering, long after the constructor completes. isProgramFile: (sf) => this.fileTag.has(sf), lifetimeRegionClass: (decl) => - isExperimentalDeclarationFile(decl.getSourceFile(), this.experimentalFiles) + isExperimentalDeclarationFile(ts.getSourceFileOfNode(decl), this.experimentalFiles) ? lifetimeRegionType(this) : null, internTableClass: (decl) => - isExperimentalDeclarationFile(decl.getSourceFile(), this.experimentalFiles) + isExperimentalDeclarationFile(ts.getSourceFileOfNode(decl), this.experimentalFiles) ? internTableType(this) : null, threadSlotsClass: (decl) => - isExperimentalDeclarationFile(decl.getSourceFile(), this.experimentalFiles) + isExperimentalDeclarationFile(ts.getSourceFileOfNode(decl), this.experimentalFiles) ? threadSlotsType(this) : null, moduleNamespaceId: (type) => this.moduleNamespaceIdOfType(type), @@ -3373,9 +3442,10 @@ export class Lowerer { const weak = (part: ts.Type): boolean => { const symbol = part.getSymbol(); return ( - (symbol?.name === "WeakMap" || symbol?.name === "WeakSet") && + ((symbol !== undefined && ts.symbolName(symbol) === "WeakMap") || + (symbol !== undefined && ts.symbolName(symbol) === "WeakSet")) && symbol.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && this.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && this.isStdlibFile(ts.getSourceFileOfNode(d)), ) ); }; @@ -3484,9 +3554,12 @@ export class Lowerer { } return false; })() - ? this.qualify(decl.getSourceFile(), `%cx${decl.getStart()}.${decl.name?.text ?? ""}`) + ? this.qualify( + ts.getSourceFileOfNode(decl), + `%cx${ts.tokenStartOfNode(decl)}.${decl.name?.text ?? ""}`, + ) : this.qualify( - decl.getSourceFile(), + ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + (decl.name ? decl.name.text : "%anon"), ); for (let index = this.localClassInstantiations.length - 1; index >= 0; index--) { @@ -3730,7 +3803,10 @@ export class Lowerer { return null; if (this.externalTypes.has(importDecl.moduleSpecifier.text)) return importDecl.moduleSpecifier.text; - const dep = this.moduleSourceFileOf(importDecl.getSourceFile(), importDecl.moduleSpecifier); + const dep = this.moduleSourceFileOf( + ts.getSourceFileOfNode(importDecl), + importDecl.moduleSpecifier, + ); return dep !== null && !dep.isDeclarationFile ? this.externalTypeSpecifierOfModuleExport(dep, member, new Set(), new Set()) : null; @@ -3778,7 +3854,7 @@ export class Lowerer { // A namespace OBJECT from a project module is not wholly external; // property accesses resolve their selected member separately above. if (importedName === null) return null; - const dep = this.moduleSourceFileOf(decl.getSourceFile(), specNode); + const dep = this.moduleSourceFileOf(ts.getSourceFileOfNode(decl), specNode); return dep !== null && !dep.isDeclarationFile ? this.externalTypeSpecifierOfModuleExport(dep, importedName, seenSymbols, seenExports) : null; @@ -3786,7 +3862,7 @@ export class Lowerer { for (const decl of declarations) { const owners = this.externalTypeSpecifiersByFile.get( - tsgoPath(resolve(decl.getSourceFile().fileName)), + tsgoPath(resolve(ts.getSourceFileOfNode(decl).fileName)), ); if (owners !== undefined && owners.length > 0) return owners[0]!; } @@ -3831,7 +3907,7 @@ export class Lowerer { } if (d === undefined || spec === undefined || !ts.isStringLiteral(spec) || name !== "default") return null; - const dep = resolveImport(this.program, d.getSourceFile(), spec.text); + const dep = resolveImport(this.program, ts.getSourceFileOfNode(d), spec.text); if (!dep) return null; sym = defaultExportSymbolOf(this, dep) ?? undefined; } @@ -3947,8 +4023,8 @@ export class Lowerer { // Project aliases/self-references resolve through the same entry point as // relative imports; an opted-in --npm-static package is the fallback. const dep = - resolveImport(this.program, importDecl.getSourceFile(), spec) ?? - npmStaticDepSf7(this.program, importDecl.getSourceFile(), spec); + resolveImport(this.program, ts.getSourceFileOfNode(importDecl), spec) ?? + npmStaticDepSf7(this.program, ts.getSourceFileOfNode(importDecl), spec); if (!dep || !isJsSourceFile(dep) || isNodeEsmFile(dep, this.program)) return null; return dep; } @@ -3988,8 +4064,8 @@ export class Lowerer { if (directSpec !== null) { if (isJsonRequireDecl(this, decl)) return null; return ( - resolveImport(this.program, decl.getSourceFile(), directSpec, "require") ?? - npmStaticDepSf7(this.program, decl.getSourceFile(), directSpec, "require") + resolveImport(this.program, ts.getSourceFileOfNode(decl), directSpec, "require") ?? + npmStaticDepSf7(this.program, ts.getSourceFileOfNode(decl), directSpec, "require") ); } return createRequireProgramModuleOf(this, decl.initializer)?.dep ?? null; @@ -4329,7 +4405,7 @@ export class Lowerer { * report on the next tick. Later loads are cache hits either way. */ punycodeRequireWarning(node: ts.Node): IrStmt { const loc = locOf(node); - const fromDependency = /[\\/]node_modules[\\/]/.test(node.getSourceFile().fileName); + const fromDependency = /[\\/]node_modules[\\/]/.test(ts.getSourceFileOfNode(node).fileName); return { kind: "exprStmt", expr: { @@ -4400,8 +4476,8 @@ export class Lowerer { // those globals stay uninitialized: the dep's module body would never // run. const dep = - resolveImport(this.program, node.getSourceFile(), spec, "require") ?? - npmStaticDepSf7(this.program, node.getSourceFile(), spec, "require"); + resolveImport(this.program, ts.getSourceFileOfNode(node), spec, "require") ?? + npmStaticDepSf7(this.program, ts.getSourceFileOfNode(node), spec, "require"); if (!dep || dep.fileName.endsWith(".json")) return null; if (this.asyncInitFiles.has(dep)) { this.unsupported( @@ -4418,7 +4494,7 @@ export class Lowerer { expr: { kind: "call", callee: initName, args: [], type: VOID, loc }, loc, }; - const parentModule = this.cjsModuleIdOf.get(node.getSourceFile()); + const parentModule = this.cjsModuleIdOf.get(ts.getSourceFileOfNode(node)); const childModule = this.cjsModuleIdOf.get(dep); if (parentModule === undefined || childModule === undefined) return init; return { @@ -5037,6 +5113,7 @@ export class Lowerer { if (isIndexedRead(e)) return true; if (ts.isPropertyAccessExpression(e) && isRecordIndexPropertyRead(e)) return true; if (ts.isPropertyAccessExpression(e) && isOptionalProcessStreamProperty(this, e)) return true; + if (isOptionalProcessVersionsRead(this, e)) return true; if (ts.isPropertyAccessExpression(e) && isSettledIteratorValueRead(this, e)) return true; if (ts.isPropertyAccessExpression(e) && isOsConstantsSymbol(symbolOf(e.expression), this)) return true; @@ -5179,7 +5256,7 @@ export class Lowerer { const decl = symbol.valueDeclaration; if (!decl || !ts.isPropertyDeclaration(decl) || !ts.isIdentifier(decl.name)) return false; if (ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Static) return false; - const sf = decl.getSourceFile(); + const sf = ts.getSourceFileOfNode(decl); if (isJsSourceFile(sf) || sf.isDeclarationFile) return false; const declared = typeOrNull(decl.name); const mapped = declared ? this.mapTypeOf(declared) : null; @@ -5269,7 +5346,8 @@ export class Lowerer { const initSymbol = initType.getAliasSymbol() ?? initType.getSymbol(); const matchResult = initSymbol !== undefined && - (initSymbol.name === "RegExpExecArray" || initSymbol.name === "RegExpMatchArray"); + (ts.symbolName(initSymbol) === "RegExpExecArray" || + ts.symbolName(initSymbol) === "RegExpMatchArray"); if (arrayType?.kind !== "array" || matchResult) return scanRecordPattern(name, init); const positions = name.elements.nodes.filter((el) => { const key = el.propertyName; @@ -5445,7 +5523,7 @@ export class Lowerer { }; for (const [symbol, signature] of signatureBySymbol) { const declaration = functionDeclBySymbol.get(symbol); - if (!declaration || !isJsSourceFile(declaration.getSourceFile())) continue; + if (!declaration || !isJsSourceFile(ts.getSourceFileOfNode(declaration))) continue; const checked = this.checker.getSignatureFromDeclaration(declaration); const checkedReturn = checked ? this.checker.getReturnTypeOfSignature(checked) : null; // Checker-known optional results already narrow at each use. Only @@ -6287,7 +6365,7 @@ export class Lowerer { const callee = functionDeclBySymbol.get(symbol); if ( callee && - isJsSourceFile(callee.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(callee)) && !node.arguments.nodes.some(ts.isSpreadElement) ) { for (let i = 0; i < sig.params.length; i++) { @@ -6503,7 +6581,7 @@ export class Lowerer { for (const [symbol, decl] of functionDeclBySymbol) if (scanReturns(symbol, decl)) changed = true; const noteGenericReturn = (symbol: ts.Symbol, decl: ts.FunctionLikeDeclaration): void => { - if (optionalReturns.has(symbol) || isJsSourceFile(decl.getSourceFile())) return; + if (optionalReturns.has(symbol) || isJsSourceFile(ts.getSourceFileOfNode(decl))) return; if (!returnsOf(decl).some(genericMayBeOptional)) return; optionalReturns.add(symbol); this.runtimeOptionalGenericReturns.add(decl); @@ -6780,7 +6858,7 @@ export class Lowerer { if ( global.type.kind !== "func" || !ts.isBinaryExpression(declaration) || - !isJsSourceFile(declaration.getSourceFile()) + !isJsSourceFile(ts.getSourceFileOfNode(declaration)) ) continue; const value = peel(declaration.right); @@ -7256,7 +7334,7 @@ export class Lowerer { if (n === 0) return; this.stats.statementsTotal += n; this.stats.statementsFailed += n; - const file = decl.getSourceFile().fileName; + const file = ts.getSourceFileOfNode(decl).fileName; for (let i = 0; i < n; i++) { this.bumpFileStat(file, "total"); this.bumpFileStat(file, "failed"); @@ -7869,14 +7947,18 @@ export class Lowerer { ): IrStmt & { kind: "runtimeFence" } { const loc = locOf(node); const code = first?.code ?? fallback?.code ?? "SC1090"; - const baseMessage = + const plainMessage = first?.message ?? fallback?.message ?? "this statement uses a construct with no static lowering"; + const baseMessage = + this.fallbackTaint !== null + ? `a value that failed its checked cast reached code with no dynamic lowering: ${plainMessage}` + : plainMessage; const messageLoc = first?.loc ?? loc; const source = first - ? (this.program.getSourceFile(messageLoc.file) ?? node.getSourceFile()) - : node.getSourceFile(); + ? (this.program.getSourceFile(messageLoc.file) ?? ts.getSourceFileOfNode(node)) + : ts.getSourceFileOfNode(node); const pos = ts.getLineAndCharacterOfPosition(source, messageLoc.start); return { kind: "runtimeFence", @@ -7995,10 +8077,12 @@ export class Lowerer { // here; constructor objects such as Headers still can. if ( typeSym && - (FETCH_AMBIENT_TYPES as readonly string[]).includes(typeSym.name) && + (FETCH_AMBIENT_TYPES as readonly string[]).includes(ts.symbolName(typeSym)) && this.isStdlibSymbol(typeSym) ) { - this.pushDiag(unsupportedNativeApiDiag(`a value of type '${typeSym.name}'`, locOf(node))); + this.pushDiag( + unsupportedNativeApiDiag(`a value of type '${ts.symbolName(typeSym)}'`, locOf(node)), + ); throw new PoisonError(); } // ENUM OBJECTS (`typeof e` — a numeric enum's type carries the @@ -8012,7 +8096,7 @@ export class Lowerer { unsupportedDiag( "SC1090", locOf(node), - `enum objects as values ('${typeSym.name}' — member reads like '${typeSym.name}.X' compile to constants; the object itself has no runtime representation)`, + `enum objects as values ('${ts.symbolName(typeSym)}' — member reads like '${ts.symbolName(typeSym)}.X' compile to constants; the object itself has no runtime representation)`, ), ); throw new PoisonError(); @@ -8042,7 +8126,7 @@ export class Lowerer { ownSym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - this.isStdlibFile(d.getSourceFile()), + this.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { this.pushDiag(noLoweringDiag(this.checker.typeToString(type), locOf(node))); @@ -8088,7 +8172,7 @@ export class Lowerer { stdlibOwnSym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - this.isStdlibFile(d.getSourceFile()), + this.isStdlibFile(ts.getSourceFileOfNode(d)), ); // OVERLOADED call signatures (SC2007) — after the generic branch, so a // generic overload set keeps the monomorphization story: a value of a @@ -8150,7 +8234,7 @@ export class Lowerer { noLoweringDiag( this.checker.typeToString(type), locOf(node), - typeHints[stdlibOwnSym?.name ?? ""], + typeHints[ts.symbolNameOf(stdlibOwnSym) ?? ""], ), ); throw new PoisonError(); @@ -8178,6 +8262,9 @@ export class Lowerer { } stdlibMemberFence(access: ts.PropertyAccessExpression): void { + // A member of a failed checked value reads dynamically, whatever the + // static member resolves to (checked-fallback.ts). + if (this.fallbackTaint !== null && fallbackTainted(this, access.expression)) return; return stdlibMemberFence(this, access); } @@ -8227,7 +8314,11 @@ export class Lowerer { } typeOf(node: ts.Node): ts.Type { - if (ts.isPropertyAccessExpression(node) && isJsSourceFile(node.getSourceFile())) { + // The dynamic lowering of a checked-cast fallback tail reads values + // derived from the failed value as `any`. + if (this.fallbackTaint !== null && fallbackTainted(this, node)) + return this.checker.getAnyType(); + if (ts.isPropertyAccessExpression(node) && isJsSourceFile(ts.getSourceFileOfNode(node))) { const receiver = node.expression.kind === ts.SyntaxKind.ThisKeyword && this.fnStack.length > 0 ? this.resolveThis()?.type @@ -8248,7 +8339,7 @@ export class Lowerer { } // A symbol-keyed field read through a CommonJS key-table key reads as // the field's layout type (tableKeyFieldType). - if (ts.isElementAccessExpression(node) && isJsSourceFile(node.getSourceFile())) { + if (ts.isElementAccessExpression(node) && isJsSourceFile(ts.getSourceFileOfNode(node))) { const field = tableKeyFieldType(this, node); if (field) { if (field.kind === "f64") return this.checker.getNumberType(); @@ -8262,7 +8353,7 @@ export class Lowerer { // into native field loads when the method is borrowed by another object. if ( node.kind === ts.SyntaxKind.ThisKeyword && - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && this.fnStack.length > 0 && this.resolveThis()?.type.kind === "dyn" ) @@ -8536,7 +8627,7 @@ export class Lowerer { // locals at null/undefined despite later writes. Keep mutable native // storage instead of a slot restricted to the initializer's unit value. if ( - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && (t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0 ) return DYN; @@ -8575,8 +8666,12 @@ export class Lowerer { * validator ICEs on them as the usual backstop. */ requireExactShape(node: ts.Node, actual: IrType, expected: IrType): void { if (typeEquals(actual, expected)) return; - if (!this.generalNodeStreams && needsGeneralNodeStreams(this, actual, expected)) + if (!this.generalNodeStreams && needsGeneralNodeStreams(this, actual, expected)) { this.requiresGeneralNodeStreams = true; + // An untyped value into a stream slot also needs the checked-dynamic + // slots (dyn-streams.ts). + if (actual.kind === "dyn") this.requiresDynNodeStreams = true; + } // dyn mismatches first. A dyn ('unknown') value flowing into a typed // slot needs a CHECKED cast — the hint points at `as `; tsc // usually rejects this before we do, so the fence is mostly defensive. @@ -8968,6 +9063,7 @@ export class Lowerer { (id) => this.unions.get(id), ); const handleOk = DYN_HANDLE_KINDS.has(expected.kind); + noteUntypedStreamFlow(this, expected); if ( canDynCheckTo( expected, @@ -9562,7 +9658,9 @@ export class Lowerer { : []; const errorOptions = parts.some((part) => { const sym = part.getAliasSymbol() ?? part.getSymbol(); - return sym?.name === "ErrorOptions" && this.isStdlibSymbol(sym); + return ( + sym !== undefined && ts.symbolName(sym) === "ErrorOptions" && this.isStdlibSymbol(sym) + ); }); if (errorOptions) { const actual = this.typeOf(node); @@ -9598,7 +9696,7 @@ export class Lowerer { let literal = node; while (ts.isParenthesizedExpression(literal)) literal = literal.expression; if ( - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && !ts.isArrayLiteralExpression(literal) && e.kind === "dynFrom" && e.value.type.kind === "array" @@ -9630,7 +9728,7 @@ export class Lowerer { if ( e.type.kind === "union" && !typeEquals(e.type, expected) && - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && this.dynConvertible(e.type) ) { const sourceArms = this.unions.get(e.type.unionId)?.arms ?? []; @@ -9685,7 +9783,7 @@ export class Lowerer { !ts.isIdentifier(d.name) || !ts.isVariableDeclarationList(d.parent) || !ts.isVariableStatement(d.parent.parent) || - d.getSourceFile().isDeclarationFile || + ts.getSourceFileOfNode(d).isDeclarationFile || (ts.getCombinedModifierFlags(d) & ts.ModifierFlags.Ambient) !== 0 ) { return null; @@ -9904,11 +10002,11 @@ export class Lowerer { const builtinOption = (name: string): boolean => parts.some((part) => { const sym = part.getAliasSymbol() ?? part.getSymbol(); - return sym?.name === name && this.isStdlibSymbol(sym); + return ts.symbolNameOf(sym) === name && this.isStdlibSymbol(sym); }); if (ts.isObjectLiteralExpression(x)) { if ( - isJsSourceFile(x.getSourceFile()) || + isJsSourceFile(ts.getSourceFileOfNode(x)) || builtinOption("RequestInit") || builtinOption("ErrorOptions") || builtinOption("SpawnOptions") || @@ -9991,7 +10089,7 @@ export class Lowerer { coerceExistingInto(node: ts.Expression, e: IrExpr, expected: IrType): IrExpr { if ( (expected.kind === "union" && this.armTag(expected.unionId, UNDEFINED_T) >= 0) || - (expected.kind === "dyn" && isJsSourceFile(node.getSourceFile())) + (expected.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(node))) ) { // A destination that accepts undefined must retain an unchecked // read's storage value even when the checker still calls it present. @@ -10014,7 +10112,8 @@ export class Lowerer { return null; const symbol = this.resolveValueSymbol(value.expression); if (!symbol || !this.isStdlibSymbol(symbol)) return null; - const kind = symbol.name === "Map" ? "map" : symbol.name === "Set" ? "set" : null; + const kind = + ts.symbolName(symbol) === "Map" ? "map" : ts.symbolName(symbol) === "Set" ? "set" : null; if (!kind) return null; const arms = expected.kind === "union" ? (this.unions.get(expected.unionId)?.arms ?? []) : [expected]; @@ -10139,7 +10238,7 @@ export class Lowerer { while (ts.isParenthesizedExpression(literal)) literal = literal.expression; return ( expected.kind === "dyn" && - isJsSourceFile(node.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(node)) && ts.isObjectLiteralExpression(literal) ); } @@ -10605,7 +10704,7 @@ export class Lowerer { } } } - if (!isJsSourceFile(decl.getSourceFile())) continue; + if (!isJsSourceFile(ts.getSourceFileOfNode(decl))) continue; for (const member of decl.members.nodes) { if (ts.isConstructorDeclaration(member)) { for (const param of member.parameters.nodes) { @@ -10638,7 +10737,7 @@ export class Lowerer { } // npm-static implicit-any classification resolves body parameter // references while collecting the method's shape. - if (implicitMonoFile(decl.getSourceFile()) && member.body) { + if (implicitMonoFile(ts.getSourceFileOfNode(decl)) && member.body) { symbolRoots.push(member.body); } } @@ -11624,6 +11723,9 @@ export class Lowerer { isSelfReference(ident: ts.Identifier): boolean { const ctx = this.ctx; if (!ctx.selfSymbol) return false; + // A reassigned JavaScript declaration names its module global, which + // may hold another function by the time the body runs. + if (isRebindableFunctionSymbol(this, ctx.selfSymbol)) return false; // The self binding can be shadowed by a scoped local of the same symbol? // No — a shadow is a different symbol; symbol identity is exact. return this.checker.getSymbolAtLocation(ident) === ctx.selfSymbol; @@ -11899,6 +12001,7 @@ export class Lowerer { } npmMemberFence(access: ts.PropertyAccessExpression): void { + if (this.fallbackTaint !== null && fallbackTainted(this, access.expression)) return; return npmMemberFence(this, access); } @@ -12839,14 +12942,14 @@ export class Lowerer { // node:stream/web's exports are the web stream globals themselves: in // JavaScript sources the same identity token as the global spelling // (`require("stream/web").ReadableStream === globalThis.ReadableStream`). - if (bi.module === "stream/web" && isJsSourceFile(expr.getSourceFile())) { + if (bi.module === "stream/web" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { return { kind: "strLit", value: `[builtin ${bi.member}]`, type: STRING, loc }; } // A bare `fs.constants` read (not one of the baked bits above). if (bi.member === "constants" && bi.module === "fs") return fsConstantsObject(loc); // `stream.Readable` as a JavaScript value: the runtime's untyped // constructor, the base a constructor function inherits. - if (isJsSourceFile(expr.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(expr))) { const classKind = builtinMemberClassKind(bi.module, bi.member); if (classKind !== null) return builtinClassValue(classKind, loc); } @@ -12854,7 +12957,7 @@ export class Lowerer { this.unsupported( "SC1090", expr, - `library functions as values (call '${expr.getText()}' directly)`, + `library functions as values (call '${ts.getTextOfNode(expr)}' directly)`, ); } this.noLowering( diff --git a/packages/compiler/src/frontend/lowering/module-member-this.ts b/packages/compiler/src/frontend/lowering/module-member-this.ts index 592136bec..8d1592b47 100644 --- a/packages/compiler/src/frontend/lowering/module-member-this.ts +++ b/packages/compiler/src/frontend/lowering/module-member-this.ts @@ -70,9 +70,9 @@ function ignoresOwnThis(lowerer: Lowerer, fn: ts.Node, bindings: Bindings, depth for (const arg of node.arguments.nodes.slice(1)) visit(arg); return; } - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - fn.forEachChild(visit); + ts.forEachChild(fn, visit); return ignores; } @@ -176,7 +176,7 @@ function mayRebind(lowerer: Lowerer, fn: ts.Node, param: ts.Identifier): boolean found = true; return; } - node.forEachChild(visit); + ts.forEachChild(node, visit); }; // The function's own parameters declare the name; their defaults can write it. const params = (fn as { parameters?: ts.NodeArray }).parameters; @@ -195,9 +195,9 @@ function returnedExpressions(fn: ts.Node): ts.Expression[] | null { const visit = (node: ts.Node): void => { if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return; if (ts.isReturnStatement(node) && node.expression) result.push(node.expression); - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - body.forEachChild(visit); + ts.forEachChild(body, visit); return result; } diff --git a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts index 4270457d5..65efe5b2a 100644 --- a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts +++ b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts @@ -365,20 +365,22 @@ export class NanCodedReads { private removalReason(slot: Slot): string { const badSource = slot.sources.find((s) => s === null || this.sourceInfo(s) === null); if (badSource !== undefined) - return `source ${badSource ? badSource.getText().slice(0, 60) : ""}`; + return `source ${badSource ? ts.getTextOfNode(badSource).slice(0, 60) : ""}`; const nan = this.genuineNaN.has(slot); const badUse = slot.uses.find((u) => !this.useSafe(u) && (nan || !this.reboxConsumer(u))); - if (badUse) - return `${nan ? "nan-use" : "use"} ${transparentParent(badUse).parent?.getText().slice(0, 80) ?? "?"}`; + if (badUse) { + const parent = transparentParent(badUse).parent; + return `${nan ? "nan-use" : "use"} ${parent === undefined ? "?" : ts.getTextOfNode(parent).slice(0, 80)}`; + } return "?"; } private debugReport(reached: Set): void { const name = (slot: Slot): string => { const decl = this.host.declarationOf(slot.symbol); - const sf = decl?.getSourceFile(); - const where = sf && decl ? `${sf.fileName}:${decl.getStart()}` : "?"; - return `${slot.kind} ${slot.symbol.name} @ ${where}`; + const sf = decl === undefined ? undefined : ts.getSourceFileOfNode(decl); + const where = sf && decl ? `${sf.fileName}:${ts.tokenStartOfNode(decl)}` : "?"; + return `${slot.kind} ${ts.symbolName(slot.symbol)} @ ${where}`; }; for (const slot of reached) { const ok = this.eligible.has(slot); @@ -483,7 +485,11 @@ export class NanCodedReads { return true; } const decl = this.host.declarationOf(symbol); - if (!decl || isJsSourceFile(decl.getSourceFile()) || decl.getSourceFile().isDeclarationFile) + if ( + !decl || + isJsSourceFile(ts.getSourceFileOfNode(decl)) || + ts.getSourceFileOfNode(decl).isDeclarationFile + ) return false; let name: ts.Identifier; if (ts.isParameter(decl)) { @@ -565,7 +571,7 @@ export class NanCodedReads { !nonNegativeIntegerLiteral(decl.initializer) || !ts.isVariableDeclarationList(decl.parent) || !(decl.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) || - isJsSourceFile(decl.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(decl)) ) return false; const name = decl.name; @@ -685,7 +691,7 @@ export class NanCodedReads { !(decl.parent.flags & ts.NodeFlags.Const) || // A module binding can escape through an export. (ts.isVariableStatement(decl.parent.parent) && ts.isSourceFile(decl.parent.parent.parent)) || - isJsSourceFile(decl.getSourceFile()) || + isJsSourceFile(ts.getSourceFileOfNode(decl)) || this.host.bytesElemOf(decl.name) === null ) return null; @@ -755,7 +761,7 @@ export class NanCodedReads { if (!decl || (!ts.isFunctionDeclaration(decl) && !ts.isMethodDeclaration(decl))) return null; if (!decl.body || decl.asteriskToken || decl.typeParameters?.nodes || !decl.name) return null; if (!ts.isIdentifier(decl.name)) return null; - const sf = decl.getSourceFile(); + const sf = ts.getSourceFileOfNode(decl); if (isJsSourceFile(sf) || sf.isDeclarationFile) return null; const flags = ts.getCombinedModifierFlags(decl); if (flags & (ts.ModifierFlags.Async | ts.ModifierFlags.Abstract | ts.ModifierFlags.Ambient)) @@ -831,7 +837,7 @@ export class NanCodedReads { private scanBindingSlot(symbol: ts.Symbol): BindingSlot | null { const decl = this.host.declarationOf(symbol); if (!decl) return null; - const sf = decl.getSourceFile(); + const sf = ts.getSourceFileOfNode(decl); if (isJsSourceFile(sf) || sf.isDeclarationFile) return null; let nameNode: ts.Identifier; const slot: BindingSlot = { @@ -1362,7 +1368,7 @@ export class NanCodedReads { decl.initializer && ts.isVariableDeclarationList(decl.parent) && decl.parent.flags & ts.NodeFlags.Const && - !isJsSourceFile(decl.getSourceFile()) + !isJsSourceFile(ts.getSourceFileOfNode(decl)) ) return this.plainNumber(decl.initializer, depth + 1); return null; diff --git a/packages/compiler/src/frontend/lowering/narrowing-stability.ts b/packages/compiler/src/frontend/lowering/narrowing-stability.ts index 01801470d..1c991c6ea 100644 --- a/packages/compiler/src/frontend/lowering/narrowing-stability.ts +++ b/packages/compiler/src/frontend/lowering/narrowing-stability.ts @@ -191,7 +191,8 @@ export class NarrowingStability { const checker = this.lowerer.checker; const signature = checker.getResolvedSignature(call); const declaration = signature ? signature.declaration : undefined; - if (!declaration || !this.lowerer.isStdlibFile(declaration.getSourceFile())) return true; + if (!declaration || !this.lowerer.isStdlibFile(ts.getSourceFileOfNode(declaration))) + return true; const callee = skipOuter(call.expression); if ( ts.isPropertyAccessExpression(callee) && @@ -287,11 +288,11 @@ export class NarrowingStability { const last = chain.links[chain.links.length - 1]; const refKey = last ? `.${last.name}` : (chain.root as ts.Identifier).text; const candidates = facts.refs.get(refKey) ?? []; - const useStart = node.getStart(); + const useStart = ts.tokenStartOfNode(node); let start = -1; for (const candidate of candidates) { if (candidate === node) continue; - const candidateStart = candidate.getStart(); + const candidateStart = ts.tokenStartOfNode(candidate); if (candidateStart >= useStart) break; if (referenceKey(candidate) !== chain.key) continue; start = candidateStart; @@ -313,7 +314,7 @@ export class NarrowingStability { // between. let end = useStart; for (const loop of facts.loops) { - const loopStart = loop.getStart(); + const loopStart = ts.tokenStartOfNode(loop); if (loopStart >= start && loopStart <= useStart && loop.end >= node.end) end = Math.max(end, loop.end); } @@ -376,7 +377,9 @@ export class NarrowingStability { if (decl !== undefined && ts.isVariableDeclaration(decl)) { if (decl.initializer !== undefined && this.mayBeStale(decl.initializer)) result = true; else { - for (const write of this.writesIn(decl.getSourceFile()).get(symbol.name) ?? []) { + for (const write of this.writesIn(ts.getSourceFileOfNode(decl)).get( + ts.symbolName(symbol), + ) ?? []) { const assignment = write.parent; if ( ts.isBinaryExpression(assignment) && @@ -430,7 +433,7 @@ export class NarrowingStability { ts.isParameter(decl)); if (decl && mutableBinding) { const home = flowBoundary(decl); - const writes = this.writesIn(decl.getSourceFile()).get(symbol.name) ?? []; + const writes = this.writesIn(ts.getSourceFileOfNode(decl)).get(ts.symbolName(symbol)) ?? []; for (const write of writes) { if (flowBoundary(write) === home) continue; if (this.lowerer.resolveValueSymbol(write) === symbol) { @@ -497,7 +500,7 @@ export class NarrowingStability { if (name === "toString" || name === "valueOf" || name === "toJSON") facts.conversion = true; if (ts.isComputedPropertyName(node.name)) { - const text = node.name.expression.getText(); + const text = ts.getTextOfNode(node.name.expression); if (text.includes("toPrimitive")) facts.conversion = true; if (text.includes("iterator") || text.includes("asyncIterator")) facts.iteration = true; } @@ -711,7 +714,7 @@ export class NarrowingStability { const symbol = type.getSymbol(); return ( symbol !== undefined && - BUILTIN_COLLECTIONS.has(symbol.name) && + BUILTIN_COLLECTIONS.has(ts.symbolName(symbol)) && this.lowerer.isStdlibSymbol(symbol) ); } @@ -1062,7 +1065,7 @@ function memberUse( function flowBoundary(node: ts.Node): ts.Node { let n: ts.Node | undefined = node.parent; while (n && !isFlowBoundary(n)) n = n.parent; - return n ?? node.getSourceFile(); + return n ?? ts.getSourceFileOfNode(node); } function isFlowBoundary(node: ts.Node): boolean { diff --git a/packages/compiler/src/frontend/lowering/native-ffi.ts b/packages/compiler/src/frontend/lowering/native-ffi.ts index c9ff3300d..0f932ba93 100644 --- a/packages/compiler/src/frontend/lowering/native-ffi.ts +++ b/packages/compiler/src/frontend/lowering/native-ffi.ts @@ -393,7 +393,7 @@ export function lowerFfiCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr (decl) => ts.isFunctionDeclaration(decl) && decl.body === undefined && - libraryCallbackOwnsFile(lowerer, decl.getSourceFile()), + libraryCallbackOwnsFile(lowerer, ts.getSourceFileOfNode(decl)), ); if (callbackShaped) { lowerer.pushDiag( @@ -429,13 +429,13 @@ export function lowerFfiCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr if (lowerer.libraryCallbacks) { const declarations = symbol.declarations; const programDeclarations = declarations.filter((decl) => - libraryCallbackOwnsFile(lowerer, decl.getSourceFile()), + libraryCallbackOwnsFile(lowerer, ts.getSourceFileOfNode(decl)), ); const programAmbient = programDeclarations.length > 0 && programDeclarations.every( (decl) => - decl.getSourceFile().isDeclarationFile || + ts.getSourceFileOfNode(decl).isDeclarationFile || (ts.getCombinedModifierFlags(decl as ts.Declaration) & ts.ModifierFlags.Ambient) !== 0, ); diff --git a/packages/compiler/src/frontend/lowering/object-factory-new.ts b/packages/compiler/src/frontend/lowering/object-factory-new.ts index ddd8d6d56..bcb675692 100644 --- a/packages/compiler/src/frontend/lowering/object-factory-new.ts +++ b/packages/compiler/src/frontend/lowering/object-factory-new.ts @@ -41,7 +41,7 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean { !owner || !declaration?.body || !bindingNeverReassigned(lowerer, symbol, owner) || - !isJsSourceFile(declaration.getSourceFile()) || + !isJsSourceFile(ts.getSourceFileOfNode(declaration)) || declaration.asteriskToken || declaration.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ) diff --git a/packages/compiler/src/frontend/lowering/private-brands.ts b/packages/compiler/src/frontend/lowering/private-brands.ts index e7b3edbd8..8507cc81a 100644 --- a/packages/compiler/src/frontend/lowering/private-brands.ts +++ b/packages/compiler/src/frontend/lowering/private-brands.ts @@ -30,7 +30,7 @@ export function perEvaluationPrivateNames(info: ClassInfo): boolean { const declaration = info.localClass ? info.decl : null; return ( !!declaration && - isJsSourceFile(declaration.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(declaration)) && declaration.members.nodes.some( (member) => member.name !== undefined && ts.isPrivateIdentifier(member.name), ) diff --git a/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts new file mode 100644 index 000000000..09d228b05 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts @@ -0,0 +1,102 @@ +import { varRef } from "../../ir/build.js"; +import { + type IrExpr, + type IrLocal, + type IrParam, + type IrType, + type SrcLoc, + VOID, + funcOf, + typeKey, +} from "../../ir/ir.js"; +import { newFnCtx, type Lowerer } from "./lowerer.js"; + +/** An executor VALUE whose parameters are checked-dynamic (a JavaScript + * function declaration: `function run(resolve, reject) {...}; return new + * Promise(run)`): the promise's native executor is an adapter closure that + * receives the native (resolve, reject) pair and calls the original with + * both boxed into the checked-dynamic tree, so the JavaScript body calls + * them like any function value. Null when the executor's shape is not + * adaptable (more than two parameters, a rest parameter, or a parameter + * that neither is dyn nor already the native closure). */ +export function adaptPromiseExecutor( + lowerer: Lowerer, + executor: IrExpr, + inner: IrType, + loc: SrcLoc, +): IrExpr | null { + const execT = executor.type; + if (execT.kind !== "func" || execT.rest || execT.params.length > 2) return null; + if (!execT.params.some((p) => p.kind === "dyn")) return null; + const resolveT = funcOf(inner.kind === "void" ? [] : [inner], VOID); + const rejectT = funcOf([{ kind: "object", className: "%Error" }], VOID); + const native = [resolveT, rejectT]; + if ( + !execT.params.every((p, i) => p.kind === "dyn" || typeKey(p) === typeKey(native[i]!)) || + !native.slice(0, execT.params.length).every((t) => lowerer.dynConvertible(t)) + ) + return null; + const adaptT = funcOf(native, VOID); + const key = `promise.executor:${typeKey(execT)}:${typeKey(inner)}`; + let name = lowerer.arrHofHelpers.get(key); + if (name === undefined) { + name = `%promise.executor.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(key, name); + const impl = `${name}.impl`; + const captured: IrLocal = { id: "f.0", name: "f", type: execT, mutable: false, boxed: true }; + const params: IrParam[] = [ + { localId: "resolve.0", name: "resolve", type: resolveT }, + { localId: "reject.0", name: "reject", type: rejectT }, + ]; + const ctx = newFnCtx(false, null, null, VOID); + lowerer.fnStack.push(ctx); + let args: IrExpr[]; + try { + args = execT.params.map((p, i) => + lowerer.coerceToExpected(varRef(params[i]!.localId, native[i]!, loc), p), + ); + } finally { + lowerer.fnStack.pop(); + } + lowerer.liftedFns.push({ + name: impl, + params, + returnType: VOID, + captures: [{ localId: captured.id, name: captured.name, type: execT }], + locals: [ + captured, + ...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })), + ...ctx.locals, + ], + body: [ + { + kind: "exprStmt", + expr: { + kind: "callValue", + callee: varRef(captured.id, execT, loc), + args, + type: execT.ret, + loc, + }, + loc, + }, + ], + loc, + }); + lowerer.liftedFns.push({ + name, + params: [{ localId: captured.id, name: captured.name, type: execT }], + returnType: adaptT, + locals: [captured], + body: [ + { + kind: "return", + value: { kind: "closure", fnName: impl, captures: [captured.id], type: adaptT, loc }, + loc, + }, + ], + loc, + }); + } + return { kind: "call", callee: name, args: [executor], type: adaptT, loc }; +} diff --git a/packages/compiler/src/frontend/lowering/rebindable-functions.ts b/packages/compiler/src/frontend/lowering/rebindable-functions.ts new file mode 100644 index 000000000..da41833a6 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/rebindable-functions.ts @@ -0,0 +1,64 @@ +/* JavaScript function declarations that their own module reassigns. + * + * Capability probes in published packages replace a function declaration + * with the implementation that works on the first call (yazl's + * `function bufferFrom(...) { bufferFrom = modern; ... }`). A top-level + * declaration normally lowers to a static function, which cannot change. + * A reassigned one instead becomes a mutable module global holding the + * function as a checked-dynamic value: the module initializer assigns it + * before its first statement (the declaration's hoisting), and reads, + * calls and writes of the name all go through the global. TypeScript + * rejects such writes, so only JavaScript sources take this path. */ +import * as ts from "../ts7/adapter.js"; +import { isJsSourceFile } from "../program.js"; +import type { Lowerer } from "./lowerer.js"; + +/** Top-level function declarations of `sf` that an assignment anywhere in + * the file writes, recorded for the queries below. */ +export function collectRebindableFunctions( + lowerer: Lowerer, + sf: ts.SourceFile, +): Set { + const found = new Set(); + if (!isJsSourceFile(sf)) return found; + const byName = new Map(); + for (const stmt of sf.statements.nodes) { + if ( + ts.isFunctionDeclaration(stmt) && + stmt.name && + stmt.body && + stmt.typeParameters === undefined + ) + byName.set(stmt.name.text, stmt); + } + if (byName.size === 0) return found; + ts.walkPreorder(sf, (node) => { + if ( + !ts.isBinaryExpression(node) || + node.operatorToken.kind !== ts.SyntaxKind.EqualsToken || + !ts.isIdentifier(node.left) + ) + return undefined; + const decl = byName.get(node.left.text); + if (!decl || found.has(decl)) return undefined; + const symbol = lowerer.checker.getSymbolAtLocation(node.left); + if (symbol && symbol.declarations.includes(decl)) found.add(decl); + return undefined; + }); + for (const decl of found) lowerer.rebindableFunctions.add(decl); + return found; +} + +export function isRebindableFunction(lowerer: Lowerer, decl: ts.Node | undefined): boolean { + return ( + decl !== undefined && ts.isFunctionDeclaration(decl) && lowerer.rebindableFunctions.has(decl) + ); +} + +/** Whether the symbol names a reassigned function declaration. */ +export function isRebindableFunctionSymbol( + lowerer: Lowerer, + symbol: ts.Symbol | undefined, +): boolean { + return symbol !== undefined && symbol.declarations.some((d) => isRebindableFunction(lowerer, d)); +} diff --git a/packages/compiler/src/frontend/lowering/require-initialization.ts b/packages/compiler/src/frontend/lowering/require-initialization.ts index 2f84e988a..d979423b0 100644 --- a/packages/compiler/src/frontend/lowering/require-initialization.ts +++ b/packages/compiler/src/frontend/lowering/require-initialization.ts @@ -119,7 +119,7 @@ export function markRequireEarlyBindings(lowerer: Lowerer): void { const tag = rawTag === "" ? "e." : rawTag.replace(/^%/, ""); const bySymbol = new Map(); for (const { decl } of declared) { - const flagId = `%g.${tag}%required.${decl.getStart(sf)}`; + const flagId = `%g.${tag}%required.${ts.tokenStartOfNode(decl)}`; lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true }); const flag: RequireInitFlag = { flagId, names: new Map() }; for (const id of boundNames(decl.name)) { @@ -148,7 +148,7 @@ export function markRequireEarlyBindings(lowerer: Lowerer): void { if (owner === undefined) continue; if (boundNames(owner.decl.name).includes(id)) continue; // Code after the require runs after it, except inside functions. - if (id.getStart(sf) >= owner.decl.getEnd() && !inDeferredPosition(id)) continue; + if (ts.tokenStartOfNode(id) >= owner.decl.end && !inDeferredPosition(id)) continue; const body = enclosingBody(id); lowerer.requireEarlyReferences.push({ flag: owner.flag, diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts b/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts index d3acaff6b..f332d0f51 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts @@ -287,7 +287,7 @@ function assertsSlot(node: ts.Node, operands: Operands): boolean { return; } if ((n.flags & ts.NodeFlags.OptionalChain) !== 0) return; - n.forEachChild(visit); + ts.forEachChild(n, visit); }; if (ts.isVariableStatement(node)) { for (const declaration of node.declarationList.declarations.nodes) @@ -323,7 +323,7 @@ function unchanged(nodes: readonly ts.Node[], { array, index }: Operands): boole let hit = false; const walk = (n: ts.Node): void => { if (ts.isIdentifier(n) && roots.includes(n.text)) hit = true; - n.forEachChild(walk); + ts.forEachChild(n, walk); }; walk(e); return hit; @@ -372,7 +372,7 @@ function unchanged(nodes: readonly ts.Node[], { array, index }: Operands): boole ok = false; return; } - n.forEachChild(visit); + ts.forEachChild(n, visit); }; for (const node of nodes) visit(node); return ok; diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts index 2fa8785c9..5ed011ce1 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts @@ -611,7 +611,7 @@ export class ArrayElementStates { if (this.host.markIterationBinding(name, known)) changed = true; return; } - name.forEachChild(mark); + ts.forEachChild(name, mark); }; if (ts.isVariableDeclarationList(initializer)) { for (const declaration of initializer.declarations.nodes) mark(declaration.name); diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts b/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts index 768932ad3..3e4605f94 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts @@ -21,7 +21,7 @@ export function reassignedBindings( else if (ts.isArrayLiteralExpression(e) || ts.isObjectLiteralExpression(e)) { const walk = (n: ts.Node): void => { if (ts.isIdentifier(n)) mark(n); - else n.forEachChild(walk); + else ts.forEachChild(n, walk); }; walk(e); } diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts b/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts index adecfa5a9..dd1dce04d 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts @@ -378,9 +378,9 @@ function usesArguments(fn: ts.FunctionExpression): boolean { if (found) return; if (ts.isIdentifier(node) && node.text === "arguments") found = true; else if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node)) - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - fn.body?.forEachChild(visit); + if (fn.body !== undefined) ts.forEachChild(fn.body, visit); return found; } diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-provenance.ts b/packages/compiler/src/frontend/lowering/runtime-optional-provenance.ts index f150e314b..3363f197d 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-provenance.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-provenance.ts @@ -33,8 +33,8 @@ export class RuntimeOptionalProvenance { function describe(node: ts.Node | undefined): string { if (!node) return "?"; - const sf = node.getSourceFile(); - const start = node.getStart(); + const sf = ts.getSourceFileOfNode(node); + const start = ts.tokenStartOfNode(node); const pos = ts.getLineAndCharacterOfPosition(sf, start); const text = sf.text.slice(start, node.end).replace(/\s+/g, " "); const short = text.length > 60 ? `${text.slice(0, 57)}...` : text; diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-records.test.ts b/packages/compiler/src/frontend/lowering/runtime-optional-records.test.ts index f95694624..50b508a52 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-records.test.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-records.test.ts @@ -21,7 +21,7 @@ function classes(source: string): { const symbolFor = (name: string): ts.Symbol => { let symbol = symbols.get(name); if (!symbol) { - symbol = { name, declarations: [] } as unknown as ts.Symbol; + symbol = { escapedName: name, declarations: [] } as unknown as ts.Symbol; symbols.set(name, symbol); } return symbol; diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-records.ts b/packages/compiler/src/frontend/lowering/runtime-optional-records.ts index bad2523c8..18e136d69 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-records.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-records.ts @@ -351,7 +351,7 @@ export class RecordFlowClasses { private referencesOf(symbol: ts.Symbol): readonly ts.Identifier[] { let list = this.references.get(symbol); if (!list) { - list = (this.names.get(symbol.name) ?? []).filter( + list = (this.names.get(ts.symbolName(symbol)) ?? []).filter( (node) => this.host.symbolOf(node) === symbol, ); this.references.set(symbol, list); @@ -412,7 +412,7 @@ export class RecordFlowClasses { const symbol = ts.isIdentifier(base) ? this.host.symbolOf(base) : null; const target = symbol ? this.host.declarationOf(symbol) : undefined; if (!target || !ts.isClassDeclaration(target)) return true; - const file = target.getSourceFile(); + const file = ts.getSourceFileOfNode(target); if (file.isDeclarationFile || isJsFile(file)) return true; current = target; } @@ -591,7 +591,7 @@ export class RecordFlowClasses { } else { this.markOpen(slot); } - if (this.shorthandNames.has(symbol.name)) this.markOpen(slot); + if (this.shorthandNames.has(ts.symbolName(symbol))) this.markOpen(slot); for (const reference of this.referencesOf(symbol)) { if (declaration && (declaration as { name?: ts.Node }).name === reference) continue; if (!this.use(reference, slot)) this.markOpen(slot); @@ -824,9 +824,9 @@ export function returnExpressions(fn: ts.FunctionLikeDeclaration): ts.Expression } if (ts.isFunctionLike(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node)) return; - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - fn.body?.forEachChild(visit); + if (fn.body !== undefined) ts.forEachChild(fn.body, visit); return out; } @@ -865,9 +865,9 @@ function usesArguments(body: ts.Node): boolean { if (found) return; if (ts.isIdentifier(node) && node.text === "arguments") found = true; else if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node)) - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - body.forEachChild(visit); + ts.forEachChild(body, visit); return found; } diff --git a/packages/compiler/src/frontend/lowering/split-consumers.ts b/packages/compiler/src/frontend/lowering/split-consumers.ts index 9624ac0aa..8195d2792 100644 --- a/packages/compiler/src/frontend/lowering/split-consumers.ts +++ b/packages/compiler/src/frontend/lowering/split-consumers.ts @@ -45,7 +45,7 @@ export function isPrivateSplitConsumer( ) return false; - for (const use of identifiersNamed(lowerer, decl.getSourceFile(), name.text)) { + for (const use of identifiersNamed(lowerer, ts.getSourceFileOfNode(decl), name.text)) { if (use === decl.name || use === name) continue; if (lowerer.resolveValueSymbol(use) === symbol) return false; } diff --git a/packages/compiler/src/frontend/lowering/static-function-names.ts b/packages/compiler/src/frontend/lowering/static-function-names.ts index d86e55a80..8868d6745 100644 --- a/packages/compiler/src/frontend/lowering/static-function-names.ts +++ b/packages/compiler/src/frontend/lowering/static-function-names.ts @@ -21,7 +21,7 @@ export function nameReadParams(lowerer: Lowerer, info: GenericFnInfo): Map(); // JavaScript generics read function names through the checked-value // box, which carries the runtime name already. - if (isJsSourceFile(info.decl.getSourceFile())) { + if (isJsSourceFile(ts.getSourceFileOfNode(info.decl))) { info.nameReadParams = out; return out; } diff --git a/packages/compiler/src/frontend/lowering/static-this.ts b/packages/compiler/src/frontend/lowering/static-this.ts index 004a72375..5c26ef0c9 100644 --- a/packages/compiler/src/frontend/lowering/static-this.ts +++ b/packages/compiler/src/frontend/lowering/static-this.ts @@ -31,10 +31,10 @@ export function rejectStaticThis( const keyword = node.kind === ts.SyntaxKind.ThisKeyword ? "this" : "super"; lowerer.unsupported("SC1090", node, message(keyword)); } - node.forEachChild(visit); + ts.forEachChild(node, visit); }; if (includeRoot) visit(root); - else root.forEachChild(visit); + else ts.forEachChild(root, visit); } export function hasStaticThis(root: ts.Node): boolean { @@ -47,9 +47,9 @@ export function hasStaticThis(root: ts.Node): boolean { ts.isClassExpression(node) ) return; - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - root.forEachChild(visit); + ts.forEachChild(root, visit); return found; } @@ -60,8 +60,8 @@ export function hasLexicalPrivateReference(root: ts.Node): boolean { const visit = (node: ts.Node): void => { if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) return; if (ts.isPrivateIdentifier(node)) found = true; - node.forEachChild(visit); + ts.forEachChild(node, visit); }; - root.forEachChild(visit); + ts.forEachChild(root, visit); return found; } diff --git a/packages/compiler/src/frontend/lowering/static-typeof.ts b/packages/compiler/src/frontend/lowering/static-typeof.ts index ea8da5e1e..54f2fdfa0 100644 --- a/packages/compiler/src/frontend/lowering/static-typeof.ts +++ b/packages/compiler/src/frontend/lowering/static-typeof.ts @@ -118,7 +118,7 @@ export function declaresHoistedBinding(stmt: ts.Statement): boolean { found = true; return; } - n.forEachChild(visit); + ts.forEachChild(n, visit); }; visit(stmt); return found; diff --git a/packages/compiler/src/frontend/lowering/string-index-bounds.ts b/packages/compiler/src/frontend/lowering/string-index-bounds.ts index acb41514f..6e422b231 100644 --- a/packages/compiler/src/frontend/lowering/string-index-bounds.ts +++ b/packages/compiler/src/frontend/lowering/string-index-bounds.ts @@ -73,7 +73,7 @@ function isBoundary(node: ts.Node): boolean { function containerOf(node: ts.Node): ts.Node { let n = node.parent; while (n && !isBoundary(n)) n = n.parent; - return n ?? node.getSourceFile(); + return n ?? ts.getSourceFileOfNode(node); } function isLoop(node: ts.Node): boolean { @@ -130,7 +130,7 @@ function writeOf(id: ts.Identifier): Write | null { const k = p.operatorToken.kind; if (p.left !== n || k < ts.SyntaxKind.FirstAssignment || k > ts.SyntaxKind.LastAssignment) return null; - const at = id.getStart(); + const at = ts.tokenStartOfNode(id); if (destructured) return { at, step: null, value: null }; if (k === ts.SyntaxKind.EqualsToken) return { at, step: null, value: p.right }; const amount = integerLiteral(p.right); @@ -146,15 +146,15 @@ function writeOf(id: ts.Identifier): Write | null { (p.operator !== ts.SyntaxKind.PlusPlusToken && p.operator !== ts.SyntaxKind.MinusMinusToken) ) return null; - if (destructured) return { at: id.getStart(), step: null, value: null }; + if (destructured) return { at: ts.tokenStartOfNode(id), step: null, value: null }; return { - at: id.getStart(), + at: ts.tokenStartOfNode(id), step: p.operator === ts.SyntaxKind.PlusPlusToken ? 1 : -1, value: null, }; } if (ts.isForOfStatement(p) || ts.isForInStatement(p)) - return p.initializer === n ? { at: id.getStart(), step: null, value: null } : null; + return p.initializer === n ? { at: ts.tokenStartOfNode(id), step: null, value: null } : null; if (ts.isParenthesizedExpression(p)) { n = p; continue; @@ -199,7 +199,7 @@ export class StringIndexBounds { private prove(read: ts.ElementAccessExpression): boolean { // JavaScript declarations are not checked against callers. - if (read.questionDotToken || isJsSourceFile(read.getSourceFile())) return false; + if (read.questionDotToken || isJsSourceFile(ts.getSourceFileOfNode(read))) return false; const textNode = read.expression; const indexNode = read.argumentExpression; if (!ts.isIdentifier(textNode) || !ts.isIdentifier(indexNode)) return false; @@ -268,7 +268,7 @@ export class StringIndexBounds { if (!(list.flags & ts.NodeFlags.Const)) return null; } else if (declaration.initializer) { writes.push({ - at: declaration.name.getStart(), + at: ts.tokenStartOfNode(declaration.name), step: null, value: declaration.initializer, }); @@ -280,7 +280,7 @@ export class StringIndexBounds { const container = containerOf(declaration); let escapes = false; let assigned = 0; - for (const id of this.namesIn(container).get(symbol.name) ?? []) { + for (const id of this.namesIn(container).get(ts.symbolName(symbol)) ?? []) { if (id === nameNode) continue; const write = writeOf(id); if (!write) continue; diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 37a1c625d..b418ebc20 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -3318,7 +3318,7 @@ export function isChildSurfaceMember( const decl = sym ? sym.declarations[0] : undefined; const iface = decl?.parent; if (!decl || !iface || !ts.isInterfaceDeclaration(iface)) return false; - if (lowerer.isStdlibFile(decl.getSourceFile())) return false; + if (lowerer.isStdlibFile(ts.getSourceFileOfNode(decl))) return false; return lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(iface.name))?.kind === "child"; } @@ -3328,7 +3328,9 @@ export function isChildSurfaceMember( * with lib interfaces, so a member can carry declarations from both. A * user's own declaration is in neither, so shadowing never matches. */ export function isStdlibSymbol(lowerer: Lowerer, symbol: ts.Symbol | undefined): boolean { - return !!symbol && symbol.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile())); + return ( + !!symbol && symbol.declarations.some((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) + ); } /** The canonical stdlib-global name `expr` denotes, or null. Three @@ -3394,14 +3396,14 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin if ( decl !== undefined && (ts.isVariableDeclaration(decl) || ts.isBindingElement(decl)) && - isJsSourceFile(decl.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(decl)) ) { const imported = lowerer.builtinImportOf(expr); if (imported?.module === "buffer" && imported.member === "Buffer") return "Buffer"; } return null; } - return resolved.name === "global" ? "globalThis" : resolved.name; + return ts.symbolName(resolved) === "global" ? "globalThis" : ts.symbolName(resolved); } if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) { if (stdlibGlobalNameOf(lowerer, expr.expression) !== "globalThis") return null; @@ -3414,7 +3416,7 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin ? lowerer.checker.getSymbolAtLocation(expr.name) : lowerer.checker.getPropertyOfType(lowerer.typeOf(selected), expr.name.text); if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null; - return symbol.name === "global" ? "globalThis" : symbol.name; + return ts.symbolName(symbol) === "global" ? "globalThis" : ts.symbolName(symbol); } return null; } @@ -3548,7 +3550,7 @@ export function nodeTypesOnlySymbol(lowerer: Lowerer, sym: ts.Symbol | null | un if (!decls || decls.length === 0) return false; let viaNode = false; for (const d of decls) { - const sf = d.getSourceFile(); + const sf = ts.getSourceFileOfNode(d); if (sf.isDeclarationFile && isNodeTypesPath(sf.fileName)) viaNode = true; else if (lowerer.isStdlibFile(sf)) return false; } diff --git a/packages/compiler/src/frontend/lowering/symbol-fields.ts b/packages/compiler/src/frontend/lowering/symbol-fields.ts index 0791fc6cd..56718615e 100644 --- a/packages/compiler/src/frontend/lowering/symbol-fields.ts +++ b/packages/compiler/src/frontend/lowering/symbol-fields.ts @@ -118,13 +118,13 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS const init = decl.initializer; if (init && (ts.isIdentifier(init) || ts.isPropertyAccessExpression(init))) { const target = classSymbolKeyOf(lowerer, init); - if (!target || bindingWritten(lowerer, sym, decl.getSourceFile())) return null; + if (!target || bindingWritten(lowerer, sym, ts.getSourceFileOfNode(decl))) return null; const preceding = list.declarations.nodes.slice(0, list.declarations.nodes.indexOf(decl)); if ( bindingEarlyUse7( lowerer.program, - decl.getSourceFile(), - decl.getSourceFile().statements.nodes.indexOf(list.parent), + ts.getSourceFileOfNode(decl), + ts.getSourceFileOfNode(decl).statements.nodes.indexOf(list.parent), decl, preceding, ) !== null @@ -139,7 +139,7 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS const { registered, description } = call; // Ordinary Symbol() is a fresh identity per evaluation, so only the // module-scope declaration above can name one fixed layout slot. - const sf = decl.getSourceFile(); + const sf = ts.getSourceFileOfNode(decl); if (bindingWritten(lowerer, sym, sf)) return null; const preceding = list.declarations.nodes.slice(0, list.declarations.nodes.indexOf(decl)); // Creating a function value does not execute its body. The general @@ -268,7 +268,7 @@ function cjsTableSymbolKeyOf( ) return null; const stmt = assign.parent; - const sf = stmt.getSourceFile(); + const sf = ts.getSourceFileOfNode(stmt); if (cjsExportDiscardReason(stmt) !== null) return null; const call = literalSymbolCallOf(lowerer, decl.initializer); if (!call) return null; @@ -330,7 +330,7 @@ function tablePropertyWritten( if (node.name.text !== name) return false; const recv = node.expression; if ( - node.getSourceFile() === home && + ts.getSourceFileOfNode(node) === home && ((ts.isIdentifier(recv) && recv.text === "exports") || isModuleExportsAccess(recv)) ) return true; @@ -381,7 +381,7 @@ function bindingWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): bo // Assignment to obj[key] changes the property, never the key's // binding. The same applies to these leaves in destructuring. if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) return "skip"; - if (!ts.isIdentifier(node) || node.text !== sym.name) return; + if (!ts.isIdentifier(node) || node.text !== ts.symbolName(sym)) return; const targetSym = ts.isShorthandPropertyAssignment(node.parent) ? lowerer.checker.getShorthandAssignmentValueSymbol(node.parent) : lowerer.checker.getSymbolAtLocation(node); diff --git a/packages/compiler/src/frontend/lowering/typed-arguments.ts b/packages/compiler/src/frontend/lowering/typed-arguments.ts index b52a3e059..2fbd83482 100644 --- a/packages/compiler/src/frontend/lowering/typed-arguments.ts +++ b/packages/compiler/src/frontend/lowering/typed-arguments.ts @@ -71,7 +71,7 @@ export function typedArgumentsCandidate(node: ts.Node): node is ts.FunctionExpre return ( ts.isFunctionExpression(node) && node.asteriskToken === undefined && - !isJsSourceFile(node.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(node)) && node.parameters.nodes.every(isThisParameter) && bodyReadsArguments(node) ); @@ -243,7 +243,7 @@ export function onlyForwarded( lowerer.checker.getNonNullableType(lowerer.typeOf(p.parent.expression)), ); const decl = sigs.length === 1 ? sigs[0]!.declaration : undefined; - if (decl && !decl.getSourceFile().isDeclarationFile) return undefined; + if (decl && !ts.getSourceFileOfNode(decl).isDeclarationFile) return undefined; } if ( ts.isVariableDeclaration(p) && @@ -317,7 +317,7 @@ export function forwardedTupleType(lowerer: Lowerer, node: ts.Expression): IrTyp ts.isParameter(decl) && decl.dotDotDotToken !== undefined && ts.isIdentifier(decl.name) && - !isJsSourceFile(decl.getSourceFile()) && + !isJsSourceFile(ts.getSourceFileOfNode(decl)) && // Checker-only probe first: an ordinary array rest never maps here. restTupleElementTypes(lowerer.typeOf(operand), lowerer.typeCtx) !== null ) { diff --git a/packages/compiler/src/frontend/lowering/var-hoisting.ts b/packages/compiler/src/frontend/lowering/var-hoisting.ts index c445f1751..6276ab671 100644 --- a/packages/compiler/src/frontend/lowering/var-hoisting.ts +++ b/packages/compiler/src/frontend/lowering/var-hoisting.ts @@ -97,7 +97,7 @@ function hoistingScopeOf(node: ts.Node): Scope { ) return p; } - return node.getSourceFile(); + return ts.getSourceFileOfNode(node); } function statementsOf(scope: Scope): readonly ts.Statement[] { @@ -221,7 +221,7 @@ class ScopeAnalysis { * the statement at index `settle`. `read`: `ref` reads the binding * itself, so tsc vouches for one in the scope's own code. */ settles(scope: Scope, settle: number, ref: ts.Identifier, read = true): boolean { - if (ref.getSourceFile() !== this.sf) return true; + if (ts.getSourceFileOfNode(ref) !== this.sf) return true; const statements = statementsOf(scope); let deferred: ts.Node | null = null; let top: ts.Node | null = null; diff --git a/packages/compiler/src/frontend/module-source-candidates.test.ts b/packages/compiler/src/frontend/module-source-candidates.test.ts index 4ff20decc..8fccef000 100644 --- a/packages/compiler/src/frontend/module-source-candidates.test.ts +++ b/packages/compiler/src/frontend/module-source-candidates.test.ts @@ -6,6 +6,7 @@ import * as ast from "./ts7/ast.js"; import { Ts7Api } from "./ts7/rpc-api.js"; import { Ts7Host } from "./ts7/program-host.js"; import { moduleSourceCandidates } from "./module-source-candidates.js"; +import { getTextOfNode } from "./ts7/node-functions.js"; test("module candidates preserve nested call order and belong to one program snapshot", () => { const directory = mkdtempSync(join(tmpdir(), "scriptc-module-candidates-")); @@ -24,7 +25,7 @@ test("module candidates preserve nested call order and belong to one program sna const source = first.getSourceFile(entry)!; const candidates = moduleSourceCandidates(first, source); expect(candidates.specifiers).toEqual(["first", "second", "third", "fourth"]); - expect(candidates.calls.map((call) => call.expression.getText())).toEqual([ + expect(candidates.calls.map((call) => getTextOfNode(call.expression))).toEqual([ "outer", "require", "import", diff --git a/packages/compiler/src/frontend/npm-static-builtin-aliases.ts b/packages/compiler/src/frontend/npm-static-builtin-aliases.ts index 7be99db85..f1e3d53d3 100644 --- a/packages/compiler/src/frontend/npm-static-builtin-aliases.ts +++ b/packages/compiler/src/frontend/npm-static-builtin-aliases.ts @@ -15,6 +15,7 @@ * of the carried names read the builtin module's members, value uses read * the class itself, and a mixture keeps the package's own JavaScript. */ import * as ts from "./ts7/syntax.js"; +import { tokenStartOfNode } from "./ts7/node-functions.js"; import { aliasReplacement, applyEdits, @@ -59,7 +60,7 @@ export function rewriteBuiltinAliasRequires( const alias = aliasFor(spec); if (alias === null) return undefined; const text = aliasReplacement(n, alias); - if (text !== null) edits.push({ start: n.getStart(sf), end: n.getEnd(), text }); + if (text !== null) edits.push({ start: tokenStartOfNode(n), end: n.end, text }); return undefined; }); if (edits.length === 0) return null; diff --git a/packages/compiler/src/frontend/npm-static-bundled-chunks.ts b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts index 253f21556..37a74f2be 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-chunks.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts @@ -24,6 +24,7 @@ import * as ts from "./ts7/syntax.js"; import { helperTokens } from "./helper-tokens.js"; import { canonicalBuiltinModule } from "./builtin-modules.js"; +import { getTextOfNode, tokenStartOfNode } from "./ts7/node-functions.js"; import { bundledShimForwarder, packageShimForBundledFile } from "./package-shims.js"; import { builtinAliasOf, @@ -372,16 +373,16 @@ function helperForm( const ret = stmts[0]; if (stmts.length !== 1 || ret === undefined || !ts.isReturnStatement(ret) || !ret.expression) return null; - const actual = helperTokens(ret.expression.getText(sf)); + const actual = helperTokens(getTextOfNode(ret.expression)); if (actual === null) return null; // The own-key reader: esbuild's alias or the 0.11 spelling. - const keysRef = /\[\s*([A-Za-z_$][\w$]*(?:\.keys)?)\s*\(/.exec(ret.expression.getText(sf)); + const keysRef = /\[\s*([A-Za-z_$][\w$]*(?:\.keys)?)\s*\(/.exec(getTextOfNode(ret.expression)); const keys = keysRef?.[1]; if (keys === undefined || (keys !== "Object.keys" && !ownNames(keys))) return null; return actual === template(KEYED_RETURN, cb, mod, keys) ? "keyed" : null; } if (ts.isArrowFunction(body) && body.parameters.nodes.length === 0 && !ts.isBlock(body.body)) { - const actual = helperTokens(body.body.getText(sf)); + const actual = helperTokens(getTextOfNode(body.body)); return actual !== null && actual === template(ARROW_BODY, cb, mod, "") ? "arrow" : null; } return null; @@ -547,7 +548,7 @@ export function splitBundledChunks( const init = t.node.initializer; return ( init !== undefined && - helperTokens(init.getText(sf)) === helperTokens("Object.getOwnPropertyNames") + helperTokens(getTextOfNode(init)) === helperTokens("Object.getOwnPropertyNames") ); }; if (helperNames.length === 0) { @@ -658,8 +659,8 @@ export function splitBundledChunks( const forwarded = forwarderSpec(chunk) ?? (shimOfChunk(chunk) !== null ? shimSpec(chunk) : null); callEdits.push({ - start: call.getStart(sf), - end: call.getEnd(), + start: tokenStartOfNode(call), + end: call.end, text: aliased ?? `require(${JSON.stringify(forwarded ?? chunk.spec)})`, }); } @@ -687,7 +688,7 @@ export function splitBundledChunks( for (const chunk of chunks.values()) { if (!unaliased.has(chunk.name) && builtinAliasOfChunk(chunk) !== null) { // A chunk of a package that IS a builtin on Node 24 is that builtin. - bundleEdits.push({ start: chunk.stmt.getStart(sf), end: chunk.stmt.getEnd(), text: "" }); + bundleEdits.push({ start: tokenStartOfNode(chunk.stmt), end: chunk.stmt.end, text: "" }); continue; } const shim = shimOfChunk(chunk); @@ -699,12 +700,12 @@ export function splitBundledChunks( path: shimDeclPath(chunk), source: bundledShimForwarder(shimDeclPath(chunk), shim), }); - bundleEdits.push({ start: chunk.stmt.getStart(sf), end: chunk.stmt.getEnd(), text: "" }); + bundleEdits.push({ start: tokenStartOfNode(chunk.stmt), end: chunk.stmt.end, text: "" }); continue; } if (forwarderSpec(chunk) !== null) { // A chunk that only re-exports a builtin module is that module. - bundleEdits.push({ start: chunk.stmt.getStart(sf), end: chunk.stmt.getEnd(), text: "" }); + bundleEdits.push({ start: tokenStartOfNode(chunk.stmt), end: chunk.stmt.end, text: "" }); continue; } const result = chunkSource( @@ -720,7 +721,7 @@ export function splitBundledChunks( ); if (typeof result !== "string") return result; out.push({ path: chunk.path, source: result }); - bundleEdits.push({ start: chunk.stmt.getStart(sf), end: chunk.stmt.getEnd(), text: "" }); + bundleEdits.push({ start: tokenStartOfNode(chunk.stmt), end: chunk.stmt.end, text: "" }); } return { text: applyEdits(source, 0, source.length, bundleEdits), chunks: out }; } @@ -820,7 +821,11 @@ export function aliasReplacement(call: ts.CallExpression, alias: BuiltinAlias): const text = aliasUseReplacement(call, alias); if (text === null) return null; const root = /^[A-Za-z_$][\w$]*/.exec(text)?.[0]; - if (root !== undefined && root !== "require" && countDeclarations(call.getSourceFile(), root) > 0) + if ( + root !== undefined && + root !== "require" && + countDeclarations(ts.getSourceFileOfNode(call), root) > 0 + ) return null; return text; } @@ -1018,8 +1023,8 @@ function chunkSource( const p = r.parent; edits.push( ts.isShorthandPropertyAssignment(p) - ? { start: r.getStart(sf), end: r.getEnd(), text: `${name}: ${global}` } - : { start: r.getStart(sf), end: r.getEnd(), text: global }, + ? { start: tokenStartOfNode(r), end: r.end, text: `${name}: ${global}` } + : { start: tokenStartOfNode(r), end: r.end, text: global }, ); } continue; @@ -1063,15 +1068,17 @@ function chunkSource( } // Declaration order of the bundle: a helper reads only earlier ones. const ordered = [...copied].sort( - (a, b) => top.get(a)!.node.getStart(sf) - top.get(b)!.node.getStart(sf), + (a, b) => tokenStartOfNode(top.get(a)!.node) - tokenStartOfNode(top.get(b)!.node), ); for (const name of ordered) { const node = top.get(name)!.node; - header.push(ts.isFunctionDeclaration(node) ? node.getText(sf) : `var ${node.getText(sf)};`); + header.push( + ts.isFunctionDeclaration(node) ? getTextOfNode(node) : `var ${getTextOfNode(node)};`, + ); } const bodyStrict = strictPrologue(body.statements.nodes); - const text = applyEdits(sf.text, body.getStart(sf) + 1, body.getEnd() - 1, edits); + const text = applyEdits(sf.text, tokenStartOfNode(body) + 1, body.end - 1, edits); const prologue = bundleStrict || bodyStrict ? '"use strict";\n' : ""; return prologue + (header.length > 0 ? header.join("\n") + "\n" : "") + text + "\n"; } diff --git a/packages/compiler/src/frontend/npm-static-bundled-cjs.ts b/packages/compiler/src/frontend/npm-static-bundled-cjs.ts index 89cd0fda1..be43284a6 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-cjs.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-cjs.ts @@ -1,5 +1,6 @@ import * as ts from "./ts7/syntax.js"; import { helperTokens } from "./helper-tokens.js"; +import { getTextOfNode, tokenStartOfNode } from "./ts7/node-functions.js"; // Bun's cached node-mode interop helper. Match the complete token stream, // including its dependencies: helper names alone are not a semantic contract. @@ -117,14 +118,14 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) } for (const [name, shape] of helperShapes) { const actual = declarations.get(name); - if (!actual || helperTokens(actual.stmt.getText(sf)) !== shape) return null; + if (!actual || helperTokens(getTextOfNode(actual.stmt)) !== shape) return null; } // These initializers must execute before any factory or namespace. The // original bundle would throw if a var helper were still undefined. let helperEnd = 0; for (const name of helperShapes.keys()) { const stmt = declarations.get(name)!.stmt; - if (stmt.getStart(sf) < helperEnd) return null; + if (tokenStartOfNode(stmt) < helperEnd) return null; helperEnd = stmt.end; } const edits: Edit[] = []; @@ -140,7 +141,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) const call = decl.initializer; if (!ts.isIdentifier(call.expression) || call.expression.text !== "__commonJS") continue; if ( - stmt.getStart(sf) < helperEnd || + tokenStartOfNode(stmt) < helperEnd || (ts.canHaveModifiers(stmt) && ts.getModifiers(stmt)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) ) @@ -193,7 +194,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) if (unsafe) return null; // The comma expression prevents inferred naming of an anonymous export. edits.push( - { start: call.getStart(sf), end: value.getStart(sf), text: "(0," }, + { start: tokenStartOfNode(call), end: tokenStartOfNode(value), text: "(0," }, { start: value.end, end: call.end, text: ")" }, ); factories.set(name, stmt); @@ -212,7 +213,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) const call = decl.initializer; if (!ts.isIdentifier(call.expression) || call.expression.text !== "__toESM") continue; if ( - stmt.getStart(sf) < helperEnd || + tokenStartOfNode(stmt) < helperEnd || (ts.canHaveModifiers(stmt) && ts.getModifiers(stmt)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) ) @@ -230,9 +231,9 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) ) return null; const factory = factories.get(requireCall.expression.text); - if (!factory || factory.end > stmt.getStart(sf)) return null; + if (!factory || factory.end > tokenStartOfNode(stmt)) return null; edits.push({ - start: call.getStart(sf), + start: tokenStartOfNode(call), end: call.end, text: `{default:${requireCall.expression.text}}`, }); @@ -286,7 +287,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) // Remove only helpers that became dead. Other bundle features may share // __defProp, so a recognized declaration alone does not justify erasure. const dead = (n: ts.Node): boolean => - edits.some((e) => n.getStart(sf) >= e.start && n.end <= e.end); + edits.some((e) => tokenStartOfNode(n) >= e.start && n.end <= e.end); const removed = new Set(); for (let changed = true; changed;) { changed = false; @@ -304,7 +305,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile) live = true; }); if (!live) { - edits.push({ start: stmt.getStart(sf), end: stmt.end, text: "" }); + edits.push({ start: tokenStartOfNode(stmt), end: stmt.end, text: "" }); removed.add(name); changed = true; } diff --git a/packages/compiler/src/frontend/npm-static-bundled-esm.ts b/packages/compiler/src/frontend/npm-static-bundled-esm.ts index d74b41a1d..83cb1c1e1 100644 --- a/packages/compiler/src/frontend/npm-static-bundled-esm.ts +++ b/packages/compiler/src/frontend/npm-static-bundled-esm.ts @@ -42,6 +42,7 @@ import { unparen, type Edit, } from "./npm-static-bundled-chunks.js"; +import { getTextOfNode, tokenStartOfNode } from "./ts7/node-functions.js"; export interface EsmInitHost { requireTargetEsModuleStamped(fromFile: string, specifier: string): boolean; @@ -107,13 +108,13 @@ function esmHelperForm( const stmts = body.body.statements.nodes; const ret = stmts[0]; if (stmts.length !== 1 || !ret || !ts.isReturnStatement(ret) || !ret.expression) return null; - const text = ret.expression.getText(sf); + const text = getTextOfNode(ret.expression); const keys = /\[\s*([A-Za-z_$][\w$]*(?:\.keys)?)\s*\(/.exec(text)?.[1]; if (keys === undefined || (keys !== "Object.keys" && !ownNames(keys))) return null; return helperTokens(text) === template(KEYED_RETURN, f, r, keys) ? "keyed" : null; } if (ts.isArrowFunction(body) && body.parameters.nodes.length === 0 && !ts.isBlock(body.body)) { - const actual = helperTokens(body.body.getText(sf)); + const actual = helperTokens(getTextOfNode(body.body)); return actual !== null && actual === template(ARROW_BODY, f, r, "") ? "arrow" : null; } return null; @@ -132,7 +133,7 @@ function removeDeclarators(sf: ts.SourceFile, dead: ReadonlySet dead.has(d)); if (deadAt.every((x) => x)) { - edits.push({ start: stmt.getStart(sf), end: stmt.getEnd(), text: "" }); + edits.push({ start: tokenStartOfNode(stmt), end: stmt.end, text: "" }); continue; } const lastLive = deadAt.lastIndexOf(false); @@ -140,8 +141,8 @@ function removeDeclarators(sf: ts.SourceFile, dead: ReadonlySet(); @@ -441,7 +442,7 @@ export function lowerBundledEsmInits( } const spec = bareRequireSpecOf(rhs); if (spec === null) continue; - const req = rhs.getText(sf); + const req = getTextOfNode(rhs); const defaultIsModule = interop && (isNodeMode || !host.requireTargetEsModuleStamped(filePath, spec)); // Every other reference must be a plain member read. @@ -460,18 +461,18 @@ export function lowerBundledEsmInits( break; } if (defaultIsModule && access.name.text === "default") - uses.push({ start: access.getStart(sf), end: access.getEnd(), text: `(${req})` }); + uses.push({ start: tokenStartOfNode(access), end: access.end, text: `(${req})` }); // `new import_x.C()` must stay `new (require("x")).C()`: a bare // `new require("x").C()` constructs the require call itself. else uses.push({ - start: r.getStart(sf), - end: r.getEnd(), + start: tokenStartOfNode(r), + end: r.end, text: inNewCallee(r) ? `(${req})` : req, }); } if (!ok) continue; - edits.push({ start: el.getStart(sf), end: el.getEnd(), text: req }, ...uses); + edits.push({ start: tokenStartOfNode(el), end: el.end, text: req }, ...uses); deadDeclarators.add(decl); interopSpecs.set(name, spec); } @@ -531,7 +532,7 @@ export function lowerBundledEsmInits( return false; }; const getterText = (value: ts.Expression): string => - applyEdits(source, value.getStart(sf), value.getEnd(), edits); + applyEdits(source, tokenStartOfNode(value), value.end, edits); /** A call through the namespace (`ns.fn()`, `L().fn()`) passes the object * the read came from as `this`; the lowered object differs from Node's * module object (`__esModule`, property attributes), so it must stay @@ -659,23 +660,23 @@ export function lowerBundledEsmInits( // call through it (`L().fn()`) loses its receiver. if (calledWithReceiver(access as ts.Expression) && !receiverHidden(entries, member)) return loaderWhy; - const init = initCall === null ? "" : `${initCall.getText(sf)}, `; + const init = initCall === null ? "" : `${getTextOfNode(initCall)}, `; edits.push({ - start: access.getStart(sf), - end: access.getEnd(), + start: tokenStartOfNode(access), + end: access.end, text: `(${init}${getterText(value)})`, }); } deadDeclarators.add(d); deadDeclarators.add(memoDecl); - loaderSpans.push({ start: d.getStart(sf), end: d.getEnd() }); + loaderSpans.push({ start: tokenStartOfNode(d), end: d.end }); lowered.add(target); } for (const r of allRefs) { const table = tables.get(r.text); if (table === undefined) continue; - if (loaderSpans.some((span) => r.getStart(sf) >= span.start && r.getEnd() <= span.end)) + if (loaderSpans.some((span) => tokenStartOfNode(r) >= span.start && r.end <= span.end)) continue; const p = r.parent; // The table's own declaration and __export call are handled below. @@ -740,7 +741,7 @@ export function lowerBundledEsmInits( const inner = unparen(ns); const init = ts.isBinaryExpression(inner) && inner.operatorToken.kind === ts.SyntaxKind.CommaToken - ? `${unparen(inner.left).getText(sf)}; ` + ? `${getTextOfNode(unparen(inner.left))}; ` : ""; if (shadowed(holder, table.entries)) return { degrade: `its shipped JS ${why}` }; const entries = table.entries @@ -748,8 +749,8 @@ export function lowerBundledEsmInits( .map(([n, value]) => `${JSON.stringify(n)}: () => ${getterText(value)}`) .join(", "); edits.push({ - start: holder.parent.getStart(sf), - end: holder.parent.getEnd(), + start: tokenStartOfNode(holder.parent), + end: holder.parent.end, text: `${init}__export(${mainTarget}, { ${entries} });`, }); lowered.add(r.text); @@ -802,7 +803,7 @@ export function lowerBundledEsmInits( `get ${/^[A-Za-z_$][\w$]*$/.test(n) ? n : JSON.stringify(n)}() { return ${getterText(value)}; }`, ) .join(", "); - edits.push({ start: p.getStart(sf), end: p.getEnd(), text: `{ ${getters} }` }); + edits.push({ start: tokenStartOfNode(p), end: p.end, text: `{ ${getters} }` }); lowered.add(r.text); } // A table nothing reads (its module is never required) goes too: its @@ -812,7 +813,7 @@ export function lowerBundledEsmInits( const live = (refsByName.get(target) ?? []).some((x) => { const p = x.parent; if (p !== undefined && ts.isVariableDeclaration(p) && p.name === x) return false; - return !(x.getStart(sf) >= table.stmt.getStart(sf) && x.getEnd() <= table.stmt.getEnd()); + return !(tokenStartOfNode(x) >= tokenStartOfNode(table.stmt) && x.end <= table.stmt.end); }); if (!live) lowered.add(target); } @@ -828,7 +829,7 @@ export function lowerBundledEsmInits( writes.has(target) ) return { degrade: ESM_SHAPE_DEGRADE }; - edits.push({ start: table.stmt.getStart(sf), end: table.stmt.getEnd(), text: "" }); + edits.push({ start: tokenStartOfNode(table.stmt), end: table.stmt.end, text: "" }); deadDeclarators.add(decl); } // Every removed declarator in one pass: declarators of one statement @@ -841,13 +842,13 @@ export function lowerBundledEsmInits( const { call, body } = init; edits.push( { - start: call.getStart(sf), - end: body.getStart(sf) + 1, + start: tokenStartOfNode(call), + end: tokenStartOfNode(body) + 1, text: `(() => { let __scriptc_done = false; function __scriptc_init() {`, }, { - start: body.getEnd() - 1, - end: call.getEnd(), + start: body.end - 1, + end: call.end, // A body split around hoisted classes ends in its last tail function. text: hoisted.has(init.name) ? "}" : `} ${RUN_ONCE_TAIL}`, }, @@ -1016,11 +1017,13 @@ function hoistInitClasses( (r) => r === target || // The class's own methods name the class itself. - (r.getStart(sf) >= cls.getStart(sf) && r.getEnd() <= cls.getEnd()) || + (tokenStartOfNode(r) >= tokenStartOfNode(cls) && r.end <= cls.end) || inDeadAnnotation(r) || (insideFunction(r) && // The initializer's own earlier code reads the unassigned binding. - !(r.getStart(sf) >= init.body.getStart(sf) && r.getStart(sf) < el.getEnd())), + !( + tokenStartOfNode(r) >= tokenStartOfNode(init.body) && tokenStartOfNode(r) < el.end + )), ) ) continue; @@ -1031,10 +1034,15 @@ function hoistInitClasses( // the neighboring pieces. // After a hoisted neighbor the separating comma is already gone. const fresh = i === 0 || hoistedEls.has(els[i - 1]!); - const from = i === 0 ? stmt.getStart(sf) : fresh ? el.getStart(sf) : els[i - 1]!.getEnd(); + const from = + i === 0 ? tokenStartOfNode(stmt) : fresh ? tokenStartOfNode(el) : els[i - 1]!.end; const sep = fresh ? "" : ";"; hoistedEls.add(el); - edits.push({ start: from, end: cls.getStart(sf), text: `${sep} ${rest}(); ${close} ` }); + edits.push({ + start: from, + end: tokenStartOfNode(cls), + text: `${sep} ${rest}(); ${close} `, + }); const next = els[i + 1]; // The class's own closing brace carries the next piece's opener: an // empty insertion at its end would share its position with the @@ -1042,18 +1050,18 @@ function hoistInitClasses( edits.push( next === undefined ? { - start: cls.getEnd() - 1, - end: cls.getEnd(), + start: cls.end - 1, + end: cls.end, text: `} function ${rest}() { ${strict}`, } : { - start: cls.getEnd(), - end: next.getStart(sf), + start: cls.end, + end: tokenStartOfNode(next), text: ` function ${rest}() { ${strict}`, }, ); if (cls.name === undefined) { - const keyword = cls.getStart(sf); + const keyword = tokenStartOfNode(cls); edits.push({ start: keyword, end: keyword + "class".length, text: `class ${name}` }); } declarators.push(decl); diff --git a/packages/compiler/src/frontend/npm-static-cjs-exports-alias.ts b/packages/compiler/src/frontend/npm-static-cjs-exports-alias.ts index dfe45b862..bf753be8c 100644 --- a/packages/compiler/src/frontend/npm-static-cjs-exports-alias.ts +++ b/packages/compiler/src/frontend/npm-static-cjs-exports-alias.ts @@ -29,6 +29,7 @@ import { unparen, type Edit, } from "./npm-static-bundled-chunks.js"; +import { tokenStartOfNode } from "./ts7/node-functions.js"; function isModuleExports(e: ts.Expression): e is ts.PropertyAccessExpression { const x = unparen(e); @@ -132,7 +133,7 @@ export function normalizeCjsFunctionExports(sf: ts.SourceFile): string | null { continue; } // Without the rebinding, `exports` keeps the replaced object. - if (!whole.rebinds || r.getStart(sf) < whole.stmt.getEnd()) { + if (!whole.rebinds || tokenStartOfNode(r) < whole.stmt.end) { unsafe = true; continue; } @@ -140,8 +141,8 @@ export function normalizeCjsFunctionExports(sf: ts.SourceFile): string | null { const shorthand = r.parent !== undefined && ts.isShorthandPropertyAssignment(r.parent) && r.parent.name === r; edits.push({ - start: r.getStart(sf), - end: r.getEnd(), + start: tokenStartOfNode(r), + end: r.end, text: shorthand ? `exports: ${fnName}` : fnName, }); continue; @@ -168,8 +169,8 @@ export function normalizeCjsFunctionExports(sf: ts.SourceFile): string | null { (ts.isPropertyAccessExpression(outer) || ts.isElementAccessExpression(outer)) && outer.expression === access ) { - if (access.getStart(sf) < whole.stmt.getEnd() && !insideFunction(access)) unsafe = true; - edits.push({ start: access.getStart(sf), end: access.getEnd(), text: fnName }); + if (tokenStartOfNode(access) < whole.stmt.end && !insideFunction(access)) unsafe = true; + edits.push({ start: tokenStartOfNode(access), end: access.end, text: fnName }); } } } @@ -177,7 +178,7 @@ export function normalizeCjsFunctionExports(sf: ts.SourceFile): string | null { if (whole.rebinds) { // `exports = module.exports = W` → `module.exports = W`. const e = unparen(whole.stmt.expression) as ts.BinaryExpression; - edits.push({ start: e.getStart(sf), end: unparen(e.right).getStart(sf), text: "" }); + edits.push({ start: tokenStartOfNode(e), end: tokenStartOfNode(unparen(e.right)), text: "" }); } if (edits.length === 0) return null; return applyEdits(source, 0, source.length, edits); diff --git a/packages/compiler/src/frontend/npm-static-cjs-exports-var.ts b/packages/compiler/src/frontend/npm-static-cjs-exports-var.ts index 1af4ea1ed..1e7489595 100644 --- a/packages/compiler/src/frontend/npm-static-cjs-exports-var.ts +++ b/packages/compiler/src/frontend/npm-static-cjs-exports-var.ts @@ -17,6 +17,7 @@ * never reassigned. Other shapes answer null and keep their existing * treatment. */ import * as ts from "./ts7/syntax.js"; +import { tokenStartOfNode } from "./ts7/node-functions.js"; function unparen(e: ts.Expression): ts.Expression { let x = e; @@ -219,14 +220,14 @@ export function normalizeCjsExportsVar(sf: ts.SourceFile): string | null { (ts.isPropertyAccessExpression(p) || ts.isElementAccessExpression(p)) && p.expression === n ) { - edits.push({ start: n.getStart(sf), end: n.getEnd(), text: "exports" }); + edits.push({ start: tokenStartOfNode(n), end: n.end, text: "exports" }); return undefined; } ok = false; /* used as a value, assigned, or passed along */ return undefined; }); if (!ok || edits.length === 0) return null; - edits.push({ start: alias.stmt.getStart(sf), end: alias.stmt.getEnd(), text: "" }); + edits.push({ start: tokenStartOfNode(alias.stmt), end: alias.stmt.end, text: "" }); edits.sort((a, b) => a.start - b.start); let out = ""; let at = 0; diff --git a/packages/compiler/src/frontend/npm-static-declaration-syntax.ts b/packages/compiler/src/frontend/npm-static-declaration-syntax.ts index 4a84dcfeb..0d713dcff 100644 --- a/packages/compiler/src/frontend/npm-static-declaration-syntax.ts +++ b/packages/compiler/src/frontend/npm-static-declaration-syntax.ts @@ -10,6 +10,7 @@ * too; inferred checked values validate at the ordinary return boundary. */ import * as ts from "./ts7/syntax.js"; +import { getTextOfNode, tokenStartOfNode } from "./ts7/node-functions.js"; export interface NpmStaticOverloadParameter { name: string; @@ -56,11 +57,11 @@ export function applyNpmStaticJsDocNamepaths( for (const tag of doc.tags?.nodes ?? []) { const expression = tag.typeExpression; if (expression?.kind !== ts.SyntaxKind.JSDocTypeExpression) continue; - const start = expression.getStart(sourceFile); + const start = tokenStartOfNode(expression); if (source[start] !== "{") continue; let depth = 1; let quote = ""; - for (let i = start + 1; i < tag.getEnd() && depth > 0; i++) { + for (let i = start + 1; i < tag.end && depth > 0; i++) { const char = source[i]!; if (quote !== "") { if (char === "\\") i++; @@ -80,7 +81,7 @@ export function applyNpmStaticJsDocNamepaths( continue; } const name = /^[$A-Z_a-z][$\w]*(?:[.#~][$A-Z_a-z][$\w]*)*/.exec( - source.slice(i, tag.getEnd()), + source.slice(i, tag.end), )?.[0]; if (!name) continue; if (name.includes("~") || name.includes("#")) replacements.set(i, name.length); @@ -171,7 +172,7 @@ export function applyNpmStaticNullableClassFields( hasModifier(member, ts.SyntaxKind.StaticKeyword) ) continue; - const leading = source.slice(member.getFullStart(), member.getStart(sourceFile)); + const leading = source.slice(member.pos, tokenStartOfNode(member)); if (jsDocTypeOf(member) !== undefined || leading.includes("@type")) continue; fields.set(member.name.text, { member, invalid: false }); } @@ -285,7 +286,7 @@ export function applyNpmStaticNullableClassFields( for (const field of fields.values()) { if (field.invalid || field.className === undefined) continue; inserts.push({ - offset: field.member.getStart(sourceFile), + offset: tokenStartOfNode(field.member), text: `/** @type {${field.className} | null} */ `, }); } @@ -358,7 +359,7 @@ export function applyNpmStaticFindReturnWidening( (returnType.typeArguments?.nodes.length ?? 0) !== 0 ) continue; - insertions.push({ offset: returnType.getEnd(), length: " | undefined".length }); + insertions.push({ offset: returnType.end, length: " | undefined".length }); } } if (insertions.length === 0) return null; @@ -428,26 +429,22 @@ function hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean { ); } -function safeTypeText( - node: ts.TypeNode, - sourceFile: ts.SourceFile, - className: string, -): string | null { - if (SAFE_KEYWORD_TYPES.has(node.kind) || ts.isThisTypeNode(node)) return node.getText(sourceFile); +function safeTypeText(node: ts.TypeNode, className: string): string | null { + if (SAFE_KEYWORD_TYPES.has(node.kind) || ts.isThisTypeNode(node)) return getTextOfNode(node); if (ts.isLiteralTypeNode(node) && node.literal.kind === ts.SyntaxKind.NullKeyword) return "null"; if (ts.isParenthesizedTypeNode(node)) { - const inner = safeTypeText(node.type, sourceFile, className); + const inner = safeTypeText(node.type, className); return inner === null ? null : `(${inner})`; } if (ts.isArrayTypeNode(node)) { - const element = safeTypeText(node.elementType, sourceFile, className); + const element = safeTypeText(node.elementType, className); return element === null ? null : `${element}[]`; } if (ts.isTypeOperatorNode(node) && node.operator === ts.SyntaxKind.ReadonlyKeyword) { - return safeTypeText(node.type, sourceFile, className); + return safeTypeText(node.type, className); } if (ts.isUnionTypeNode(node)) { - const arms = node.types.nodes.map((type) => safeTypeText(type, sourceFile, className)); + const arms = node.types.nodes.map((type) => safeTypeText(type, className)); return arms.some((arm) => arm === null) ? null : arms.join(" | "); } if ( @@ -483,7 +480,7 @@ function overloadSignature( ) { return null; } - const returnType = safeTypeText(method.type, sourceFile, className); + const returnType = safeTypeText(method.type, className); if (returnType === null) return null; const parameters: NpmStaticOverloadParameter[] = []; for (const parameter of method.parameters.nodes) { @@ -496,7 +493,7 @@ function overloadSignature( ) { return null; } - const type = safeTypeText(parameter.type, sourceFile, className); + const type = safeTypeText(parameter.type, className); if (type === null) return null; parameters.push({ name: parameter.name.text, @@ -553,13 +550,9 @@ export function parseNpmStaticDeclarationOverloads( return classes; } -function nullableSelfType( - node: ts.TypeNode, - sourceFile: ts.SourceFile, - className: string, -): string | null { +function nullableSelfType(node: ts.TypeNode, className: string): string | null { if (!ts.isUnionTypeNode(node) || node.types.nodes.length !== 2) return null; - const arms = node.types.nodes.map((type) => safeTypeText(type, sourceFile, className)); + const arms = node.types.nodes.map((type) => safeTypeText(type, className)); return arms.includes(className) && arms.includes("null") ? `${className} | null` : null; } @@ -613,8 +606,7 @@ export function parseNpmStaticDeclarationProperties( continue; } const type = - nullableSelfType(member.type, sourceFile, className) ?? - nullableVoidCallbackType(member.type); + nullableSelfType(member.type, className) ?? nullableVoidCallbackType(member.type); if (type !== null) properties.set(member.name.text, type); } if (properties.size > 0) classes.set(className, properties); @@ -1012,13 +1004,10 @@ export function applyNpmStaticDeclarationOverloads( ) { continue; } - const leading = source.slice( - bodyStatement.getFullStart(), - bodyStatement.getStart(sourceFile), - ); + const leading = source.slice(bodyStatement.pos, tokenStartOfNode(bodyStatement)); if (!leading.includes("@type")) { inserts.push({ - offset: bodyStatement.getStart(sourceFile), + offset: tokenStartOfNode(bodyStatement), text: `/** @type {${backing.type}} */ `, }); } @@ -1036,13 +1025,13 @@ export function applyNpmStaticDeclarationOverloads( (parameter) => parameter.optional && !existing.includes(`[${parameter.name}]`), ); if (missingOptional) - inserts.push({ offset: member.getStart(sourceFile), text: `${implementation} ` }); + inserts.push({ offset: tokenStartOfNode(member), text: `${implementation} ` }); else if ( jsDocs.length === 0 && member.parameters.nodes.length === 0 && SCALAR_METHOD_RETURNS.has(signatures[0]!.returnType) ) { - inserts.push({ offset: member.getStart(sourceFile), text: `${implementation} ` }); + inserts.push({ offset: tokenStartOfNode(member), text: `${implementation} ` }); } } continue; @@ -1050,7 +1039,9 @@ export function applyNpmStaticDeclarationOverloads( const implementation = jsDocs.length === 0 ? implementationComment(statement.name.text, member, signatures) : null; if (jsDocs.length === 0 && implementation === null) continue; - const offset = jsDocs[0]?.getStart(sourceFile) ?? member.getStart(sourceFile); + const offset = + (jsDocs[0] === undefined ? undefined : tokenStartOfNode(jsDocs[0])) ?? + tokenStartOfNode(member); const text = `${signatures.map(overloadComment).join(" ")} ${implementation === null ? "" : implementation + " "}`; inserts.push({ offset, text }); } @@ -1158,9 +1149,9 @@ export function applyNpmStaticDeclarationProperties( !callbackInitializer(member.initializer)) ) continue; - const leading = source.slice(member.getFullStart(), member.getStart(sourceFile)); + const leading = source.slice(member.pos, tokenStartOfNode(member)); if (jsDocTypeOf(member) !== undefined || leading.includes("@type")) continue; - inserts.push({ offset: member.getStart(sourceFile), text: `/** @type {${type}} */ ` }); + inserts.push({ offset: tokenStartOfNode(member), text: `/** @type {${type}} */ ` }); } const constructor = statement.members.nodes.find( (member): member is ts.ConstructorDeclaration => @@ -1184,12 +1175,9 @@ export function applyNpmStaticDeclarationProperties( } const type = properties.get(left.name.text); if (type === undefined) continue; - const leading = source.slice( - bodyStatement.getFullStart(), - bodyStatement.getStart(sourceFile), - ); + const leading = source.slice(bodyStatement.pos, tokenStartOfNode(bodyStatement)); if (leading.includes("@type")) continue; - inserts.push({ offset: bodyStatement.getStart(sourceFile), text: `/** @type {${type}} */ ` }); + inserts.push({ offset: tokenStartOfNode(bodyStatement), text: `/** @type {${type}} */ ` }); } } if (inserts.length === 0) return null; diff --git a/packages/compiler/src/frontend/npm-static-enum-iife.ts b/packages/compiler/src/frontend/npm-static-enum-iife.ts index 288a172e2..e1b9220a9 100644 --- a/packages/compiler/src/frontend/npm-static-enum-iife.ts +++ b/packages/compiler/src/frontend/npm-static-enum-iife.ts @@ -21,6 +21,7 @@ * export). Merged declarations, computed or referencing members, and any * other write to the variable keep the original text. */ import * as ts from "./ts7/syntax.js"; +import { tokenStartOfNode } from "./ts7/node-functions.js"; import { applyEdits, unparen, type Edit } from "./npm-static-bundled-chunks.js"; /** True when the text may hold an enum IIFE (a cheap probe). */ @@ -42,17 +43,17 @@ export function rewriteEnumIifes(sf: ts.SourceFile): string | null { // enum) keeps exporting the binding. const declaration = stmts[i - 1] as ts.VariableStatement; const modifiers = sf.text.slice( - declaration.getStart(sf), - declaration.declarationList.getStart(sf), + tokenStartOfNode(declaration), + tokenStartOfNode(declaration.declarationList), ); edits.push({ - start: declaration.getStart(sf), - end: declaration.getEnd(), + start: tokenStartOfNode(declaration), + end: declaration.end, text: `${modifiers}var ${decl} = ${objectLiteral(iife.members)};`, }); edits.push({ - start: stmts[i]!.getStart(sf), - end: stmts[i]!.getEnd(), + start: tokenStartOfNode(stmts[i]!), + end: stmts[i]!.end, text: iife.exportName === null ? "" : `exports.${iife.exportName} = ${decl};`, }); } @@ -232,8 +233,8 @@ function objectLiteral(members: Members): string { /** True when the module writes `name` anywhere outside the enum call. */ function writtenElsewhere(sf: ts.SourceFile, name: string, iife: ts.Statement): boolean { - const start = iife.getStart(sf); - const end = iife.getEnd(); + const start = tokenStartOfNode(iife); + const end = iife.end; let written = false; ts.walkPreorder(sf, (n) => { if (written) return "stop"; diff --git a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts index 348b04817..c48420625 100644 --- a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts +++ b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts @@ -6,6 +6,7 @@ import { isModuleExports, } from "./cjs-syntax.js"; import { rewriteBundledFunctionImports } from "./npm-static-bundled-cjs.js"; +import { getTextOfNode, tokenStartOfNode } from "./ts7/node-functions.js"; /** Resolution stays with the compilation host. Syntax never reads disk or * starts a parser implicitly, so native and Node hosts run the same pass. */ @@ -57,7 +58,7 @@ function liveExportTableFirst( }; let sawTable = false; for (const stmt of sf.statements.nodes) { - if (stmt.getStart(sf) >= installAt) return sawTable; + if (tokenStartOfNode(stmt) >= installAt) return sawTable; if (ts.isFunctionDeclaration(stmt) || ts.isEmptyStatement(stmt)) continue; if (ts.isExpressionStatement(stmt) && ts.isStringLiteral(stmt.expression)) continue; if (ts.isVariableStatement(stmt)) { @@ -146,7 +147,7 @@ function chaseValue(e: ts.Expression): { kind: "ident" | "hoist"; text: string } cur.kind === ts.SyntaxKind.FalseKeyword || cur.kind === ts.SyntaxKind.NullKeyword ) { - return { kind: "hoist", text: cur.getText() }; + return { kind: "hoist", text: getTextOfNode(cur) }; } // ident-rooted member chains: this.x / a.b.c / a["k"].d — side-effect // free reads the tail const can evaluate once. @@ -161,7 +162,7 @@ function chaseValue(e: ts.Expression): { kind: "ident" | "hoist"; text: string } ) { walk = walk.expression; } - if (ts.isIdentifier(walk)) return { kind: "hoist", text: cur.getText() }; + if (ts.isIdentifier(walk)) return { kind: "hoist", text: getTextOfNode(cur) }; return null; } @@ -182,7 +183,7 @@ function chaseObjectLiteral(e: ts.Expression): { kind: "hoist"; text: string } | ) return null; } - return { kind: "hoist", text: cur.getText() }; + return { kind: "hoist", text: getTextOfNode(cur) }; } /** The getter body's returned expression for the two descriptor getter @@ -326,7 +327,7 @@ function recognizedToEsmDecl(d: ts.VariableDeclaration): boolean { let to: ts.Expression = expr.arguments.nodes[0]!; while (ts.isParenthesizedExpression(to)) to = to.expression; if (!ts.isConditionalExpression(to)) return false; - const condText = to.condition.getText(); + const condText = getTextOfNode(to.condition); if (!condText.includes(isNodeMode) || !condText.includes("__esModule")) return false; let stamp: ts.Expression = to.whenTrue; while (ts.isParenthesizedExpression(stamp)) stamp = stamp.expression; @@ -443,7 +444,7 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH ) { if (parent.name.text === "default" && defaultIsModule) { if (!readOnlyAccess(parent)) return fail(TO_ESM_ESCAPE_DEGRADE); - pads.push({ start: child.getEnd(), end: parent.getEnd() }); + pads.push({ start: child.end, end: parent.end }); } } else { return fail(TO_ESM_ESCAPE_DEGRADE); @@ -451,8 +452,8 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH // the wrapper itself: callee + '(' down, and everything after the // require argument (`, 1` included) — the bare require survives at its // original offsets. - pads.push({ start: call.getStart(sf), end: arg0!.getStart(sf) }); - pads.push({ start: arg0!.getEnd(), end: call.getEnd() }); + pads.push({ start: tokenStartOfNode(call), end: tokenStartOfNode(arg0!) }); + pads.push({ start: arg0!.end, end: call.end }); } if (bindings.size > 0) { @@ -513,7 +514,7 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH violated = true; return; } - pads.push({ start: n.expression.getEnd(), end: n.getEnd() }); + pads.push({ start: n.expression.end, end: n.end }); } }; ts.walkPreorder(sf, padDefaults); @@ -628,8 +629,8 @@ export function rewriteBundlerCjsSyntax( ts.isIdentifier(rhs.arguments.nodes[0]!) ) { esbuildMainTarget = (rhs.arguments.nodes[0] as ts.Identifier).text; - tablePos = stmt.getStart(sf); - neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + tablePos = tokenStartOfNode(stmt); + neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end }); sawBundleShape = true; continue; } @@ -662,9 +663,9 @@ export function rewriteBundlerCjsSyntax( names, finalRhs: cur, viaBareExports, - pos: stmt.getStart(sf), - start: stmt.getStart(sf), - end: stmt.getEnd(), + pos: tokenStartOfNode(stmt), + start: tokenStartOfNode(stmt), + end: stmt.end, }); continue; // classified later (only when a rewrite actually happens) } @@ -684,7 +685,7 @@ export function rewriteBundlerCjsSyntax( isModuleExports(inner.left) && ts.isObjectLiteralExpression(inner.right) ) { - neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end }); sawBundleShape = true; continue; } @@ -695,7 +696,7 @@ export function rewriteBundlerCjsSyntax( const table = exportCallOf(e); if (table !== null) { exportTables.push(table); - neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end }); sawBundleShape = true; continue; } @@ -705,7 +706,7 @@ export function rewriteBundlerCjsSyntax( const spec = bareRequireSpecOf(e.arguments.nodes[0]!); if (spec !== null) { addStarSpec(spec); - neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end }); sawBundleShape = true; continue; } @@ -716,7 +717,7 @@ export function rewriteBundlerCjsSyntax( const spec = bareRequireSpecOf(e.arguments.nodes[1]!); if (spec !== null) { addStarSpec(spec); - neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end }); sawBundleShape = true; continue; } @@ -725,7 +726,7 @@ export function rewriteBundlerCjsSyntax( const def = definePropertyExportOf(e); if (def !== null) { if (def.esModuleStamp) { - neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end }); // the canonical table re-stamps it (lexer-visibly — merve detects // this defineProperty form, so parity wants a visible entry) continue; @@ -733,7 +734,7 @@ export function rewriteBundlerCjsSyntax( const chased = def.value !== null ? chaseValue(def.value) : null; if (chased !== null) { definePropEntries.push({ name: def.name, value: def.value }); - neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end }); sawBundleShape = true; } // unchaseable descriptor: LEAVE the statement (its runtime fence @@ -792,7 +793,7 @@ export function rewriteBundlerCjsSyntax( // The table feeding module.exports keeps a getter it cannot chase // as a live accessor rather than a snapshot. if (spec.kind === "any" && value !== null && t.target === esbuildMainTarget) - claim(name, { name, kind: "getter", value: value.getText() }); + claim(name, { name, kind: "getter", value: getTextOfNode(value) }); else claim(name, spec); } } @@ -951,8 +952,8 @@ export function rewriteBundlerCjsSyntax( helperDecls.set( d.name.text, decls.length === 1 - ? { start: stmt.getStart(sf), end: stmt.getEnd() } - : { start: d.getStart(sf), end: d.getEnd() }, + ? { start: tokenStartOfNode(stmt), end: stmt.end } + : { start: tokenStartOfNode(d), end: d.end }, ); } if (anyHelper && decls.length > 1) helperStmts.push(stmt); @@ -966,8 +967,8 @@ export function rewriteBundlerCjsSyntax( if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.name === node) return; const list = refs.get(node.text); - if (list === undefined) refs.set(node.text, [node.getStart(sf)]); - else list.push(node.getStart(sf)); + if (list === undefined) refs.set(node.text, [tokenStartOfNode(node)]); + else list.push(tokenStartOfNode(node)); } }; ts.walkPreorder(sf, record); @@ -999,12 +1000,12 @@ export function rewriteBundlerCjsSyntax( const deadAt = decls.map((d) => { if (!ts.isIdentifier(d.name) || !removed.has(d.name.text)) return false; const span = helperDecls.get(d.name.text)!; - if (span.start !== d.getStart(sf)) return false; + if (span.start !== tokenStartOfNode(d)) return false; declaratorSpans.add(span); return true; }); if (deadAt.every((x) => x)) { - declaratorPads.push({ start: stmt.getStart(sf), end: stmt.getEnd() }); + declaratorPads.push({ start: tokenStartOfNode(stmt), end: stmt.end }); continue; } // Dead declarators before the last live one take their trailing @@ -1015,8 +1016,8 @@ export function rewriteBundlerCjsSyntax( const d = decls[i]!; declaratorPads.push( i < lastLive - ? { start: d.getStart(sf), end: decls[i + 1]!.getStart(sf) } - : { start: decls[lastLive]!.getEnd(), end: d.getEnd() }, + ? { start: tokenStartOfNode(d), end: tokenStartOfNode(decls[i + 1]!) } + : { start: decls[lastLive]!.end, end: d.end }, ); } } diff --git a/packages/compiler/src/frontend/npm-static-subtyping.ts b/packages/compiler/src/frontend/npm-static-subtyping.ts index 04c66a559..18d69d4c4 100644 --- a/packages/compiler/src/frontend/npm-static-subtyping.ts +++ b/packages/compiler/src/frontend/npm-static-subtyping.ts @@ -19,8 +19,8 @@ export function isNpmStaticSubclassArgument( symbol.declarations.some( (declaration) => (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)) && - /\.[cm]?js$/.test(declaration.getSourceFile().fileName) && - npmStaticPackageOfPath(declaration.getSourceFile().fileName) !== null, + /\.[cm]?js$/.test(ts.getSourceFileOfNode(declaration).fileName) && + npmStaticPackageOfPath(ts.getSourceFileOfNode(declaration).fileName) !== null, ) ); }; @@ -50,12 +50,12 @@ export function isNpmStaticSubclassArgument( }; let accepted = false; ts.walkPreorder(source, (node) => { - if (node.end < diagnostic.pos || node.getStart(source) > diagnostic.end) return "skip"; + if (node.end < diagnostic.pos || ts.tokenStartOfNode(node) > diagnostic.end) return "skip"; if (!ts.isCallExpression(node) && !ts.isNewExpression(node)) return undefined; const index = node.arguments?.nodes.findIndex( (argument) => - argument.getStart(source) === diagnostic.pos && argument.end === diagnostic.end, + ts.tokenStartOfNode(argument) === diagnostic.pos && argument.end === diagnostic.end, ) ?? -1; if (index < 0) return undefined; const parameter = checker.getResolvedSignature(node)?.getParameters()[index]; diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 71f6d8057..a9e926a1a 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -1259,7 +1259,7 @@ export function isEsModuleStamp(expr: ts.Expression): boolean { function pureCjsExport7(program: ts.Program, stmt: ts.Statement): boolean { const cjs = cjsExportAssignmentOf(stmt); if (cjs === null) return false; - const sf = stmt.getSourceFile(); + const sf = ts.getSourceFileOfNode(stmt); const plainMember = (left: ts.Expression): boolean => { if (!ts.isPropertyAccessExpression(left) || left.questionDotToken) return false; // A replacement export can have setters or non-writable function @@ -1314,7 +1314,7 @@ function sourceBinding7(program: ts.Program, ident: ts.Identifier): boolean { return ( symbol !== undefined && symbol.declarations.some( - (decl) => !decl.getSourceFile().isDeclarationFile && !ts.isSourceFile(decl), + (decl) => !ts.getSourceFileOfNode(decl).isDeclarationFile && !ts.isSourceFile(decl), ) ); } @@ -1341,7 +1341,7 @@ function purePrefixStmt7(program: ts.Program, s: ts.Statement): boolean { if (ts.isEmptyStatement(s) || ts.isFunctionDeclaration(s)) return true; if (ts.isExpressionStatement(s) && ts.isStringLiteral(s.expression)) return true; // directive if (isRequireStatement7(s)) return true; - if (isCjsJsFile7(s.getSourceFile(), program)) { + if (isCjsJsFile7(ts.getSourceFileOfNode(s), program)) { if (ts.isExpressionStatement(s) && isEsModuleStamp(s.expression)) { const call = s.expression as ts.CallExpression; const object = (call.expression as ts.PropertyAccessExpression).expression as ts.Identifier; @@ -1468,7 +1468,7 @@ function requireEarlyReference7( if (cached !== undefined) return cached; let found: ts.Identifier | null = null; const stmt = decl.parent?.parent; - const sf = decl.getSourceFile(); + const sf = ts.getSourceFileOfNode(decl); if (stmt !== undefined && ts.isVariableStatement(stmt) && ts.isSourceFile(stmt.parent)) { const stmts = sf.statements.nodes; const k = stmts.indexOf(stmt); @@ -2044,7 +2044,7 @@ function isCreateRequireImport7(program: ts.Program, ident: ts.Identifier): bool 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 p: ts.Node | undefined = targetDecl.parent; p !== undefined && !ts.isSourceFile(p); @@ -2052,7 +2052,8 @@ function isCreateRequireImport7(program: ts.Program, ident: ts.Identifier): bool ) { if (ts.isModuleDeclaration(p) && ts.isStringLiteral(p.name)) { return ( - canonicalBuiltinModule(p.name.text) === "module" && target.name === "createRequire" + canonicalBuiltinModule(p.name.text) === "module" && + ts.symbolName(target) === "createRequire" ); } } @@ -2136,7 +2137,7 @@ function stableCreateRequireBindingReason7( return "its createRequire binding is redeclared"; if ((ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Export) !== 0) return "its createRequire result escapes through an exported binding"; - const sf = decl.getSourceFile(); + const sf = ts.getSourceFileOfNode(decl); const refs = identifierOccurrences7(sf, decl.name.text); checker.prefetchSymbolNodesExact(refs); for (const ref of refs) { @@ -2203,7 +2204,7 @@ function npmStaticModuleImportReason7( } if (!bindings || !ts.isNamedImports(bindings)) return null; const checker = program.getTypeChecker(); - const sf = stmt.getSourceFile(); + const sf = ts.getSourceFileOfNode(stmt); for (const specifier of bindings.elements.nodes) { if ((specifier.propertyName?.text ?? specifier.name.text) !== "createRequire") continue; const imported = checker.getSymbolAtLocation(specifier.name); @@ -2283,7 +2284,7 @@ function requireCallBindingKind7(program: ts.Program, node: ts.Node): RequireCal const symbol = checker.getSymbolAtLocation(call.expression); if (symbol === undefined) return "ambient"; const decls = symbol.declarations; - if (decls.every((d) => d.getSourceFile().isDeclarationFile)) return "ambient"; + if (decls.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)) return "ambient"; return isCreateRequireBinding7(program, call.expression) ? "createRequire" : "source"; } @@ -2438,15 +2439,15 @@ function selfImportTdzFences7( // are initialized. Hoisted functions have none. const tdzEndOf = (aliased: ts.Symbol): number | null => { const d = aliased.valueDeclaration ?? aliased.declarations[0]; - if (d === undefined || d.getSourceFile() !== sf) return null; + if (d === undefined || ts.getSourceFileOfNode(d) !== sf) return null; if (ts.isFunctionDeclaration(d)) return null; - return d.getEnd(); + return d.end; }; const fence = (ref: ts.Identifier, name: string): void => { diags.push( unsupportedDiag( "SC1030", - { file: sf.fileName, start: ref.getStart(sf), end: ref.getEnd() }, + { file: sf.fileName, start: ts.tokenStartOfNode(ref), end: ref.end }, `the self-import binding '${name}' read above its declaration (the import aliases this module's OWN binding — Node initializes it when the declaration executes, so this read throws ReferenceError; move the read below the declaration)`, ), ); @@ -2462,7 +2463,7 @@ function selfImportTdzFences7( ts.isIdentifier(node) && node !== bindingName && node.text === bindingName.text && - node.getStart(sf) < end && + ts.tokenStartOfNode(node) < end && !insideFunction(node) && checker.getSymbolAtLocation(node) === sym ) { @@ -2495,7 +2496,7 @@ function selfImportTdzFences7( const memberSym = checker.getSymbolAtLocation(node.name); if (memberSym !== undefined) { const end = tdzEndOf(memberSym); - if (end !== null && node.getStart(sf) < end) + if (end !== null && ts.tokenStartOfNode(node) < end) fence(node.expression, `${nsName.text}.${node.name.text}`); } } @@ -2695,7 +2696,7 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod if (sym === undefined) return false; if (sym.flags & ts.SymbolFlags.Alias) sym = checker.getAliasedSymbol(sym); const decls = sym.declarations; - return decls.length > 0 && decls.every((d) => d.getSourceFile().isDeclarationFile); + return decls.length > 0 && decls.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile); }; const hasDecorator = (n: ts.Node): boolean => ((n as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []).some( @@ -2969,8 +2970,8 @@ export function makeCycleAdmission( } }; const lineOf = (node: ts.Node): string => { - const nsf = node.getSourceFile(); - return `${nsf.fileName}:${ts.getLineAndCharacterOfPosition(nsf, node.getStart(nsf)).line + 1}`; + const nsf = ts.getSourceFileOfNode(node); + return `${nsf.fileName}:${ts.getLineAndCharacterOfPosition(nsf, ts.tokenStartOfNode(node)).line + 1}`; }; // Cluster verdict memo (keyed by the component array identity): true // when every member is an ES module passing the inert-top-level bar. @@ -3477,7 +3478,7 @@ function exactCycleRefusal7( const targetOf = (stmt: ts.ImportDeclaration | ts.ExportDeclaration): ts.SourceFile | null => { const spec = stmt.moduleSpecifier; if (spec === undefined || !ts.isStringLiteral(spec)) return null; - const sf = stmt.getSourceFile(); + const sf = ts.getSourceFileOfNode(stmt); return resolveImport7(program, sf, spec.text) ?? npmStaticDepSf7(program, sf, spec.text); }; for (const m of comp) { @@ -3711,8 +3712,8 @@ function computeCycleEarlyBindings7( module: m, exposed, rank, - start: stmt.getStart(m), - end: stmt.getEnd(), + start: ts.tokenStartOfNode(stmt), + end: stmt.end, reference: null, folded: null, }); @@ -3757,13 +3758,13 @@ function computeCycleEarlyBindings7( if ( c.kind === "class" && sf === c.module && - ref.getStart(sf) >= c.start && - ref.getEnd() <= c.end + ts.tokenStartOfNode(ref) >= c.start && + ref.end <= c.end ) continue; let early: boolean; if (inDeferredPosition7(ref)) early = c.exposed; - else if (sf === c.module) early = ref.getStart(sf) < c.start; + else if (sf === c.module) early = ts.tokenStartOfNode(ref) < c.start; else if (!memberSet.has(sf) || !rankOf.has(sf)) early = false; else early = !known || rankOf.get(sf)! < c.rank; if (early) c.reference = ref; @@ -3902,7 +3903,7 @@ function moduleEarlyBindingsOf7( store(stmt.name, [stmt]); } else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) { store(stmt.name, [stmt]); - candidates.push({ name: stmt.name, index, start: stmt.getStart(sf), cls: stmt }); + candidates.push({ name: stmt.name, index, start: ts.tokenStartOfNode(stmt), cls: stmt }); candidateNames.add(stmt.name.text); } else if (ts.isVariableStatement(stmt)) { const lexical = (stmt.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0; @@ -3914,7 +3915,7 @@ function moduleEarlyBindingsOf7( } if (!lexical) continue; for (const name of boundNames7(d.name)) { - candidates.push({ name, index, start: stmt.getStart(sf) }); + candidates.push({ name, index, start: ts.tokenStartOfNode(stmt) }); candidateNames.add(name.text); } } @@ -4006,9 +4007,10 @@ function moduleEarlyBindingsOf7( c !== undefined && node !== c.name && !reachedRefs.has(symbol) && - !(c.cls !== undefined && node.getStart(sf) >= c.cls.pos && node.end <= c.cls.end) + !(c.cls !== undefined && ts.tokenStartOfNode(node) >= c.cls.pos && node.end <= c.cls.end) ) { - if (inDeferredPosition7(node) || node.getStart(sf) < c.start) reachedRefs.set(symbol, node); + if (inDeferredPosition7(node) || ts.tokenStartOfNode(node) < c.start) + reachedRefs.set(symbol, node); } for (const unit of unitsBySymbol.get(symbol) ?? []) { if (reachedUnits.has(unit)) continue; @@ -4027,8 +4029,8 @@ function moduleEarlyBindingsOf7( const report = (declaration: ts.Node): void => { while ( next < candidates.length && - candidates[next]!.name.getStart(sf) >= declaration.getStart(sf) && - candidates[next]!.name.getEnd() <= declaration.getEnd() + ts.tokenStartOfNode(candidates[next]!.name) >= ts.tokenStartOfNode(declaration) && + candidates[next]!.name.end <= declaration.end ) { const c = candidates[next++]!; const symbol = checker.getSymbolAtLocation(c.name); @@ -4088,7 +4090,7 @@ function returnsOnlyClosuresOver7( const decls = symbol === undefined ? [] : symbol.declarations; if (decls.length !== 1) return false; const decl = decls[0]!; - if (decl.getSourceFile().isDeclarationFile) return false; + if (ts.getSourceFileOfNode(decl).isDeclarationFile) return false; let fn: ts.Node | undefined; if (ts.isFunctionDeclaration(decl)) fn = decl; else if (ts.isVariableDeclaration(decl) && decl.initializer !== undefined) { @@ -4209,12 +4211,13 @@ function laterJobCallee7(checker: ts.TypeChecker, callee: ts.Expression): boolea } if (symbol === undefined) return false; const decls = symbol.declarations; - if (decls.length === 0 || !decls.every((d) => d.getSourceFile().isDeclarationFile)) return false; + if (decls.length === 0 || !decls.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)) + return false; if (LATER_JOB_BUILTINS7.has(name.text)) return true; // The JavaScript standard library's callbacks mostly run synchronously; // package declarations describe user code. return decls.every((d) => { - const file = d.getSourceFile().fileName; + const file = ts.getSourceFileOfNode(d).fileName; return isNodeTypesPath(file) || file === fallbackDtsPath(); }); } @@ -4361,8 +4364,8 @@ function resolveNpmImport7( } function locOf7(node: ts.Node): { file: string; start: number; end: number } { - const sf = node.getSourceFile(); - return { file: sf.fileName, start: node.getStart(sf), end: node.getEnd() }; + const sf = ts.getSourceFileOfNode(node); + return { file: sf.fileName, start: ts.tokenStartOfNode(node), end: node.end }; } /** The program file holding an opted-in package's resolved runtime JS: @@ -4916,7 +4919,7 @@ function preflight7(load: LoadResult): { } if (reDep && !reDep.fileName.endsWith(".json")) { deps.push({ dep: reDep, stmt }); - depPositions.get(sf)!.push({ dep: reDep, pos: stmt.getStart(sf) }); + depPositions.get(sf)!.push({ dep: reDep, pos: ts.tokenStartOfNode(stmt) }); } continue; } @@ -4964,7 +4967,7 @@ function preflight7(load: LoadResult): { refusals.get(sf)!.push({ crash: { message, className, loc: locOf7(stmt) }, fallback: unsupportedDiag("SC1010", locOf7(stmt), fallbackFeature), - pos: stmt.getStart(sf), + pos: ts.tokenStartOfNode(stmt), }); }; // Project modules win before npm fallback: this is the source graph @@ -5177,7 +5180,7 @@ function preflight7(load: LoadResult): { } if (dep && !isJson) { deps.push({ dep, stmt }); - depPositions.get(sf)!.push({ dep, pos: stmt.getStart(sf) }); + depPositions.get(sf)!.push({ dep, pos: ts.tokenStartOfNode(stmt) }); } } { @@ -5217,7 +5220,11 @@ function preflight7(load: LoadResult): { for (let k = 0; k < stmts.length; k++) { const stmt = stmts[k]!; for (const req of requiresOf7(stmt)) { - const loc = { file: sf.fileName, start: req.node.getStart(sf), end: req.node.getEnd() }; + const loc = { + file: sf.fileName, + start: ts.tokenStartOfNode(req.node), + end: req.node.end, + }; const bindingKind = requireCallBindingKind7(program, req.node); // TypeScript source files join this scan only through the // canonical createRequire bridge. Their ordinary require-shaped @@ -5363,7 +5370,7 @@ function preflight7(load: LoadResult): { ); for (const call of nestedBareRequires) { const spec = requireSpecOf7(call)!; - const loc = { file: sf.fileName, start: call.getStart(sf), end: call.getEnd() }; + const loc = { file: sf.fileName, start: ts.tokenStartOfNode(call), end: call.end }; const bindingKind = requireCallBindingKind7(program, call); if (!jsRequireFile && bindingKind !== "createRequire") continue; if (isNodeEsmFile7(sf, program)) { @@ -5780,7 +5787,7 @@ function cjsNamedImportLinkCheck( // swaps ` as ` for `: ` — reproduced exactly, quirks included // (multi-line import statements simply lose the hint). const text = bad.sf.text; - const pos = bad.nameNode.getStart(bad.sf); + const pos = ts.tokenStartOfNode(bad.nameNode); const lineStart = text.lastIndexOf("\n", pos - 1) + 1; const lineEnd = text.indexOf("\n", pos); let line = text.slice(lineStart, lineEnd < 0 ? text.length : lineEnd); @@ -6094,7 +6101,7 @@ function earlierLinkCrash( if (!element.isTypeOnly) { entries.push({ local: element.name.text, - start: (element.propertyName ?? element.name).getStart(sf), + start: ts.tokenStartOfNode(element.propertyName ?? element.name), kind: 0, }); } @@ -6118,7 +6125,7 @@ function earlierLinkCrash( for (const element of exportClause.elements.nodes) { if ( !element.isTypeOnly && - (element.propertyName ?? element.name).getStart(sf) === crash.loc.start + ts.tokenStartOfNode(element.propertyName ?? element.name) === crash.loc.start ) { return [1, reexport]; } @@ -6184,7 +6191,11 @@ export function isJsSourceFile(sf: ts.SourceFile): boolean { export function isCheckedJsSourceFile(sf: ts.SourceFile, projectCheckJs: boolean): boolean { if (!isJsSourceFile(sf) || isNodeModulesPath(sf.fileName)) return false; let checked = projectCheckJs; - const head = sf.text.slice(0, sf.statements.nodes[0]?.getStart(sf) ?? sf.text.length); + const head = sf.text.slice( + 0, + (sf.statements.nodes.length === 0 ? undefined : ts.tokenStartOfNode(sf.statements.nodes[0]!)) ?? + sf.text.length, + ); for (const line of head.split("\n")) { const directive = /^\s*\/\/\s*@ts-(no)?check\b/.exec(line); if (directive) checked = directive[1] === undefined; @@ -6383,8 +6394,8 @@ export function cjsVoidInitPreambleReals( tail = tail.right; } if (!isVoid(tail)) return null; - const sf = cjs.expr.getSourceFile(); - const myPos = cjs.expr.getStart(sf); + const sf = ts.getSourceFileOfNode(cjs.expr); + const myPos = ts.tokenStartOfNode(cjs.expr); const reals = new Map< string, Extract, { kind: "member" }> @@ -6397,7 +6408,7 @@ export function cjsVoidInitPreambleReals( other?.kind === "member" && ts.isIdentifier(other.name) && other.name.text === name && - other.expr.getStart(sf) > myPos && + ts.tokenStartOfNode(other.expr) > myPos && !isVoid(other.value), ); if (real?.kind !== "member") return null; @@ -6555,7 +6566,7 @@ export function isCjsExportTableLiteral(obj: ts.ObjectLiteralExpression): boolea } const decl = obj.parent; if (!ts.isVariableDeclaration(decl) || decl.initializer !== obj) return false; - const sf = obj.getSourceFile(); + const sf = ts.getSourceFileOfNode(obj); if (!isJsSourceFile(sf)) return false; for (const stmt of sf.statements.nodes) { const cjs = cjsExportAssignmentOf(stmt); diff --git a/packages/compiler/src/frontend/services.test.ts b/packages/compiler/src/frontend/services.test.ts index bd172bbc5..e491b0ac5 100644 --- a/packages/compiler/src/frontend/services.test.ts +++ b/packages/compiler/src/frontend/services.test.ts @@ -7,6 +7,7 @@ import { Ts7Api } from "./ts7/rpc-api.js"; import { checkPreflight, isNodeEsmFile, loadProgram } from "./program.js"; import { clearResolveCaches } from "./resolve.js"; import type { Ts7ApiOptions } from "./ts7/program-host.js"; +import { getTextOfNode } from "./ts7/node-functions.js"; const directories: string[] = []; afterEach(() => { @@ -80,7 +81,7 @@ test("syntax sessions reuse one connection and returned ASTs survive edits and c services.close(); services.close(); expect(connections[0]!.close).toHaveBeenCalledTimes(1); - expect(before.statements.nodes[0]!.getText()).toBe(before.text); + expect(getTextOfNode(before.statements.nodes[0]!)).toBe(before.text); }); test("program hosts remain usable after their creating services close", () => { diff --git a/packages/compiler/src/frontend/tier-analysis.ts b/packages/compiler/src/frontend/tier-analysis.ts index 2ed5c8c9b..b75e491a8 100644 --- a/packages/compiler/src/frontend/tier-analysis.ts +++ b/packages/compiler/src/frontend/tier-analysis.ts @@ -107,8 +107,8 @@ function nodeAt(sf: ts.SourceFile, start: number, end: number): ts.Node { let node: ts.Node = sf; for (;;) { let next: ts.Node | undefined; - node.forEachChild((child) => { - if (child.getStart(sf) <= start && end <= child.getEnd()) { + ts.forEachChild(node, (child) => { + if (ts.tokenStartOfNode(child) <= start && end <= child.end) { next = child; return true; } @@ -263,23 +263,23 @@ function functionName(node: ts.Node): string { if (ts.isBinaryExpression(parent) && parent.right === node) { const left = parent.left; if (ts.isIdentifier(left)) return left.text; - if (ts.isPropertyAccessExpression(left)) return left.getText(); + if (ts.isPropertyAccessExpression(left)) return ts.getTextOfNode(left); } } - const sf = node.getSourceFile(); - const pos = ts.getLineAndCharacterOfPosition(sf, node.getStart(sf)); + const sf = ts.getSourceFileOfNode(node); + const pos = ts.getLineAndCharacterOfPosition(sf, ts.tokenStartOfNode(node)); return ``; } function locOfNode(node: ts.Node): SrcLoc { - const sf = node.getSourceFile(); - return { file: sf.fileName, start: node.getStart(sf), end: node.getEnd() }; + const sf = ts.getSourceFileOfNode(node); + return { file: sf.fileName, start: ts.tokenStartOfNode(node), end: node.end }; } function enclosingFunction(node: ts.Node): ts.Node { let current: ts.Node | undefined = node.parent; while (current !== undefined && !isFunctionLikeOrFile(current)) current = current.parent; - return current ?? node.getSourceFile(); + return current ?? ts.getSourceFileOfNode(node); } function functionUnit(node: ts.Node, instance: string | null): DemotionUnit { @@ -423,7 +423,10 @@ class UnitResolver { return this.declarationUnit(binding); } if (ts.isVariableDeclaration(decl) || ts.isParameter(decl) || ts.isBindingElement(decl)) { - const name = nameText(decl.name) ?? decl.name?.getText() ?? ""; + const name = + nameText(decl.name) ?? + (decl.name === undefined ? undefined : ts.getTextOfNode(decl.name)) ?? + ""; const scope = enclosingFunction(decl); const unit: DemotionUnit = { kind: "binding", name, loc: locOfNode(decl) }; if (ts.isParameter(decl) && ts.isFunctionLikeDeclaration(decl.parent)) @@ -437,7 +440,7 @@ class UnitResolver { /** The unit an operand expression denotes. */ valueUnit(expr: ts.Node): { unit: DemotionUnit; reflection: boolean } | null { const { root, viaPrototype } = rootOf(expr); - const sf = root.getSourceFile(); + const sf = ts.getSourceFileOfNode(root); if (root.kind === ts.SyntaxKind.ThisKeyword) { const owner = enclosingFunction(root); if (ts.isSourceFile(owner)) @@ -475,7 +478,7 @@ class UnitResolver { if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = this.checker.getAliasedSymbol(symbol); const decls = symbol === undefined ? [] : symbol.declarations; - const decl = decls.find((d) => !d.getSourceFile().isDeclarationFile); + const decl = decls.find((d) => !ts.getSourceFileOfNode(d).isDeclarationFile); if (decl === undefined) { // CommonJS `exports`/`module` as values: the module object. if (isModuleObjectName(root)) diff --git a/packages/compiler/src/frontend/ts7/api-conformance.ts b/packages/compiler/src/frontend/ts7/api-conformance.ts new file mode 100644 index 000000000..304b4f32f --- /dev/null +++ b/packages/compiler/src/frontend/ts7/api-conformance.ts @@ -0,0 +1,297 @@ +/* Compile-time checks that scriptc's AST and semantic objects have the shapes + * of tsc-ts/api's interfaces. The frontend keeps its own types, whose nodes + * extend the AstNode class, because native code reads class fields directly + * and reads through a structural interface dynamically. This module only + * declares types and emits nothing. */ + +import type * as api from "tsc-ts/api"; +import type { AstNode, AstNodeList } from "./ast-node.js"; +import type * as ast from "./ast-types.js"; + +type Conforms = [T, U]; + +/* Members where tsgo's wire, which the RPC model reads, differs from tsc-ts: + * - SourceFile.externalModuleIndicator is `true` where moduleDetection makes a + * file a module without an import or export. + * - JSDocTypeLiteral.jsdocPropertyTags is one tag. + * - SyntaxList.children is a node list. + * - SyntheticExpression's isSpread and type are factory fields that a parsed + * tree never has. */ +type Without = Omit; + +export interface AstConformance { + Node: Conforms; + NodeList: Conforms; + SourceFile: Conforms< + Without, + Without + >; + ArrayBindingPattern: Conforms; + ArrayLiteralExpression: Conforms; + ArrayTypeNode: Conforms; + ArrowFunction: Conforms; + AsExpression: Conforms; + AwaitExpression: Conforms; + BigIntLiteral: Conforms; + BinaryExpression: Conforms; + BindingElement: Conforms; + Block: Conforms; + BodyBase: Conforms; + BreakStatement: Conforms; + CallExpression: Conforms; + CallSignatureDeclaration: Conforms; + CaseBlock: Conforms; + CaseClause: Conforms; + CatchClause: Conforms; + ClassDeclaration: Conforms; + ClassElementBase: Conforms; + ClassExpression: Conforms; + ClassLikeBase: Conforms; + ClassStaticBlockDeclaration: Conforms< + ast.ClassStaticBlockDeclaration, + api.ClassStaticBlockDeclaration + >; + ComputedPropertyName: Conforms; + ConditionalExpression: Conforms; + ConditionalTypeNode: Conforms; + ConstructSignatureDeclaration: Conforms< + ast.ConstructSignatureDeclaration, + api.ConstructSignatureDeclaration + >; + ConstructorDeclaration: Conforms; + ConstructorTypeNode: Conforms; + ContinueStatement: Conforms; + DebuggerStatement: Conforms; + DeclarationBase: Conforms; + Decorator: Conforms; + DefaultClause: Conforms; + DeleteExpression: Conforms; + DoStatement: Conforms; + ElementAccessExpression: Conforms; + EmptyStatement: Conforms; + EnumDeclaration: Conforms; + EnumMember: Conforms; + ExportAssignment: Conforms; + ExportDeclaration: Conforms; + ExportSpecifier: Conforms; + ExpressionBase: Conforms; + ExpressionStatement: Conforms; + ExpressionWithTypeArguments: Conforms< + ast.ExpressionWithTypeArguments, + api.ExpressionWithTypeArguments + >; + ExternalModuleReference: Conforms; + ForInStatement: Conforms; + ForOfStatement: Conforms; + ForStatement: Conforms; + FunctionDeclaration: Conforms; + FunctionExpression: Conforms; + FunctionLikeBase: Conforms; + FunctionLikeWithBodyBase: Conforms; + FunctionTypeNode: Conforms; + GetAccessorDeclaration: Conforms; + HeritageClause: Conforms; + Identifier: Conforms; + IfStatement: Conforms; + ImportAttribute: Conforms; + ImportAttributes: Conforms; + ImportClause: Conforms; + ImportDeclaration: Conforms; + ImportEqualsDeclaration: Conforms; + ImportSpecifier: Conforms; + ImportTypeNode: Conforms; + IndexSignatureDeclaration: Conforms; + IndexedAccessTypeNode: Conforms; + InferTypeNode: Conforms; + InterfaceDeclaration: Conforms; + IntersectionTypeNode: Conforms; + IterationStatementBase: Conforms; + JSDoc: Conforms; + JSDocAllType: Conforms; + JSDocAugmentsTag: Conforms; + JSDocCallbackTag: Conforms; + JSDocCommentBase: Conforms; + JSDocDeprecatedTag: Conforms; + JSDocImplementsTag: Conforms; + JSDocImportTag: Conforms; + JSDocLink: Conforms; + JSDocLinkCode: Conforms; + JSDocLinkPlain: Conforms; + JSDocNameReference: Conforms; + JSDocNonNullableType: Conforms; + JSDocNullableType: Conforms; + JSDocOptionalType: Conforms; + JSDocOverloadTag: Conforms; + JSDocOverrideTag: Conforms; + JSDocParameterTag: Conforms; + JSDocPrivateTag: Conforms; + JSDocPropertyTag: Conforms; + JSDocProtectedTag: Conforms; + JSDocPublicTag: Conforms; + JSDocReadonlyTag: Conforms; + JSDocReturnTag: Conforms; + JSDocSatisfiesTag: Conforms; + JSDocSeeTag: Conforms; + JSDocSignature: Conforms; + JSDocTagBase: Conforms; + JSDocTemplateTag: Conforms; + JSDocText: Conforms; + JSDocThisTag: Conforms; + JSDocThrowsTag: Conforms; + JSDocTypeBase: Conforms; + JSDocTypeExpression: Conforms; + JSDocTypeLiteral: Conforms< + Without, + Without + >; + JSDocTypeTag: Conforms; + JSDocTypedefTag: Conforms; + JSDocUnknownTag: Conforms; + JSDocVariadicType: Conforms; + JsxAttribute: Conforms; + JsxAttributes: Conforms; + JsxClosingElement: Conforms; + JsxClosingFragment: Conforms; + JsxElement: Conforms; + JsxExpression: Conforms; + JsxFragment: Conforms; + JsxNamespacedName: Conforms; + JsxOpeningElement: Conforms; + JsxOpeningFragment: Conforms; + JsxSelfClosingElement: Conforms; + JsxSpreadAttribute: Conforms; + JsxText: Conforms; + KeywordExpression: Conforms; + KeywordTypeNode: Conforms; + LabeledStatement: Conforms; + LeftHandSideExpressionBase: Conforms< + ast.LeftHandSideExpressionBase, + api.LeftHandSideExpressionBase + >; + LiteralExpressionBase: Conforms; + LiteralLikeNodeBase: Conforms; + LiteralTypeNode: Conforms; + MappedTypeNode: Conforms; + MemberExpressionBase: Conforms; + MetaProperty: Conforms; + MethodDeclaration: Conforms; + MethodSignatureDeclaration: Conforms< + ast.MethodSignatureDeclaration, + api.MethodSignatureDeclaration + >; + MissingDeclaration: Conforms; + ModifiersBase: Conforms; + ModuleBlock: Conforms; + ModuleDeclaration: Conforms; + NamedExports: Conforms; + NamedImports: Conforms; + NamedMemberBase: Conforms; + NamedTupleMember: Conforms; + NamespaceExport: Conforms; + NamespaceExportDeclaration: Conforms< + ast.NamespaceExportDeclaration, + api.NamespaceExportDeclaration + >; + NamespaceImport: Conforms; + NewExpression: Conforms; + NoSubstitutionTemplateLiteral: Conforms< + ast.NoSubstitutionTemplateLiteral, + api.NoSubstitutionTemplateLiteral + >; + NodeBase: Conforms; + NodeWithTypeArgumentsBase: Conforms; + NonNullExpression: Conforms; + NotEmittedStatement: Conforms; + NotEmittedTypeElement: Conforms; + NumericLiteral: Conforms; + ObjectBindingPattern: Conforms; + ObjectLiteralElementBase: Conforms; + ObjectLiteralExpression: Conforms; + OmittedExpression: Conforms; + OptionalTypeNode: Conforms; + ParameterDeclaration: Conforms; + ParenthesizedExpression: Conforms; + ParenthesizedTypeNode: Conforms; + PartiallyEmittedExpression: Conforms< + ast.PartiallyEmittedExpression, + api.PartiallyEmittedExpression + >; + PostfixUnaryExpression: Conforms; + PrefixUnaryExpression: Conforms; + PrimaryExpressionBase: Conforms; + PrivateIdentifier: Conforms; + PropertyAccessExpression: Conforms; + PropertyAssignment: Conforms; + PropertyDeclaration: Conforms; + PropertySignatureDeclaration: Conforms< + ast.PropertySignatureDeclaration, + api.PropertySignatureDeclaration + >; + QualifiedName: Conforms; + RegularExpressionLiteral: Conforms; + RestTypeNode: Conforms; + ReturnStatement: Conforms; + SatisfiesExpression: Conforms; + SemicolonClassElement: Conforms; + SetAccessorDeclaration: Conforms; + ShorthandPropertyAssignment: Conforms< + ast.ShorthandPropertyAssignment, + api.ShorthandPropertyAssignment + >; + SpreadAssignment: Conforms; + SpreadElement: Conforms; + StatementBase: Conforms; + StringLiteral: Conforms; + SwitchStatement: Conforms; + SyntaxList: Conforms, Without>; + SyntheticExpression: Conforms< + Without, + Without + >; + SyntheticReferenceExpression: Conforms< + ast.SyntheticReferenceExpression, + api.SyntheticReferenceExpression + >; + TaggedTemplateExpression: Conforms; + TemplateExpression: Conforms; + TemplateHead: Conforms; + TemplateLiteralLikeNodeBase: Conforms< + ast.TemplateLiteralLikeNodeBase, + api.TemplateLiteralLikeNodeBase + >; + TemplateLiteralTypeNode: Conforms; + TemplateLiteralTypeSpan: Conforms; + TemplateMiddle: Conforms; + TemplateSpan: Conforms; + TemplateTail: Conforms; + ThisTypeNode: Conforms; + ThrowStatement: Conforms; + Token: Conforms; + TryStatement: Conforms; + TupleTypeNode: Conforms; + TypeAliasDeclaration: Conforms; + TypeAssertion: Conforms; + TypeElementBase: Conforms; + TypeLiteralNode: Conforms; + TypeNodeBase: Conforms; + TypeOfExpression: Conforms; + TypeOperatorNode: Conforms; + TypeParameterDeclaration: Conforms; + TypePredicateNode: Conforms; + TypeQueryNode: Conforms; + TypeReferenceNode: Conforms; + UnaryExpressionBase: Conforms; + UnionOrIntersectionTypeNodeBase: Conforms< + ast.UnionOrIntersectionTypeNodeBase, + api.UnionOrIntersectionTypeNodeBase + >; + UnionTypeNode: Conforms; + UpdateExpressionBase: Conforms; + VariableDeclaration: Conforms; + VariableDeclarationList: Conforms; + VariableStatement: Conforms; + VoidExpression: Conforms; + WhileStatement: Conforms; + WithStatement: Conforms; + YieldExpression: Conforms; +} diff --git a/packages/compiler/src/frontend/ts7/ast-guards.generated.ts b/packages/compiler/src/frontend/ts7/ast-guards.generated.ts index aeaf56e9a..d163ac18d 100644 --- a/packages/compiler/src/frontend/ts7/ast-guards.generated.ts +++ b/packages/compiler/src/frontend/ts7/ast-guards.generated.ts @@ -4,6 +4,7 @@ import type { AbstractKeyword, AccessExpression, AccessorDeclaration, AccessorKeyword, AdditiveOperator, AdditiveOperatorOrHigher, AnyImportSyntax, ArrayBindingElement, ArrayBindingPattern, ArrayDestructuringAssignment, ArrayLiteralExpression, ArrayTypeNode, ArrowFunction, AsExpression, AssertKeyword, AssertionExpression, AssertsKeyword, AssignmentOperator, AssignmentOperatorOrHigher, AssignmentOperatorToken, AsteriskToken, AsyncKeyword, AwaitExpression, AwaitKeyword, BigIntLiteral, BinaryExpression, BinaryOperator, BinaryOperatorToken, BindingElement, BindingName, BindingPattern, BitwiseOperator, BitwiseOperatorOrHigher, Block, BlockOrExpression, BooleanLiteral, BreakOrContinueStatement, BreakStatement, CallExpression, CallLikeExpression, CallOrNewExpression, CallSignatureDeclaration, CaseBlock, CaseClause, CaseKeyword, CatchClause, ClassDeclaration, ClassExpression, ClassLikeDeclaration, ClassStaticBlockDeclaration, ColonToken, CompoundAssignmentOperator, ComputedPropertyName, ConciseBody, ConditionalExpression, ConditionalTypeNode, ConstKeyword, ConstructSignatureDeclaration, ConstructorDeclaration, ConstructorTypeNode, ContinueStatement, DebuggerStatement, DeclarationName, DeclareKeyword, Decorator, DefaultClause, DefaultKeyword, DeleteExpression, DestructuringAssignment, DoStatement, DotDotDotToken, DotToken, ElementAccessExpression, EmptyStatement, EndOfFile, EntityName, EnumDeclaration, EnumMember, EqualityOperator, EqualityOperatorOrHigher, EqualsGreaterThanToken, EqualsToken, ExclamationToken, ExponentiationOperator, ExportAssignment, ExportDeclaration, ExportKeyword, ExportSpecifier, Expression, ExpressionStatement, ExpressionWithTypeArguments, ExternalModuleReference, FalseLiteral, ForInStatement, ForInitializer, ForOfStatement, ForStatement, FunctionBody, FunctionDeclaration, FunctionExpression, FunctionLikeDeclaration, FunctionTypeNode, GetAccessorDeclaration, HeritageClause, Identifier, IfStatement, ImportAttribute, ImportAttributeName, ImportAttributes, ImportClause, ImportClauseOrBindingPattern, ImportDeclaration, ImportEqualsDeclaration, ImportExpression, ImportPhaseModifierSyntaxKind, ImportSpecifier, ImportTypeNode, InKeyword, IndexSignatureDeclaration, IndexedAccessTypeNode, InferTypeNode, InterfaceDeclaration, IntersectionTypeNode, JSDoc, JSDocAllType, JSDocAugmentsTag, JSDocCallbackTag, JSDocComment, JSDocDeprecatedTag, JSDocFullName, JSDocImplementsTag, JSDocImportTag, JSDocLink, JSDocLinkCode, JSDocLinkPlain, JSDocNameReference, JSDocNonNullableType, JSDocNullableType, JSDocOptionalType, JSDocOverloadTag, JSDocOverrideTag, JSDocParameterTag, JSDocPrivateTag, JSDocPropertyTag, JSDocProtectedTag, JSDocPublicTag, JSDocReadonlyTag, JSDocReturnTag, JSDocSatisfiesTag, JSDocSeeTag, JSDocSignature, JSDocTemplateTag, JSDocText, JSDocThisTag, JSDocThrowsTag, JSDocTypeExpression, JSDocTypeLiteral, JSDocTypeTag, JSDocTypedefTag, JSDocUnknownTag, JSDocVariadicType, JsxAttribute, JsxAttributeLike, JsxAttributeName, JsxAttributeValue, JsxAttributes, JsxChild, JsxClosingElement, JsxClosingFragment, JsxElement, JsxExpression, JsxFragment, JsxNamespacedName, JsxOpeningElement, JsxOpeningFragment, JsxOpeningLikeElement, JsxSelfClosingElement, JsxSpreadAttribute, JsxTagNameExpression, JsxText, JsxTokenSyntaxKind, KeywordExpression, KeywordExpressionSyntaxKind, KeywordTypeNode, KeywordTypeSyntaxKind, LabeledStatement, LeftHandSideExpression, LiteralExpression, LiteralLikeNode, LiteralToken, LiteralTypeNode, LogicalOperator, LogicalOperatorOrHigher, LogicalOrCoalescingAssignmentOperator, MappedTypeNode, MemberName, MetaProperty, MethodDeclaration, MethodSignatureDeclaration, MinusToken, MissingDeclaration, Modifier, ModifierLike, ModifierSyntaxKind, ModuleBlock, ModuleBody, ModuleDeclaration, ModuleExportName, ModuleName, ModuleReference, MultiplicativeOperator, MultiplicativeOperatorOrHigher, NamedExportBindings, NamedExports, NamedImportBindings, NamedImports, NamedImportsOrExports, NamedTupleMember, NamespaceExport, NamespaceExportDeclaration, NamespaceImport, NewExpression, NoSubstitutionTemplateLiteral, Node, NonNullExpression, NotEmittedStatement, NotEmittedTypeElement, NullLiteral, NumericLiteral, NumericOrStringLikeLiteral, ObjectBindingPattern, ObjectDestructuringAssignment, ObjectLiteralElementLike, ObjectLiteralExpression, ObjectLiteralLikeNode, ObjectTypeDeclaration, OmittedExpression, OptionalTypeNode, OutKeyword, OverrideKeyword, ParameterDeclaration, ParenthesizedExpression, ParenthesizedTypeNode, PartiallyEmittedExpression, PlusToken, PostfixUnaryExpression, PostfixUnaryOperator, PrefixUnaryExpression, PrefixUnaryOperator, PrivateIdentifier, PrivateKeyword, PropertyAccessExpression, PropertyAssignment, PropertyDeclaration, PropertyName, PropertyNameLiteral, PropertySignatureDeclaration, ProtectedKeyword, PseudoLiteralSyntaxKind, PseudoLiteralToken, PublicKeyword, QualifiedName, QuestionDotToken, QuestionToken, ReadonlyKeyword, RegularExpressionLiteral, RelationalOperator, RelationalOperatorOrHigher, RestTypeNode, ReturnStatement, SatisfiesExpression, SemicolonClassElement, SetAccessorDeclaration, ShiftOperator, ShiftOperatorOrHigher, ShorthandPropertyAssignment, SignatureDeclaration, SourceFile, SpreadAssignment, SpreadElement, Statement, StaticKeyword, StringLiteral, StringLiteralLikeNode, SuperExpression, SwitchStatement, SyntaxList, SyntheticExpression, SyntheticReferenceExpression, TaggedTemplateExpression, TemplateExpression, TemplateHead, TemplateLiteral, TemplateLiteralLikeNode, TemplateLiteralToken, TemplateLiteralTypeNode, TemplateLiteralTypeSpan, TemplateMiddle, TemplateMiddleOrTail, TemplateSpan, TemplateTail, ThisExpression, ThisTypeNode, ThrowStatement, Token, TriviaSyntaxKind, TrueLiteral, TryStatement, TupleTypeNode, TypeAliasDeclaration, TypeAssertion, TypeLiteralNode, TypeNode, TypeOfExpression, TypeOperatorNode, TypeParameterDeclaration, TypePredicateNode, TypePredicateParameterName, TypeQueryNode, TypeReferenceNode, UnaryExpressionBase, UnionOrIntersectionTypeNode, UnionTypeNode, VariableDeclaration, VariableDeclarationList, VariableOrParameterDeclaration, VariableOrPropertyDeclaration, VariableStatement, VoidExpression, WhileStatement, WithStatement, YieldExpression } from "./ast-types.js"; import { SyntaxKind, NodeFlags, ScriptKind, OuterExpressionKinds } from "./enums.js"; +import { getSourceFileOfNode } from "./node-functions.js"; export function isToken(node: Node | undefined): node is Token { if (node === undefined) return false; @@ -1913,7 +1914,7 @@ export function skipOuterExpressions(node: Node, kinds: OuterExpressionKinds = O function isJSDocTypeAssertion(node: Node | undefined): boolean { if (node === undefined) return false; - const sourceFile = node.getSourceFile(); + const sourceFile = getSourceFileOfNode(node); if (sourceFile.scriptKind !== ScriptKind.JS && sourceFile.scriptKind !== ScriptKind.JSX) { return false; } diff --git a/packages/compiler/src/frontend/ts7/ast-node.test.ts b/packages/compiler/src/frontend/ts7/ast-node.test.ts index e2440b787..b9b8f0e7b 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.test.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.test.ts @@ -20,7 +20,13 @@ import { walkPreorder } from "./ast.js"; import { ts7Executable } from "./rpc-api.js"; import { Ts7RpcClient } from "./rpc-client.js"; import { spawnTs7Wire } from "./rpc-process.js"; -import { isJSDocTypeLiteral, isJSDocPropertyTag } from "./ast-guards.generated.js"; +import { + isArrayLiteralExpression, + isCallExpression, + isJSDocTypeLiteral, + isJSDocPropertyTag, +} from "./ast-guards.generated.js"; +import { ModifierFlags, SyntaxKind } from "./enums.js"; const require = createRequire(import.meta.url); const sdkRoot = dirname(require.resolve("typescript/package.json")); @@ -95,6 +101,8 @@ const cases: Record = { '/** @typedef {Object} Options\n * @property {string} name\n * @property {number} count\n */\n/** @type {Options} */ const options = { name: "x", count: 2 };', "missing.ts": "const incomplete = ;\nfunction f( {\n", "empty.ts": "", + "lists.ts": + "const trailing = [1, 2 /* last */, ];\nconst plain = [1, 2];\nf(1, 2,);\nexport abstract class Flags { protected static readonly value = 1; }\n", }; const decoded = new Map(); let directory: string; @@ -159,6 +167,26 @@ function ids( return nodes?.map((n) => (n as OracleNode).index); } +test("node lists report a trailing comma and modifier lists their flags", () => { + const { file } = decoded.get("lists.ts")!; + const lists: [SyntaxKind, boolean][] = []; + const modifiers: number[] = []; + walkPreorder(file.root, (node) => { + if (isArrayLiteralExpression(node)) lists.push([node.kind, node.elements.hasTrailingComma()]); + if (isCallExpression(node)) lists.push([node.kind, node.arguments.hasTrailingComma()]); + if (node.modifiers !== undefined) modifiers.push(node.modifiers.modifierFlags); + }); + expect(lists).toEqual([ + [SyntaxKind.ArrayLiteralExpression, true], + [SyntaxKind.ArrayLiteralExpression, false], + [SyntaxKind.CallExpression, true], + ]); + expect(modifiers).toEqual([ + ModifierFlags.Export | ModifierFlags.Abstract, + ModifierFlags.Protected | ModifierFlags.Static | ModifierFlags.Readonly, + ]); +}); + test("JSDoc property refinements describe the pinned native wire", () => { const { file, oracle } = decoded.get("properties.js")!; let checked = 0; diff --git a/packages/compiler/src/frontend/ts7/ast-node.ts b/packages/compiler/src/frontend/ts7/ast-node.ts index 3760fff8d..8eaa87c72 100644 --- a/packages/compiler/src/frontend/ts7/ast-node.ts +++ b/packages/compiler/src/frontend/ts7/ast-node.ts @@ -35,6 +35,25 @@ export class AstNodeList { get end(): number { return this.wire.end(this.index) >>> 0; } + + /** Whether a comma follows the last node before the list's end. */ + hasTrailingComma(): boolean { + const last = this.nodes[this.nodes.length - 1]; + if (last === undefined) return false; + const text = last.getSourceFile().text; + const next = astSkipTrivia(text, last.end, false, false); + return next < this.end && text.charCodeAt(next) === 44; + } +} + +/** A declaration's modifiers, with the flags of all of them: tsc-ts's + * ModifierList. */ +export class AstModifierList extends AstNodeList { + get modifierFlags(): number { + let result = 0; + for (const modifier of this.nodes) result |= astModifierFlag(modifier.kind); + return result; + } } /** One immutable AST response owns one object per node and one array per @@ -85,6 +104,12 @@ export class AstFile { } list(index: number): AstNodeList { + return new AstNodeList(this.wire, index, this.listNodes(index)); + } + modifierList(index: number): AstModifierList { + return new AstModifierList(this.wire, index, this.listNodes(index)); + } + private listNodes(index: number): AstNode[] { let nodes = this.lists[index]; if (nodes === undefined) { nodes = []; @@ -92,7 +117,7 @@ export class AstFile { this.lists[index] = nodes; this.materialized?.(); } - return new AstNodeList(this.wire, index, nodes); + return nodes; } namedChild(node: AstNode, name: string): number { @@ -422,6 +447,11 @@ export class AstNode { return result; } + get modifiers(): AstModifierList | undefined { + const index = this.file.namedChild(this, "modifiers"); + return index === 0 ? undefined : this.file.modifierList(index); + } + get body(): AstNode | undefined { const body = this.childNode("body"); // TS7 can serialize a reparsed @type callable before a method's @@ -599,9 +629,6 @@ export class AstNode { get members(): AstNodeList | undefined { return this.childList("members"); } - get modifiers(): AstNodeList | undefined { - return this.childList("modifiers"); - } get moduleReference(): AstNode | undefined { return this.childNode("moduleReference"); } diff --git a/packages/compiler/src/frontend/ts7/ast-types.generated.ts b/packages/compiler/src/frontend/ts7/ast-types.generated.ts index de6ff892f..59f01e9ea 100644 --- a/packages/compiler/src/frontend/ts7/ast-types.generated.ts +++ b/packages/compiler/src/frontend/ts7/ast-types.generated.ts @@ -3,7 +3,7 @@ // Regenerate when changing the TypeScript pin; do not edit by hand. import type { AstNode } from "./ast-node.js"; -import type { JsxTagNamePropertyAccess, NodeArray } from "./ast-types.js"; +import type { JsxTagNamePropertyAccess, ModifierArray, NodeArray } from "./ast-types.js"; import { SyntaxKind, NodeFlags, ModifierFlags, TokenFlags } from "./enums.js"; type Node = AstNode; @@ -142,7 +142,7 @@ export interface DeclarationBase extends Node { } export interface ModifiersBase extends Node { - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.VariableStatement | SyntaxKind.Parameter | SyntaxKind.MissingDeclaration | SyntaxKind.FunctionDeclaration | SyntaxKind.ClassDeclaration | SyntaxKind.ClassExpression | SyntaxKind.InterfaceDeclaration | SyntaxKind.TypeAliasDeclaration | SyntaxKind.EnumMember | SyntaxKind.EnumDeclaration | SyntaxKind.ImportDeclaration | SyntaxKind.ExportAssignment | SyntaxKind.NamespaceExportDeclaration | SyntaxKind.Constructor | SyntaxKind.GetAccessor | SyntaxKind.SetAccessor | SyntaxKind.IndexSignature | SyntaxKind.MethodSignature | SyntaxKind.MethodDeclaration | SyntaxKind.PropertySignature | SyntaxKind.PropertyDeclaration | SyntaxKind.ClassStaticBlockDeclaration | SyntaxKind.BinaryExpression | SyntaxKind.ArrowFunction | SyntaxKind.FunctionExpression | SyntaxKind.PropertyAssignment | SyntaxKind.ShorthandPropertyAssignment | SyntaxKind.FunctionType | SyntaxKind.ConstructorType | SyntaxKind.ModuleDeclaration | SyntaxKind.ImportEqualsDeclaration | SyntaxKind.ExportDeclaration | SyntaxKind.TypeParameter; } @@ -170,7 +170,7 @@ export interface FunctionLikeWithBodyBase extends Node { } export interface ClassLikeBase extends Node { - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: Identifier | undefined; readonly typeParameters: NodeArray | undefined; @@ -209,7 +209,7 @@ export interface ClassElementBase extends Node { } export interface NamedMemberBase extends Node { - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -397,7 +397,7 @@ export interface Block extends Node { export interface VariableStatement extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.VariableStatement; readonly declarationList: VariableDeclarationList; @@ -420,7 +420,7 @@ export interface VariableDeclarationList extends Node { export interface ParameterDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.Parameter; readonly dotDotDotToken: DotDotDotToken | undefined; @@ -441,14 +441,14 @@ export interface BindingElement extends Node { export interface MissingDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.MissingDeclaration; } export interface FunctionDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly typeParameters: NodeArray | undefined; readonly parameters: NodeArray; @@ -461,7 +461,7 @@ export interface FunctionDeclaration extends Node { export interface ClassDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: Identifier | undefined; readonly typeParameters: NodeArray | undefined; @@ -472,7 +472,7 @@ export interface ClassDeclaration extends Node { export interface ClassExpression extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: Identifier | undefined; readonly typeParameters: NodeArray | undefined; @@ -490,7 +490,7 @@ export interface HeritageClause extends Node { export interface InterfaceDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.InterfaceDeclaration; readonly name: Identifier; @@ -501,7 +501,7 @@ export interface InterfaceDeclaration extends Node { export interface TypeAliasDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.TypeAliasDeclaration; readonly name: Identifier; @@ -511,7 +511,7 @@ export interface TypeAliasDeclaration extends Node { export interface EnumMember extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -521,7 +521,7 @@ export interface EnumMember extends Node { export interface EnumDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.EnumDeclaration; readonly name: Identifier; @@ -546,7 +546,7 @@ export interface NotEmittedTypeElement extends Node { export interface ImportDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.ImportDeclaration; readonly importClause: ImportClause | undefined; @@ -574,7 +574,7 @@ export interface NamedImports extends Node { export interface ExportAssignment extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.ExportAssignment; readonly isExportEquals: boolean; @@ -584,7 +584,7 @@ export interface ExportAssignment extends Node { export interface NamespaceExportDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.NamespaceExportDeclaration; readonly name: Identifier; @@ -628,7 +628,7 @@ export interface ConstructSignatureDeclaration extends Node { export interface ConstructorDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly typeParameters: NodeArray | undefined; readonly parameters: NodeArray; @@ -639,7 +639,7 @@ export interface ConstructorDeclaration extends Node { } export interface GetAccessorDeclaration extends Node { - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -653,7 +653,7 @@ export interface GetAccessorDeclaration extends Node { } export interface SetAccessorDeclaration extends Node { - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -668,7 +668,7 @@ export interface SetAccessorDeclaration extends Node { export interface IndexSignatureDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly typeParameters: NodeArray | undefined; readonly parameters: NodeArray; @@ -678,7 +678,7 @@ export interface IndexSignatureDeclaration extends Node { export interface MethodSignatureDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -690,7 +690,7 @@ export interface MethodSignatureDeclaration extends Node { export interface MethodDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -704,7 +704,7 @@ export interface MethodDeclaration extends Node { export interface PropertySignatureDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -715,7 +715,7 @@ export interface PropertySignatureDeclaration extends Node { export interface PropertyDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -731,7 +731,7 @@ export interface SemicolonClassElement extends Node { export interface ClassStaticBlockDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.ClassStaticBlockDeclaration; readonly body: Block; @@ -786,7 +786,7 @@ export interface NoSubstitutionTemplateLiteral extends Node { export interface BinaryExpression extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.BinaryExpression; readonly left: Expression; @@ -818,7 +818,7 @@ export interface YieldExpression extends Node { export interface ArrowFunction extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly typeParameters: NodeArray | undefined; readonly parameters: NodeArray; @@ -831,7 +831,7 @@ export interface ArrowFunction extends Node { export interface FunctionExpression extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly typeParameters: NodeArray | undefined; readonly parameters: NodeArray; @@ -969,7 +969,7 @@ export interface SpreadAssignment extends Node { export interface PropertyAssignment extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -980,7 +980,7 @@ export interface PropertyAssignment extends Node { export interface ShorthandPropertyAssignment extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly name: PropertyName; readonly postfixToken: QuestionToken | ExclamationToken | undefined; @@ -1180,7 +1180,7 @@ export interface ParenthesizedTypeNode extends Node { export interface FunctionTypeNode extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly typeParameters: NodeArray | undefined; readonly parameters: NodeArray; @@ -1190,7 +1190,7 @@ export interface FunctionTypeNode extends Node { export interface ConstructorTypeNode extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly typeParameters: NodeArray | undefined; readonly parameters: NodeArray; @@ -1562,7 +1562,7 @@ export interface JSDocNameReference extends Node { export interface ModuleDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly asteriskToken: AsteriskToken | undefined; readonly body: ModuleBody | undefined; @@ -1573,7 +1573,7 @@ export interface ModuleDeclaration extends Node { export interface ImportEqualsDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.ImportEqualsDeclaration; readonly isTypeOnly: boolean; @@ -1583,7 +1583,7 @@ export interface ImportEqualsDeclaration extends Node { export interface ExportDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.ExportDeclaration; readonly isTypeOnly: boolean; @@ -1647,7 +1647,7 @@ export interface JSDocLinkCode extends Node { export interface TypeParameterDeclaration extends Node { readonly flags: NodeFlags; - readonly modifiers: NodeArray | undefined; + readonly modifiers: ModifierArray | undefined; readonly modifierFlags: ModifierFlags; readonly kind: SyntaxKind.TypeParameter; readonly name: Identifier; @@ -1842,7 +1842,7 @@ export interface CaseClause extends Node { export interface DefaultClause extends Node { readonly flags: NodeFlags; readonly kind: SyntaxKind.DefaultClause; - readonly expression: Expression; + readonly expression: undefined; readonly statements: NodeArray; } diff --git a/packages/compiler/src/frontend/ts7/ast-types.ts b/packages/compiler/src/frontend/ts7/ast-types.ts index 6d421e58d..e40e40e30 100644 --- a/packages/compiler/src/frontend/ts7/ast-types.ts +++ b/packages/compiler/src/frontend/ts7/ast-types.ts @@ -1,4 +1,4 @@ -import type { AstNode, AstNodeList } from "./ast-node.js"; +import type { AstModifierList, AstNode, AstNodeList } from "./ast-node.js"; import { SyntaxKind } from "./enums.js"; import type { EndOfFile, @@ -14,8 +14,11 @@ export type Node = AstNode; /** A node list. The type is the list class itself, not a structural shape: * native code represents a class and a record type differently. */ export type NodeArray = AstNodeList; +/** A declaration's modifiers: a node list with the flags of all of them. */ +export type ModifierArray = AstModifierList; export type Path = string; -export type __String = string; +/** An escaped symbol name, the key of member and export tables (TypeScript 7's `__String`). */ +export type __String = string & { readonly __escapedIdentifier: void }; export interface TextRange { pos: number; end: number; diff --git a/packages/compiler/src/frontend/ts7/ast.ts b/packages/compiler/src/frontend/ts7/ast.ts index ec26aa0b0..a92cfafd8 100644 --- a/packages/compiler/src/frontend/ts7/ast.ts +++ b/packages/compiler/src/frontend/ts7/ast.ts @@ -53,6 +53,7 @@ import { ModifierFlags, SyntaxKind } from "./enums.js"; // The frontend's node types, guards and token spellings share the concrete // AstNode layout. Parser and transform factories remain outside this client. export * from "./ast-types.js"; +export * from "./node-functions.js"; export * from "./ast-guards.generated.js"; export { tokenToString } from "./ast-tokens.generated.js"; diff --git a/packages/compiler/src/frontend/ts7/checker.test.ts b/packages/compiler/src/frontend/ts7/checker.test.ts index 7b3647b2d..ac062b283 100644 --- a/packages/compiler/src/frontend/ts7/checker.test.ts +++ b/packages/compiler/src/frontend/ts7/checker.test.ts @@ -10,10 +10,10 @@ import { ts7Executable } from "./rpc-api.js"; import { Ts7Session } from "./session.js"; import { SemanticChecker } from "./semantic-checker.js"; import { SemanticSnapshot } from "./semantic-model.js"; -import { TypeFlags } from "./enums.js"; +import { SignatureKind, SymbolFlags, TypeFlags } from "./enums.js"; import { createProgram, Ts7Host } from "./program-adapter.js"; import type { AstNode } from "./ast-node.js"; -import type { SourceFile } from "./ast-types.js"; +import type { Identifier, SourceFile } from "./ast-types.js"; const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; const protocolPath = (path: string) => path.replaceAll("\\", "/"); @@ -167,6 +167,12 @@ test("all public query and prefetch entry points reject a disposed facade before const calls: Record unknown> = { getCallSignatures: () => facade.getCallSignatures(type), getConstructSignatures: () => facade.getConstructSignatures(type), + getSignaturesOfType: () => facade.getSignaturesOfType(type, SignatureKind.Call), + resolveName: () => facade.resolveName("value", SymbolFlags.Value, name), + getResolvedSymbol: () => facade.getResolvedSymbol(name as Identifier), + getAnyType: () => facade.getAnyType(), + getNeverType: () => facade.getNeverType(), + getBigIntType: () => facade.getBigIntType(), getPropertyOfType: () => facade.getPropertyOfType(type, "missing"), prefetchSourceFile: () => facade.prefetchSourceFile(root), prefetchSourceFileStructures: () => facade.prefetchSourceFileStructures([]), diff --git a/packages/compiler/src/frontend/ts7/checker.ts b/packages/compiler/src/frontend/ts7/checker.ts index 41eb0d0b9..4f2d60294 100644 --- a/packages/compiler/src/frontend/ts7/checker.ts +++ b/packages/compiler/src/frontend/ts7/checker.ts @@ -35,7 +35,7 @@ import { CheckerCache } from "./checker-cache.js"; * repeats an IPC request. All paths are verified against the raw checker * and against 5.9.3 by the adapter's suites. */ -import type { Node, SourceFile } from "./ast-types.js"; +import type { Identifier, Node, SourceFile } from "./ast-types.js"; import type { IndexInfo, InterfaceType, @@ -46,9 +46,10 @@ import type { TypeReference, } from "./semantic-types.js"; import type { SemanticChecker as Checker } from "./semantic-checker.js"; -import type { SemanticProject as Project } from "./semantic-model.js"; +import { symbolNameOf, type SemanticProject as Project } from "./semantic-model.js"; import { isTypeNode, walkPreorder } from "./ast.js"; -import { SignatureKind, SyntaxKind, TypeFlags } from "./enums.js"; +import { SignatureKind, SyntaxKind, TypeFlags, type SymbolFlags } from "./enums.js"; +import { getSourceFileOfNode } from "./node-functions.js"; /** Array-overload chunk size: large enough that per-request overhead * vanishes, small enough to keep any single JSON-RPC payload modest. */ @@ -259,6 +260,14 @@ export class CheckerFacade { return sigs; } + /** The call or construct signatures of `type`, as getCallSignatures and getConstructSignatures + * answer them. */ + getSignaturesOfType(type: Type, kind: SignatureKind): readonly Signature[] { + return kind === SignatureKind.Call + ? this.getCallSignatures(type) + : this.getConstructSignatures(type); + } + /** 5.9.3's type.getProperty(name). */ getPropertyOfType(type: Type, name: string): Ts7Symbol | undefined { this.ensureActive(); @@ -370,7 +379,7 @@ export class CheckerFacade { private markManaged(roots: readonly Node[]): void { for (const root of roots) { - const sf = root.getSourceFile(); + const sf = getSourceFileOfNode(root); this.cache.managedTypes.add(sf); this.cache.managedSymbols.add(sf); } @@ -447,7 +456,7 @@ export class CheckerFacade { private autoPrefetch(node: Node, kind: "types" | "symbols"): void { if (this.options.autoPrefetch === false) return; - const sf = node.getSourceFile(); + const sf = getSourceFileOfNode(node); // A referenced declaration rarely needs the rest of its library's // metadata. Keep those queries direct; explicit whole-file prefetch // remains available to consumers that do need the complete surface. @@ -786,6 +795,38 @@ export class CheckerFacade { return this.raw.getTypeOfSymbolAtLocation(symbol, location); } + resolveName( + name: string, + meaning: SymbolFlags, + location?: Node, + excludeGlobals?: boolean, + ): Ts7Symbol | undefined { + this.ensureActive(); + return this.raw.resolveName(name, meaning, location, excludeGlobals); + } + + /** The value `node` names where it is: resolveName of its text with SymbolFlags.Value and + * SymbolFlags.ExportValue. */ + getResolvedSymbol(node: Identifier): Ts7Symbol | undefined { + this.ensureActive(); + return this.raw.getResolvedSymbol(node); + } + + getAnyType(): Type { + this.ensureActive(); + return this.anyType(); + } + + getNeverType(): Type { + this.ensureActive(); + return this.intrinsic("never", () => this.raw.getNeverType()); + } + + getBigIntType(): Type { + this.ensureActive(); + return this.intrinsic("bigint", () => this.raw.getBigIntType()); + } + getUnknownType(): Type { this.ensureActive(); if (this.cache.unknownType === null) this.cache.unknownType = this.raw.getUnknownType(); @@ -851,7 +892,7 @@ export class CheckerFacade { * programs see the es2025 lib's Promise (the ambient world forces it). */ private promiseArgumentOf(type: Type): Type | null { if (!type.isTypeReference()) return null; - const name = type.getTarget()?.getSymbol()?.name; + const name = symbolNameOf(type.getTarget()?.getSymbol()); if (name !== "Promise" && name !== "PromiseLike") return null; const args = this.getTypeArguments(type); return args[0] ?? null; diff --git a/packages/compiler/src/frontend/ts7/diagnostic.ts b/packages/compiler/src/frontend/ts7/diagnostic.ts new file mode 100644 index 000000000..db3ff0e63 --- /dev/null +++ b/packages/compiler/src/frontend/ts7/diagnostic.ts @@ -0,0 +1,38 @@ +import type { DiagnosticCategory } from "./enums.generated.js"; +import type { Ts7DiagnosticData } from "./session-schema.generated.js"; + +/** A diagnostic with every member set, as tsc-ts/api's Diagnostic has them. The server leaves out + * the members a diagnostic doesn't have. `fileName` is undefined for global and program + * diagnostics. */ +export interface Ts7Diagnostic { + readonly fileName: string | undefined; + readonly pos: number; + readonly end: number; + readonly code: number; + readonly category: DiagnosticCategory; + readonly text: string; + readonly reportsUnnecessary: boolean; + readonly reportsDeprecated: boolean; + readonly messageChain: readonly Ts7Diagnostic[]; + readonly relatedInformation: readonly Ts7Diagnostic[]; +} + +/** `data` with its absent members set: undefined, false, or no chained or related diagnostics. */ +export function ts7Diagnostic(data: Ts7DiagnosticData): Ts7Diagnostic { + const chain: Ts7Diagnostic[] = []; + for (const item of data.messageChain ?? []) chain.push(ts7Diagnostic(item)); + const related: Ts7Diagnostic[] = []; + for (const item of data.relatedInformation ?? []) related.push(ts7Diagnostic(item)); + return { + fileName: data.fileName, + pos: data.pos, + end: data.end, + code: data.code, + category: data.category, + text: data.text, + reportsUnnecessary: data.reportsUnnecessary ?? false, + reportsDeprecated: data.reportsDeprecated ?? false, + messageChain: chain, + relatedInformation: related, + }; +} diff --git a/packages/compiler/src/frontend/ts7/node-functions.ts b/packages/compiler/src/frontend/ts7/node-functions.ts new file mode 100644 index 000000000..3b37779e8 --- /dev/null +++ b/packages/compiler/src/frontend/ts7/node-functions.ts @@ -0,0 +1,23 @@ +/* Position and text functions over nodes, with tsc-ts/api's names. The + * frontend calls these rather than node methods, so that its node types can + * be tsc-ts/api's interfaces, which have no methods. Today every node is an + * AstNode, and each function forwards to the AstNode method. */ + +import type { AstNode } from "./ast-node.js"; +import type { Node, SourceFile } from "./ast-types.js"; + +/** The start of the node's first token, after its leading trivia, and before + * its JSDoc comments when `includeJSDoc` is true. */ +export function tokenStartOfNode(node: Node, includeJSDoc = false): number { + return (node as AstNode).getStart(undefined, includeJSDoc); +} + +/** The node's text from its first token to its end. */ +export function getTextOfNode(node: Node): string { + return (node as AstNode).getText(); +} + +/** The source file that contains the node. */ +export function getSourceFileOfNode(node: Node): SourceFile { + return (node as AstNode).getSourceFile(); +} diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts index 54c647220..928b2bb58 100644 --- a/packages/compiler/src/frontend/ts7/program-host.ts +++ b/packages/compiler/src/frontend/ts7/program-host.ts @@ -21,7 +21,7 @@ import { dirname, join, resolve } from "node:path"; import { Ts7Api } from "./api.js"; import type { Ts7TimingInfo } from "./session-timing.js"; import type { Ts7FileSystem } from "./rpc-filesystem.js"; -import type { CompilerOptions, Diagnostic } from "./semantic-types.js"; +import type { CompilerOptions, Diagnostic, SourceFileMetadata } from "./semantic-types.js"; import type { SourceFile } from "./ast-types.js"; import type { Ts7SessionProject as Project, Ts7SessionSnapshot as Snapshot } from "./session.js"; import { CheckerFacade } from "./checker.js"; @@ -349,10 +349,22 @@ export class Ts7Program { return this.project.program.getSyntacticDiagnostics(sourceFile?.fileName); } + getBindDiagnostics(sourceFile?: SourceFile): readonly Diagnostic[] { + return this.project.program.getBindDiagnostics(sourceFile?.fileName); + } + getSemanticDiagnostics(sourceFile?: SourceFile): readonly Diagnostic[] { return this.project.program.getSemanticDiagnostics(sourceFile?.fileName); } + getSuggestionDiagnostics(sourceFile?: SourceFile): readonly Diagnostic[] { + return this.project.program.getSuggestionDiagnostics(sourceFile?.fileName); + } + + getDeclarationDiagnostics(sourceFile?: SourceFile): readonly Diagnostic[] { + return this.project.program.getDeclarationDiagnostics(sourceFile?.fileName); + } + getGlobalDiagnostics(): readonly Diagnostic[] { return this.project.program.getGlobalDiagnostics(); } @@ -365,6 +377,10 @@ export class Ts7Program { return this.project.program.getConfigFileParsingDiagnostics(); } + getSourceFileMetadata(fileName: string): SourceFileMetadata | undefined { + return this.project.program.getSourceFileMetadata(resolve(fileName)); + } + isSourceFileDefaultLibrary(file: SourceFile): boolean { return this.project.program.isSourceFileDefaultLibrary(file); } diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.test.ts b/packages/compiler/src/frontend/ts7/semantic-checker.test.ts index 5c5f2e2fc..90a7a25cf 100644 --- a/packages/compiler/src/frontend/ts7/semantic-checker.test.ts +++ b/packages/compiler/src/frontend/ts7/semantic-checker.test.ts @@ -10,6 +10,8 @@ import { tsgoExecutable } from "./rpc-api.js"; import { spawnTs7Wire } from "./rpc-process.js"; import { Ts7Session } from "./session.js"; import { SyntaxKind } from "./enums.js"; +import { getSourceFileOfNode } from "./node-functions.js"; +import { forEachChild } from "./ast.js"; const source = [ "export class Box { value = 1; }", @@ -98,15 +100,15 @@ test("synthetic type roots and descendants match the pinned SDK without claiming expect(actual.file.node(1)).toBe(actual); expect(actual.parent).toBeUndefined(); expect(() => actual.file.sourceFile).toThrow("expected a source file root"); - expect(() => actual.getSourceFile()).toThrow("expected a source file root"); - actual.forEachChild((child) => { + expect(() => getSourceFileOfNode(actual)).toThrow("expected a source file root"); + forEachChild(actual, (child) => { expect(child.parent).toBe(actual); }); checked++; } expect(checked).toBe(6); expect(root.file.sourceFile).toBe(root); - expect(root.getSourceFile()).toBe(root); + expect(getSourceFileOfNode(root)).toBe(root); } finally { snapshot.dispose(); sdkSnapshot.dispose(); diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.ts b/packages/compiler/src/frontend/ts7/semantic-checker.ts index 17ca43bf8..92aa77333 100644 --- a/packages/compiler/src/frontend/ts7/semantic-checker.ts +++ b/packages/compiler/src/frontend/ts7/semantic-checker.ts @@ -16,16 +16,32 @@ import { type SemanticQuery, } from "./semantic-model.js"; import { SemanticSymbolFlags } from "./semantic-schema.generated.js"; +import type { TypePredicateKind } from "./enums.generated.js"; -export interface SemanticIndexInfo { - keyType: SemanticType; - valueType: SemanticType; - isReadonly: boolean; - declaration: SemanticNodeHandle | undefined; +/** An index signature of a type. Its declaration resolves on first read: most queries only count a + * type's index infos. */ +export class SemanticIndexInfo { + private declarationNode: AstNode | undefined; + private declarationResolved = false; + + constructor( + readonly keyType: SemanticType, + readonly type: SemanticType, + readonly isReadonly: boolean, + private readonly declarationHandle: SemanticNodeHandle | undefined, + ) {} + + get declaration(): AstNode | undefined { + if (!this.declarationResolved) { + this.declarationNode = this.declarationHandle?.resolve(); + this.declarationResolved = true; + } + return this.declarationNode; + } } export interface SemanticTypePredicate { - kind: number; + kind: TypePredicateKind; parameterIndex: number; parameterName: string | undefined; type: SemanticType | undefined; @@ -382,15 +398,17 @@ export class SemanticChecker { const data = this.project.request("getIndexInfosOfType", { type: type.id, }); - return (data ?? []).map((item) => ({ - keyType: this.project.type(item.keyType), - valueType: this.project.type(item.valueType), - isReadonly: item.isReadonly ?? false, - declaration: - item.declaration === undefined - ? undefined - : new SemanticNodeHandle(item.declaration, this.project), - })); + return (data ?? []).map( + (item) => + new SemanticIndexInfo( + this.project.type(item.keyType), + this.project.type(item.valueType), + item.isReadonly ?? false, + item.declaration === undefined + ? undefined + : new SemanticNodeHandle(item.declaration, this.project), + ), + ); } private getWellKnownSymbols(): WellKnownSymbols { this.project.ensureActive(); diff --git a/packages/compiler/src/frontend/ts7/semantic-model.test.ts b/packages/compiler/src/frontend/ts7/semantic-model.test.ts index e0230e8fe..36a445bc8 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.test.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.test.ts @@ -23,7 +23,13 @@ import { isUnionType, type Type, } from "typescript/unstable/sync"; -import { SemanticNodeHandle, SemanticSnapshot, SemanticType } from "./semantic-model.js"; +import { + SemanticNodeHandle, + SemanticSnapshot, + SemanticType, + symbolNameOf, +} from "./semantic-model.js"; +import type { __String } from "./ast-types.js"; import { SemanticObjectFlags as ObjectFlags, SemanticSignatureFlags as SignatureFlags, @@ -97,7 +103,7 @@ test("absent handles and empty known lists do not issue requests", () => { expect(type.getExtendsType()).toBeUndefined(); expect(type.getBaseType()).toBeUndefined(); expect(type.getConstraint()).toBeUndefined(); - expect(type.getTypes()).toBeUndefined(); + expect(type.getTypes()).toEqual([]); expect(type.getBaseTypes()).toBeUndefined(); expect(type.getTypeParameters()).toEqual([]); expect(type.getOuterTypeParameters()).toEqual([]); @@ -304,8 +310,8 @@ test("symbol tables keep escaped keys, display names, canonical context and cach answers.set("getExportSymbolOfSymbol", symbol(3)); const members = parent.getMembers(); const exports = parent.getExports(); - expect(members.get("___x")?.name).toBe("__x"); - expect(exports.get("__x")?.name).toBe("__x"); + expect(symbolNameOf(members.get("___x" as __String))).toBe("__x"); + expect(symbolNameOf(exports.get("__x" as __String))).toBe("__x"); expect(parent.getMembers()).toBe(members); expect(parent.getExports()).toBe(exports); expect(parent.getParent()).toBe(first.symbol(symbol(2))); diff --git a/packages/compiler/src/frontend/ts7/semantic-model.ts b/packages/compiler/src/frontend/ts7/semantic-model.ts index 4920c96ae..5d1bab78e 100644 --- a/packages/compiler/src/frontend/ts7/semantic-model.ts +++ b/packages/compiler/src/frontend/ts7/semantic-model.ts @@ -4,6 +4,8 @@ import type { SemanticTypeData as TypeResponse, } from "./semantic-schema.generated.js"; import { AstNode } from "./ast-node.js"; +import type { __String } from "./ast-types.js"; +import type * as enums from "./enums.generated.js"; import { parseAstNodeHandle } from "./ast-wire.js"; import type { SemanticChecker, SemanticJsDocTag } from "./semantic-checker.js"; import { parseSemanticJson } from "./semantic-json.js"; @@ -290,9 +292,9 @@ export class SemanticNodeHandle { export class SemanticSymbol { readonly id: number; - readonly escapedName: string; - readonly name: string; - readonly flags: number; + /** The key of the member and export tables the symbol is in; `symbolName` gives its name. */ + readonly escapedName: __String; + readonly flags: enums.SymbolFlags; readonly checkFlags: number; /** The declarations' node handles, as the server sends them. */ readonly declarationHandles: SemanticNodeHandle[]; @@ -302,16 +304,15 @@ export class SemanticSymbol { private valueDeclarationResolved = false; private readonly parent: number | undefined; private readonly exportSymbol: number | undefined; - private membersCache: Map | undefined; - private exportsCache: Map | undefined; + private membersCache: Map<__String, SemanticSymbol> | undefined; + private exportsCache: Map<__String, SemanticSymbol> | undefined; constructor( data: SymbolResponse, readonly canonicalProject: SemanticProject, ) { this.id = data.id; - this.escapedName = data.name; - this.name = data.name.startsWith("___") ? data.name.slice(1) : data.name; + this.escapedName = data.name as __String; this.flags = data.flags; this.checkFlags = data.checkFlags; this.declarationHandles = (data.declarations ?? []).map( @@ -362,18 +363,18 @@ export class SemanticSymbol { if (result === undefined) throw new Error(`Missing export symbol for ${this.id}`); return result; } - private table(method: string): Map { - const table = new Map(); + private table(method: string): Map<__String, SemanticSymbol> { + const table = new Map<__String, SemanticSymbol>(); for (const symbol of this.canonicalProject.fetchSymbols(this.id, method)) table.set(symbol.escapedName, symbol); return table; } - getMembers(): Map { + getMembers(): Map<__String, SemanticSymbol> { this.canonicalProject.ensureActive(); if (this.membersCache === undefined) this.membersCache = this.table("getMembersOfSymbol"); return this.membersCache; } - getExports(): Map { + getExports(): Map<__String, SemanticSymbol> { this.canonicalProject.ensureActive(); if (this.exportsCache === undefined) this.exportsCache = this.table("getExportsOfSymbol"); return this.exportsCache; @@ -392,10 +393,26 @@ export class SemanticSymbol { } } +/** The name of `symbol`, as TypeScript 7's API gives it: its escaped name, with the extra `_` of a + * name that starts with `__` removed. */ +export function symbolName(symbol: SemanticSymbol): string { + const name: string = symbol.escapedName; + return name.startsWith("___") ? name.slice(1) : name; +} + +/** `symbolName(symbol)`, or undefined without a symbol. */ +export function symbolNameOf(symbol: SemanticSymbol | null | undefined): string | undefined { + return symbol === undefined || symbol === null ? undefined : symbolName(symbol); +} + /** Type refinements keep the same native object, including its project and * lazy handle caches. Narrowed fields are read from their original storage. */ -export interface SemanticObjectType extends SemanticType { - readonly objectFlags: number; +export type SemanticObjectType = SemanticType; +export interface SemanticTypeParameter extends SemanticType { + readonly isThisType: boolean; +} +export interface SemanticTemplateLiteralType extends SemanticType { + readonly texts: string[]; } export interface SemanticIntrinsicType extends SemanticType { readonly intrinsicName: string; @@ -416,7 +433,7 @@ export interface SemanticBooleanLiteralType extends SemanticLiteralType { readonly value: boolean; } export interface SemanticTupleType extends SemanticObjectType { - readonly elementFlags: number[]; + readonly elementFlags: enums.ElementFlags[]; readonly fixedLength: number; readonly readonly: boolean; } @@ -424,12 +441,14 @@ export interface SemanticTupleType extends SemanticObjectType { /** Kind-specific accessors share one nominal layout. */ export class SemanticType { readonly id: number; - readonly flags: number; - readonly objectFlags: number | undefined; + readonly flags: enums.TypeFlags; + /** The object flags of an object type; 0 for other types, as TypeScript 7's API gives them. */ + readonly objectFlags: enums.ObjectFlags; readonly value: string | number | boolean | bigint | undefined; readonly intrinsicName: string | undefined; + /** Whether a type parameter is the `this` type; undefined for other types. */ readonly isThisType: boolean | undefined; - readonly elementFlags: number[] | undefined; + readonly elementFlags: enums.ElementFlags[] | undefined; readonly fixedLength: number | undefined; readonly readonly: boolean | undefined; readonly texts: string[] | undefined; @@ -444,7 +463,7 @@ export class SemanticType { ) { this.id = data.id; this.flags = data.flags; - this.objectFlags = data.objectFlags; + this.objectFlags = data.objectFlags ?? 0; const value = data.value; if ((data.flags & TypeFlags.BigIntLiteral) !== 0 && value != null) { if (typeof value !== "string") @@ -454,7 +473,8 @@ export class SemanticType { this.value = value ?? undefined; } this.intrinsicName = data.intrinsicName; - this.isThisType = data.isThisType; + this.isThisType = + (data.flags & TypeFlags.TypeParameter) !== 0 ? (data.isThisType ?? false) : undefined; this.elementFlags = data.elementFlags; this.fixedLength = data.fixedLength; this.readonly = data.readonly; @@ -494,26 +514,26 @@ export class SemanticType { getConstraint(): SemanticType | undefined { return this.project.fetchType(this.id, "getConstraintOfType", this.data.substConstraint); } - getTypeParameters(): SemanticType[] { + getTypeParameters(): SemanticTypeParameter[] { return this.project.fetchTypes( this.id, "getTypeParametersOfType", this.data.typeParameters ?? [], - ); + ) as SemanticTypeParameter[]; } - getOuterTypeParameters(): SemanticType[] { + getOuterTypeParameters(): SemanticTypeParameter[] { return this.project.fetchTypes( this.id, "getOuterTypeParametersOfType", this.data.outerTypeParameters ?? [], - ); + ) as SemanticTypeParameter[]; } - getLocalTypeParameters(): SemanticType[] { + getLocalTypeParameters(): SemanticTypeParameter[] { return this.project.fetchTypes( this.id, "getLocalTypeParametersOfType", this.data.localTypeParameters ?? [], - ); + ) as SemanticTypeParameter[]; } getAliasTypeArguments(): SemanticType[] { return this.project.fetchTypes( @@ -522,10 +542,11 @@ export class SemanticType { this.data.aliasTypeArguments ?? [], ); } - getTypes(): SemanticType[] | undefined { + /** The members of a union or intersection, or the types of a template literal type; empty for + * other types. */ + getTypes(): SemanticType[] { this.project.ensureActive(); - if ((this.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral)) === 0) - return undefined; + if ((this.flags & (TypeFlags.UnionOrIntersection | TypeFlags.TemplateLiteral)) === 0) return []; if (this.constituentsPanic !== undefined) throw this.constituentsPanic; if (this.constituents === undefined) { try { @@ -567,7 +588,7 @@ export class SemanticType { return result; } isClassOrInterface(): boolean { - return this.isObjectType() && ((this.objectFlags ?? 0) & ObjectFlags.ClassOrInterface) !== 0; + return this.isObjectType() && (this.objectFlags & ObjectFlags.ClassOrInterface) !== 0; } isUnionType(): boolean { return (this.flags & TypeFlags.Union) !== 0; @@ -575,7 +596,7 @@ export class SemanticType { isIntersectionType(): boolean { return (this.flags & TypeFlags.Intersection) !== 0; } - isObjectType(): this is SemanticObjectType { + isObjectType(): boolean { return (this.flags & TypeFlags.Object) !== 0; } isIntrinsicType(): this is SemanticIntrinsicType { @@ -600,10 +621,10 @@ export class SemanticType { return (this.flags & TypeFlags.BooleanLiteral) !== 0; } isTypeReference(): boolean { - return this.isObjectType() && ((this.objectFlags ?? 0) & ObjectFlags.Reference) !== 0; + return this.isObjectType() && (this.objectFlags & ObjectFlags.Reference) !== 0; } - isTupleType(): boolean { - return this.isObjectType() && ((this.objectFlags ?? 0) & ObjectFlags.Tuple) !== 0; + isTupleType(): this is SemanticTupleType { + return this.isObjectType() && (this.objectFlags & ObjectFlags.Tuple) !== 0; } isIndexType(): boolean { return (this.flags & TypeFlags.Index) !== 0; @@ -617,20 +638,20 @@ export class SemanticType { isSubstitutionType(): boolean { return (this.flags & TypeFlags.Substitution) !== 0; } - isTemplateLiteralType(): boolean { + isTemplateLiteralType(): this is SemanticTemplateLiteralType { return (this.flags & TypeFlags.TemplateLiteral) !== 0; } isStringMappingType(): boolean { return (this.flags & TypeFlags.StringMapping) !== 0; } - isTypeParameter(): boolean { + isTypeParameter(): this is SemanticTypeParameter { return (this.flags & TypeFlags.TypeParameter) !== 0; } } export class SemanticSignature { readonly id: number; - readonly flags: number; + readonly flags: enums.SignatureFlags; /** The declaration's node handle, as the server sends it. */ readonly declarationHandle: SemanticNodeHandle | undefined; private declarationNode: AstNode | undefined; @@ -658,12 +679,12 @@ export class SemanticSignature { return this.declarationNode; } - getTypeParameters(): SemanticType[] { + getTypeParameters(): SemanticTypeParameter[] { return this.project.fetchTypes( this.id, "getTypeParametersOfSignature", this.data.typeParameters ?? [], - ); + ) as SemanticTypeParameter[]; } getParameters(): SemanticSymbol[] { return this.project.fetchSymbols( diff --git a/packages/compiler/src/frontend/ts7/semantic-types.ts b/packages/compiler/src/frontend/ts7/semantic-types.ts index b4f3d5f2e..ea43e3885 100644 --- a/packages/compiler/src/frontend/ts7/semantic-types.ts +++ b/packages/compiler/src/frontend/ts7/semantic-types.ts @@ -1,27 +1,40 @@ /** Frontend names over the concrete native client. No SDK object or - * structural cast participates in the production checker boundary. */ + * structural cast participates in the production checker boundary. The + * names follow tsc-ts/api's. */ export type { SemanticType as Type, - SemanticType as InterfaceType, - SemanticType as TypeReference, SemanticType as UnionOrIntersectionType, SemanticType as UnionType, + SemanticType as IntersectionType, + SemanticType as FreshableType, + SemanticType as InterfaceType, + SemanticType as TypeReference, SemanticTupleType as TupleType, // A reference carries its element flags on the target, not necessarily on // itself. Claiming a tuple shape here makes a native presence test constant. - SemanticObjectType as TupleTypeReference, + SemanticType as TupleTypeReference, + SemanticLiteralType as LiteralType, SemanticStringLiteralType as StringLiteralType, SemanticNumberLiteralType as NumberLiteralType, + SemanticBigIntLiteralType as BigIntLiteralType, SemanticBooleanLiteralType as BooleanLiteralType, + SemanticIntrinsicType as IntrinsicType, SemanticObjectType as ObjectType, + SemanticTypeParameter as TypeParameter, + SemanticType as IndexType, + SemanticType as IndexedAccessType, + SemanticType as ConditionalType, + SemanticType as SubstitutionType, + SemanticTemplateLiteralType as TemplateLiteralType, + SemanticType as StringMappingType, SemanticSignature as Signature, SemanticSymbol as Symbol, } from "./semantic-model.js"; +export { symbolName, symbolNameOf } from "./semantic-model.js"; export type { SemanticIndexInfo as IndexInfo, SemanticTypePredicate as TypePredicate, } from "./semantic-checker.js"; -export type { - Ts7CompilerOptionsData as CompilerOptions, - Ts7DiagnosticData as Diagnostic, -} from "./session-schema.generated.js"; +export type { Ts7CompilerOptionsData as CompilerOptions } from "./session-schema.generated.js"; +export type { Ts7Diagnostic as Diagnostic } from "./diagnostic.js"; +export type { Ts7SourceMetadata as SourceFileMetadata } from "./session-schema.generated.js"; diff --git a/packages/compiler/src/frontend/ts7/session.test.ts b/packages/compiler/src/frontend/ts7/session.test.ts index 522b72fe6..d957fe0dd 100644 --- a/packages/compiler/src/frontend/ts7/session.test.ts +++ b/packages/compiler/src/frontend/ts7/session.test.ts @@ -14,6 +14,8 @@ import { spawnTs7Wire } from "./rpc-process.js"; import { tsgoExecutable } from "./rpc-api.js"; import { Ts7Session, Ts7SessionProject, Ts7SessionSnapshot } from "./session.js"; import { AstNode } from "./ast-node.js"; +import { ts7Diagnostic } from "./diagnostic.js"; +import { getTextOfNode } from "./node-functions.js"; const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; const protocolPath = (path: string) => path.replaceAll("\\", "/"); @@ -104,8 +106,11 @@ test("owned snapshots preserve SDK options, diagnostics, metadata and default-pr "getSuggestionDiagnostics", "getDeclarationDiagnostics", ] as const) { - expect(project.program[name](file), name).toEqual(expected.program[name](file)); - expect(project.program[name](), name).toEqual(expected.program[name]()); + // scriptc's diagnostics have every member set (ts7Diagnostic); the SDK's leave absent ones out. + expect(project.program[name](file), name).toEqual( + expected.program[name](file).map(ts7Diagnostic), + ); + expect(project.program[name](), name).toEqual(expected.program[name]().map(ts7Diagnostic)); } for (const name of [ "getProgramDiagnostics", @@ -289,7 +294,7 @@ test("a released latest snapshot cannot lend stale ASTs to the same project", () source = "export const third = true;"; const third = session.updateSnapshot({ fileChanges: { created: [file] } }); expect(third.getProject(config)!.program.getSourceFile(file)!.text).toBe(source); - expect(old.statements.nodes[0]!.getText()).toBe("export const value = 1;"); + expect(getTextOfNode(old.statements.nodes[0]!)).toBe("export const value = 1;"); } finally { session.close(); rmSync(dir, { recursive: true, force: true }); diff --git a/packages/compiler/src/frontend/ts7/session.ts b/packages/compiler/src/frontend/ts7/session.ts index 4933da9d6..87b97d25e 100644 --- a/packages/compiler/src/frontend/ts7/session.ts +++ b/packages/compiler/src/frontend/ts7/session.ts @@ -1,3 +1,4 @@ +import { ts7Diagnostic, type Ts7Diagnostic } from "./diagnostic.js"; import type { SourceFile } from "./ast-types.js"; import { AstFile, AstNode } from "./ast-node.js"; import { Ts7RpcClient } from "./rpc-client.js"; @@ -314,31 +315,35 @@ export class Ts7SessionProgram { isSourceFileDefaultLibrary(file: AstNode): boolean { return this.getSourceFileMetadataByPath(file.path)?.isDefaultLibrary ?? false; } - private diagnostics(method: string, file?: SemanticDocument): Ts7DiagnosticData[] { - return this.session.request(method, this.query(file)) ?? []; + private diagnostics(method: string, file?: SemanticDocument): Ts7Diagnostic[] { + const result: Ts7Diagnostic[] = []; + for (const data of this.session.request(method, this.query(file)) ?? + []) + result.push(ts7Diagnostic(data)); + return result; } - getSyntacticDiagnostics(file?: SemanticDocument): Ts7DiagnosticData[] { + getSyntacticDiagnostics(file?: SemanticDocument): Ts7Diagnostic[] { return this.diagnostics("getSyntacticDiagnostics", file); } - getBindDiagnostics(file?: SemanticDocument): Ts7DiagnosticData[] { + getBindDiagnostics(file?: SemanticDocument): Ts7Diagnostic[] { return this.diagnostics("getBindDiagnostics", file); } - getSemanticDiagnostics(file?: SemanticDocument): Ts7DiagnosticData[] { + getSemanticDiagnostics(file?: SemanticDocument): Ts7Diagnostic[] { return this.diagnostics("getSemanticDiagnostics", file); } - getSuggestionDiagnostics(file?: SemanticDocument): Ts7DiagnosticData[] { + getSuggestionDiagnostics(file?: SemanticDocument): Ts7Diagnostic[] { return this.diagnostics("getSuggestionDiagnostics", file); } - getDeclarationDiagnostics(file?: SemanticDocument): Ts7DiagnosticData[] { + getDeclarationDiagnostics(file?: SemanticDocument): Ts7Diagnostic[] { return this.diagnostics("getDeclarationDiagnostics", file); } - getProgramDiagnostics(): Ts7DiagnosticData[] { + getProgramDiagnostics(): Ts7Diagnostic[] { return this.diagnostics("getProgramDiagnostics"); } - getGlobalDiagnostics(): Ts7DiagnosticData[] { + getGlobalDiagnostics(): Ts7Diagnostic[] { return this.diagnostics("getGlobalDiagnostics"); } - getConfigFileParsingDiagnostics(): Ts7DiagnosticData[] { + getConfigFileParsingDiagnostics(): Ts7Diagnostic[] { return this.diagnostics("getConfigFileParsingDiagnostics"); } dispose(): void { diff --git a/packages/compiler/src/frontend/ts7/source-parser.test.ts b/packages/compiler/src/frontend/ts7/source-parser.test.ts index d5b54f080..b6950b3d5 100644 --- a/packages/compiler/src/frontend/ts7/source-parser.test.ts +++ b/packages/compiler/src/frontend/ts7/source-parser.test.ts @@ -8,6 +8,7 @@ import type { Ts7FileSystem } from "./rpc-filesystem.js"; import { Ts7SourceParser, type Ts7SourceKind } from "./source-parser.js"; import { closeSourceParser, parseSourceFile } from "./source-parser-node.js"; import * as ts from "./syntax.js"; +import { getTextOfNode, tokenStartOfNode } from "./node-functions.js"; const parsers: Ts7SourceParser[] = []; const directories: string[] = []; @@ -46,9 +47,9 @@ test("each parse owns its text and nodes across same-path edits, deletion and cl expect(restored.text).toBe(first.text); parser.close(); parser.close(); - expect(first.statements.nodes[0]!.getText()).toBe("export const first = 1;"); + expect(getTextOfNode(first.statements.nodes[0]!)).toBe("export const first = 1;"); expect(second.statements.nodes[0]!.parent).toBe(second); - expect(other.statements.nodes[0]!.getText()).toBe("exports.third = 3;"); + expect(getTextOfNode(other.statements.nodes[0]!)).toBe("exports.third = 3;"); expect(() => parser.parse("late.ts", "", "ts")).toThrow("closed"); }); @@ -114,9 +115,9 @@ for (const { name, kind, source } of syntaxCases) { expect(file.isDeclarationFile).toBe(oracle.isDeclarationFile); expect( file.statements.nodes.map((statement) => ({ - start: statement.getStart(), + start: tokenStartOfNode(statement), end: statement.end, - text: statement.getText(), + text: getTextOfNode(statement), })), ).toEqual( oracle.statements.map((statement) => ({ @@ -198,7 +199,11 @@ test("JSDoc callable types cannot replace method body views", () => { const expected = oracle.statements[0]!; if (!ts.isClassDeclaration(native) || !ts5.isClassDeclaration(expected)) throw new Error("expected class"); - expect(native.members.nodes.map((member) => member.body?.getText())).toEqual( + expect( + native.members.nodes.map((member) => + member.body === undefined ? undefined : getTextOfNode(member.body), + ), + ).toEqual( expected.members.map((member) => ts5.isMethodDeclaration(member) ? member.body?.getText() : undefined, ), @@ -231,7 +236,7 @@ test("a parser starts a new server once it has parsed its source budget, and ear expect(servers).toHaveLength(2); const third = parser.parse("b.ts", "export const b = 3;", "ts"); expect(servers).toHaveLength(2); - expect(first.statements.nodes[0]!.getText()).toBe(`export const a = ${"1 + ".repeat(20)}1;`); - expect(second.statements.nodes[0]!.getText()).toBe("export const b = 2;"); - expect(third.statements.nodes[0]!.getText()).toBe("export const b = 3;"); + expect(getTextOfNode(first.statements.nodes[0]!)).toBe(`export const a = ${"1 + ".repeat(20)}1;`); + expect(getTextOfNode(second.statements.nodes[0]!)).toBe("export const b = 2;"); + expect(getTextOfNode(third.statements.nodes[0]!)).toBe("export const b = 3;"); }); diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 87711a659..d16cd6ef0 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -69,7 +69,7 @@ export function jsOpenObjectType( shapes: ShapeRegistry, unions: UnionRegistry, ): IrType { - if (!decl || !isJsSourceFile(decl.getSourceFile())) return type; + if (!decl || !isJsSourceFile(ts.getSourceFileOfNode(decl))) return type; const arms = type.kind === "union" ? unions.get(type.unionId)?.arms : [type]; const present = arms?.filter((arm) => arm.kind !== "undefinedT"); const parameter = ts.isParameter(decl) @@ -1004,13 +1004,13 @@ export function isGeneralNodeStreamInterface( if (psym === undefined) return false; return psym.declarations.some((d) => { if (!(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d))) return false; - if (!ctx.isStdlibFile(d.getSourceFile())) return false; - if (psym.name === "ReadableStream" || psym.name === "WritableStream") + if (!ctx.isStdlibFile(ts.getSourceFileOfNode(d))) return false; + if (ts.symbolName(psym) === "ReadableStream" || ts.symbolName(psym) === "WritableStream") return ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS"); - if (psym.name === "ChildStdin") return isDeclaredInAmbientModule(d, "child_process"); + if (ts.symbolName(psym) === "ChildStdin") return isDeclaredInAmbientModule(d, "child_process"); return ( - (psym.name === "Readable" || psym.name === "Writable") && - isNodeTypesPath(d.getSourceFile().fileName) && + (ts.symbolName(psym) === "Readable" || ts.symbolName(psym) === "Writable") && + isNodeTypesPath(ts.getSourceFileOfNode(d).fileName) && isDeclaredInAmbientModule(d, "stream") ); }); @@ -1027,7 +1027,7 @@ export function isWebAssemblyType( if (!symbol) return false; return symbol.declarations.some( (d) => - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientNamespace(d as ts.Declaration, "WebAssembly"), ); } @@ -1178,6 +1178,12 @@ export interface TypeMapperCtx { * from every stream producer share their slots. Off (the default), the * stdio handles keep those spellings. */ generalNodeStreams?: boolean; + /** With generalNodeStreams, the program's checked-dynamic stream slots + * (lowerToIr decides it per program, dyn-streams.ts): NodeJS.ReadableStream + * and NodeJS.WritableStream map to the checked-dynamic representation, so + * a slot holds a native stream (a boxed runtime stream, identity kept) or + * a JavaScript stream object. Members dispatch per value at run time. */ + dynNodeStreams?: boolean; /** Successful context-free mappings, keyed by the * TypeScript server's stable type id. A generated facade can reference the * same large record/function type thousands of times; remapping its full @@ -1309,7 +1315,7 @@ export function isLegacyUrlSymbol( ): boolean { if (!symbol) return false; for (const d of symbol.declarations) { - if (!ctx.isStdlibFile(d.getSourceFile())) continue; + if (!ctx.isStdlibFile(ts.getSourceFileOfNode(d))) continue; let family = false; for (let n: ts.Node | undefined = d; n; n = n.parent) { if (ts.isInterfaceDeclaration(n) && LEGACY_URL_TYPES.has(n.name.text)) family = true; @@ -1349,7 +1355,10 @@ export function isOsConstantsSymbol( const alias = types && ts.isTypeAliasDeclaration(d) && d.name.text === "SignalConstants"; const signals = !types && ts.isVariableDeclaration(d) && ts.isIdentifier(d.name) && d.name.text === "signals"; - if (!(alias || signals || ts.isModuleDeclaration(d)) || !ctx.isStdlibFile(d.getSourceFile())) + if ( + !(alias || signals || ts.isModuleDeclaration(d)) || + !ctx.isStdlibFile(ts.getSourceFileOfNode(d)) + ) continue; let inConstants = ts.isModuleDeclaration(d) && d.name.text === "constants"; for (let n: ts.Node | undefined = d.parent; n; n = n.parent) { @@ -1372,14 +1381,14 @@ function isWorkerHandleSymbol( ctx: Pick, ): boolean { return ( - (symbol?.name === "Worker" || - symbol?.name === "MessagePort" || - symbol?.name === "MessageChannel" || - symbol?.name === "BroadcastChannel") && + ((symbol !== undefined && ts.symbolName(symbol) === "Worker") || + (symbol !== undefined && ts.symbolName(symbol) === "MessagePort") || + (symbol !== undefined && ts.symbolName(symbol) === "MessageChannel") || + (symbol !== undefined && ts.symbolName(symbol) === "BroadcastChannel")) && symbol.declarations.some( (d) => (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "worker_threads"), ) ); @@ -1398,11 +1407,11 @@ function isLookupOptionsUnion(type: ts.Type, ctx: Pick ctx.isStdlibFile(d.getSourceFile())) + !/^Lookup(One|All)?Options$/.test(ts.symbolName(symbol)) || + !symbol.declarations.some((d) => ctx.isStdlibFile(ts.getSourceFileOfNode(d))) ) return false; - if (symbol.name === "LookupAllOptions") all = true; + if (ts.symbolName(symbol) === "LookupAllOptions") all = true; } return all; } @@ -1449,12 +1458,12 @@ function isFetchBodyUnion(type: ts.Type, ctx: Pick ctx.isStdlibFile(d.getSourceFile())) + !FETCH_BODY_MEMBERS.has(ts.symbolName(symbol)) || + !symbol.declarations.some((d) => ctx.isStdlibFile(ts.getSourceFileOfNode(d))) ) return false; - if (symbol.name === "FormData") formData = true; - if (symbol.name === "URLSearchParams") searchParams = true; + if (ts.symbolName(symbol) === "FormData") formData = true; + if (ts.symbolName(symbol) === "URLSearchParams") searchParams = true; } return formData && searchParams; } @@ -1474,7 +1483,7 @@ function isDynInterfaceSubset(type: ts.Type, ctx: TypeMapperCtx): boolean { if ( parent === undefined || !ts.isInterfaceDeclaration(parent) || - !ctx.isStdlibFile(parent.getSourceFile()) || + !ctx.isStdlibFile(ts.getSourceFileOfNode(parent)) || (owner !== undefined && owner !== parent) ) return false; @@ -1496,16 +1505,17 @@ function isFetchHeadersUnion(type: ts.Type, ctx: Pick ctx.isStdlibFile(d.getSourceFile())) + symbol !== undefined && + ts.symbolName(symbol) === "Headers" && + symbol.declarations.some((d) => ctx.isStdlibFile(ts.getSourceFileOfNode(d))) ) { headers = true; continue; } - const alias = member.getAliasSymbol()?.name; + const alias = ts.symbolNameOf(member.getAliasSymbol()); if ( - symbol?.name === "Array" || - symbol?.name === "ReadonlyArray" || + (symbol !== undefined && ts.symbolName(symbol) === "Array") || + (symbol !== undefined && ts.symbolName(symbol) === "ReadonlyArray") || alias === "Record" || alias === "HeaderRecord" ) { @@ -1534,7 +1544,7 @@ function isReceiveMessageResult( !fn || !ts.isFunctionDeclaration(fn) || fn.name?.text !== "receiveMessageOnPort" || - !ctx.isStdlibFile(decl.getSourceFile()) || + !ctx.isStdlibFile(ts.getSourceFileOfNode(decl)) || !isDeclaredInAmbientModule(fn, "worker_threads") ) { return false; @@ -1578,9 +1588,9 @@ export function mapAsyncGeneratorAnnotation(type: ts.Type, ctx: TypeMapperCtx): const symbol = type.getSymbol(); if ( !symbol || - !["AsyncIterator", "AsyncIterableIterator", "AsyncIterable"].includes(symbol.name) || + !["AsyncIterator", "AsyncIterableIterator", "AsyncIterable"].includes(ts.symbolName(symbol)) || !symbol.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) return null; @@ -1609,11 +1619,13 @@ export function isFetchHandleType( const symbol = member.getSymbol(); if ( !symbol || - (symbol.name !== "Headers" && symbol.name !== "Request" && symbol.name !== "Response") || + (ts.symbolName(symbol) !== "Headers" && + ts.symbolName(symbol) !== "Request" && + ts.symbolName(symbol) !== "Response") || !symbol.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) return false; @@ -1694,7 +1706,7 @@ function spreadsCheckedDynamic(symbol: ts.Symbol | undefined, ctx: TypeMapperCtx if (symbol === undefined || (symbol.flags & ts.SymbolFlags.ObjectLiteral) === 0) return false; const decl = symbol.declarations[0]; if (decl === undefined || !ts.isObjectLiteralExpression(decl)) return false; - if (isJsSourceFile(decl.getSourceFile())) return false; + if (isJsSourceFile(ts.getSourceFileOfNode(decl))) return false; if (ctx.checker.getContextualType(decl)) return false; return decl.properties.nodes.some( (prop) => @@ -1708,7 +1720,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // TypeScript's intrinsic globalThis symbol has no declaration. A stored // reference needs native object identity, including var's undefined state. const intrinsic = type.getSymbol(); - if (intrinsic?.name === "globalThis" && intrinsic.declarations.length === 0) return DYN; + if ( + intrinsic !== undefined && + ts.symbolName(intrinsic) === "globalThis" && + intrinsic.declarations.length === 0 + ) + return DYN; if (ctx.jsDynamicDeclaration !== undefined && intrinsic !== undefined) { const objectLiteral = (intrinsic.flags & ts.SymbolFlags.ObjectLiteral) !== 0; const classInstance = @@ -1747,8 +1764,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // intersection mapping decomposes it into unsupported utility types. const directAlias = type.getAliasSymbol(); if ( - directAlias?.name === "ExecFileException" && - directAlias.declarations.some((decl) => ctx.isStdlibFile(decl.getSourceFile())) + directAlias !== undefined && + ts.symbolName(directAlias) === "ExecFileException" && + directAlias.declarations.some((decl) => ctx.isStdlibFile(ts.getSourceFileOfNode(decl))) ) { return { kind: "object", className: "%Error" }; } @@ -1757,11 +1775,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // ordinary Error as well. const execException = type.getSymbol(); if ( - execException?.name === "ExecException" && + execException !== undefined && + ts.symbolName(execException) === "ExecException" && execException.declarations.some( (decl) => ts.isInterfaceDeclaration(decl) && - ctx.isStdlibFile(decl.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(decl)) && isDeclaredInAmbientModule(decl, "child_process"), ) ) { @@ -1782,7 +1801,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( type.isIntersectionType() && ts.constituentTypes(type).every((t) => { - if (t.getSymbol()?.name !== "Promise") return false; + if (ts.symbolNameOf(t.getSymbol()) !== "Promise") return false; const arg = ctx.checker.getTypeArguments(t as ts.TypeReference)[0]; return arg !== undefined && isWebAssemblyType(arg, ctx); }) @@ -1801,7 +1820,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (ctx.erasingSignature || ctx.isErasing?.()) && !(type.flags & ts.TypeFlags.TypeParameter) && !checker.getCallSignatures(type).some((sig) => sig === ctx.erasingSignature) && - !ERASURE_KEPT_CONTAINERS.has(type.getSymbol()?.name ?? "") && + !ERASURE_KEPT_CONTAINERS.has(ts.symbolNameOf(type.getSymbol()) ?? "") && mentionsErasedTypeParam(type, ctx, 0) ) { contextResolutions++; @@ -1884,8 +1903,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // constructors. Only the standard library's interface gets this ABI. const errorOptionsSymbol = widened.getSymbol(); if ( - errorOptionsSymbol?.name === "ErrorOptions" && - errorOptionsSymbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) + errorOptionsSymbol !== undefined && + ts.symbolName(errorOptionsSymbol) === "ErrorOptions" && + errorOptionsSymbol.declarations.some((d) => ctx.isStdlibFile(ts.getSourceFileOfNode(d))) ) { return DYN; } @@ -1900,7 +1920,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { { const parseArgsSym = widened.getAliasSymbol() ?? widened.getSymbol(); const belongs = (declaration: ts.Node): boolean => { - if (!ctx.isStdlibFile(declaration.getSourceFile())) return false; + if (!ctx.isStdlibFile(ts.getSourceFileOfNode(declaration))) return false; let family = false; for (let node: ts.Node | undefined = declaration; node; node = node.parent) { if ( @@ -1916,7 +1936,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { }; if ( parseArgsSym && - PARSE_ARGS_DYN_TYPES.has(parseArgsSym.name) && + PARSE_ARGS_DYN_TYPES.has(ts.symbolName(parseArgsSym)) && parseArgsSym.declarations.some(belongs) ) return DYN; @@ -1936,10 +1956,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const boxSym = widened.getSymbol(); if ( boxSym && - (boxSym.name === "Number" || boxSym.name === "String" || boxSym.name === "Boolean") && - boxSym.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) + (ts.symbolName(boxSym) === "Number" || + ts.symbolName(boxSym) === "String" || + ts.symbolName(boxSym) === "Boolean") && + boxSym.declarations.some((d) => ctx.isStdlibFile(ts.getSourceFileOfNode(d))) ) { - return boxSym.name === "Number" ? F64 : boxSym.name === "String" ? STRING : BOOL; + return ts.symbolName(boxSym) === "Number" + ? F64 + : ts.symbolName(boxSym) === "String" + ? STRING + : BOOL; } } // The lib's PRIMITIVE-CONSTRUCTOR interfaces as TYPES (`typeof String`, @@ -1957,16 +1983,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const ctorSym = widened.getSymbol(); if ( ctorSym && - (ctorSym.name === "StringConstructor" || - ctorSym.name === "NumberConstructor" || - ctorSym.name === "BooleanConstructor") && - ctorSym.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) + (ts.symbolName(ctorSym) === "StringConstructor" || + ts.symbolName(ctorSym) === "NumberConstructor" || + ts.symbolName(ctorSym) === "BooleanConstructor") && + ctorSym.declarations.some((d) => ctx.isStdlibFile(ts.getSourceFileOfNode(d))) ) { return funcOf( [STRING], - ctorSym.name === "StringConstructor" + ts.symbolName(ctorSym) === "StringConstructor" ? STRING - : ctorSym.name === "NumberConstructor" + : ts.symbolName(ctorSym) === "NumberConstructor" ? F64 : BOOL, ); @@ -2070,7 +2096,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // declaration file. { const classSym = widened.getSymbol(); - const name = classSym?.name; + const name = ts.symbolNameOf(classSym); if (name === "LifetimeRegion" || name === "InternTable" || name === "ThreadSlots") { for (const d of classSym!.declarations) { if (!ts.isClassDeclaration(d)) continue; @@ -2098,7 +2124,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { return decls && decls.length > 0 && decls.every((d) => { - const sf = d.getSourceFile(); + const sf = ts.getSourceFileOfNode(d); return sf.isDeclarationFile && !ctx.isStdlibFile(sf) && !ctx.isExternalTypeFile(sf); }) ? decls @@ -2108,7 +2134,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { decls === null || decls.every( (d) => - packageTypeFile(d.getSourceFile(), ctx) && + packageTypeFile(ts.getSourceFileOfNode(d), ctx) && (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d) || ts.isTypeLiteralNode(d) || @@ -2315,18 +2341,19 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Nameless class DECLARATIONS are legal only as `export default class // {}` — classNamer spells them "%anon" (unique per file), the same // name their collection registered. - !classDecl.getSourceFile().isDeclarationFile && + !ts.getSourceFileOfNode(classDecl).isDeclarationFile && checker.getConstructSignatures(widened).length === 0 && // A TypeScript class declared OUTSIDE the lowered program (reached // through `import type` alone) never registers, and the program can // hold no instance of it: its instance type maps below as the // structural type it describes, like an interface. - (ctx.isProgramFile(classDecl.getSourceFile()) || isJsSourceFile(classDecl.getSourceFile())) + (ctx.isProgramFile(ts.getSourceFileOfNode(classDecl)) || + isJsSourceFile(ts.getSourceFileOfNode(classDecl))) ) { // A JavaScript class outside the lowered program (jsdoc `typeof // import()` over a never-required module) stays unmappable: JSDoc // types are not checked, so its slots fall back to the dynamic value. - if (!ctx.isProgramFile(classDecl.getSourceFile())) return null; + if (!ctx.isProgramFile(ts.getSourceFileOfNode(classDecl))) return null; // The class inside a MIXIN function: per-call-site instantiations // share this one node — the CURRENT instantiation (collecting or // lowering) is the answer; outside any mixin context the node's type @@ -2370,10 +2397,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( classDecl && (ts.isClassDeclaration(classDecl) || ts.isClassExpression(classDecl)) && - !classDecl.getSourceFile().isDeclarationFile && + !ts.getSourceFileOfNode(classDecl).isDeclarationFile && checker.getConstructSignatures(widened).length > 0 ) { - if (!ctx.isProgramFile(classDecl.getSourceFile())) return null; + if (!ctx.isProgramFile(ts.getSourceFileOfNode(classDecl))) return null; // The MIXIN class node's static side (`typeof C` inside the mixin): // the current instantiation's classval, like the instance type above. { @@ -2439,18 +2466,20 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // naming the offending half, as does the `new Map` lowering per site. const psym = widened.getSymbol(); const isStdlibInterface = (name: string): boolean => - psym?.name === name && + psym !== undefined && + ts.symbolName(psym) === name && psym.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ); // The timer handles: an interface in the fallback and in @types/node 22+, // a class (`class Timeout implements Timer`) in @types/node 20 and older. const isStdlibTimerHandle = (name: string): boolean => - psym?.name === name && + psym !== undefined && + ts.symbolName(psym) === name && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ); // The builtin Error classes: references to the lib's Error interfaces map to the runtime-provided class // hierarchy (provenance, not the name — a user's own `class Error` @@ -2464,8 +2493,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // node:assert's AssertionError is declared as a class (in the shipped // fallback and in @types/node), not as a lib interface. if ( - psym?.name === "AssertionError" && - psym.declarations.some((d) => ts.isClassDeclaration(d) && ctx.isStdlibFile(d.getSourceFile())) + psym !== undefined && + ts.symbolName(psym) === "AssertionError" && + psym.declarations.some( + (d) => ts.isClassDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), + ) ) return { kind: "object", className: "%AssertionError" }; // The lib's `Object` interface is a TOP type: tsc admits every @@ -2482,12 +2514,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // representation. The node:module class's merged instance interface is // included, while unrelated user interfaces named Module stay structural. if ( - (psym?.name === "ScriptcModule" || psym?.name === "Module") && + ((psym !== undefined && ts.symbolName(psym) === "ScriptcModule") || + (psym !== undefined && ts.symbolName(psym) === "Module")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - (psym.name === "ScriptcModule" || + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && + (ts.symbolName(psym) === "ScriptcModule" || isDeclaredInAmbientNamespace(d, "NodeJS") || isDeclaredInAmbientModule(d, "module")), ) @@ -2502,11 +2535,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // NodeJS.EventEmitter interface (the structural surface net.Socket // et al. extend) is the same runtime value. Provenance-checked. if ( - psym?.name === "EventEmitter" && + psym !== undefined && + ts.symbolName(psym) === "EventEmitter" && psym.declarations.some( (d) => (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "events") || isDeclaredInAmbientNamespace(d, "NodeJS")), ) ) { @@ -2517,11 +2551,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // precedent). The NAME is generic, so the provenance check adds the // enclosing ambient module, like net.Server. if ( - psym?.name === "Interface" && + psym !== undefined && + ts.symbolName(psym) === "Interface" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "readline") || isDeclaredInAmbientModule(d, "readline/promises")), ) @@ -2532,11 +2567,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // it keeps Node's own shape (an undefined name, the escape sequences' // code member). if ( - psym?.name === "Key" && + psym !== undefined && + ts.symbolName(psym) === "Key" && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "readline") || isDeclaredInAmbientModule(d, "readline/promises")), ) @@ -2545,11 +2581,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { } // readline/promises.Readline: an f64 handle the same way. if ( - psym?.name === "Readline" && + psym !== undefined && + ts.symbolName(psym) === "Readline" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "readline/promises"), ) ) { @@ -2562,11 +2599,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // plus the ambient module suffices for both the fallback declarations // and @types/node's class Channel. if ( - psym?.name === "Channel" && + psym !== undefined && + ts.symbolName(psym) === "Channel" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "diagnostics_channel"), ) ) { @@ -2576,11 +2614,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // the runtime's store id space (the Channel story — stores are // process-lived; contexts ride the fiber machinery). if ( - psym?.name === "AsyncLocalStorage" && + psym !== undefined && + ts.symbolName(psym) === "AsyncLocalStorage" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "async_hooks"), ) ) { @@ -2589,11 +2628,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // diagnostics_channel.TracingChannel: the same f64-handle story over the // runtime's tracing registry (five event channels per entry). if ( - psym?.name === "TracingChannel" && + psym !== undefined && + ts.symbolName(psym) === "TracingChannel" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "diagnostics_channel"), ) ) { @@ -2603,7 +2643,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const encodeResult = psym && psym.declarations.some((decl) => { - if (!ctx.isStdlibFile(decl.getSourceFile())) return false; + if (!ctx.isStdlibFile(ts.getSourceFileOfNode(decl))) return false; if (ts.isInterfaceDeclaration(decl) && decl.name.text === "EncodeIntoResult") return true; const parent = decl.parent; return ( @@ -2628,19 +2668,20 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // WHATWG codec instances have ordinary reference identity and ownership. // Their private encoding slot is omitted from enumeration and JSON. if ( - (psym?.name === "TextEncoder" || psym?.name === "TextDecoder") && + ((psym !== undefined && ts.symbolName(psym) === "TextEncoder") || + (psym !== undefined && ts.symbolName(psym) === "TextDecoder")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return { kind: "record", shapeId: ctx.shapes.intern( [ - { name: `%${psym.name}`, type: F64 }, - ...(psym.name === "TextDecoder" + { name: `%${ts.symbolName(psym)}`, type: F64 }, + ...(ts.symbolName(psym) === "TextDecoder" ? [ { name: "%fatal", type: BOOL }, { name: "%ignoreBOM", type: BOOL }, @@ -2662,11 +2703,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // special-cased in lowerNew / lowerStringDecoderMethodCall, so the // record shape never surfaces. if ( - psym?.name === "StringDecoder" && + psym !== undefined && + ts.symbolName(psym) === "StringDecoder" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return { @@ -2691,14 +2733,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if ( flags & ts.TypeFlags.Object && promiseDeclarations.length > 0 && - promiseDeclarations.every((d) => d.getSourceFile().isDeclarationFile) + promiseDeclarations.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile) ) { const props = checker.getPropertiesOfType(widened); if ( props.length === 3 && - ["promise", "resolve", "reject"].every((n) => props.some((p) => p.name === n)) + ["promise", "resolve", "reject"].every((n) => props.some((p) => ts.symbolName(p) === n)) ) { - const promProp = props.find((p) => p.name === "promise")!; + const promProp = props.find((p) => ts.symbolName(p) === "promise")!; const promT = mapType(checker.getTypeOfSymbol(promProp), ctx); if (promT?.kind === "promise") { const inner = promT.inner; @@ -2747,9 +2789,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // option record whose members include functions, agents and signals. // Values ride checked-dynamic storage, like SpawnOptions. if ( - psym?.name === "RequestOptions" && + psym !== undefined && + ts.symbolName(psym) === "RequestOptions" && psym.declarations.some((d) => { - if (!ts.isInterfaceDeclaration(d) || !ctx.isStdlibFile(d.getSourceFile())) return false; + if (!ts.isInterfaceDeclaration(d) || !ctx.isStdlibFile(ts.getSourceFileOfNode(d))) + return false; for (let p: ts.Node | undefined = d.parent; p !== undefined; p = p.parent) { if (ts.isModuleDeclaration(p) && ts.isStringLiteral(p.name)) return HTTP_REQUEST_OPTION_MODULES.has(p.name.text); @@ -2772,9 +2816,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { { const alias = widened.getAliasSymbol(); if ( - alias?.name === "ReadableStreamReadResult" && + alias !== undefined && + ts.symbolName(alias) === "ReadableStreamReadResult" && alias.declarations.some( - (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { const elemTs = widened.getAliasTypeArguments()?.[0]; @@ -2786,10 +2831,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { } if ( psym && - (psym.name === "ReadableStreamReadValueResult" || - psym.name === "ReadableStreamReadDoneResult") && + (ts.symbolName(psym) === "ReadableStreamReadValueResult" || + ts.symbolName(psym) === "ReadableStreamReadDoneResult") && psym.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { const elemTs = checker.getTypeArguments(widened as ts.TypeReference)[0]; @@ -2801,11 +2846,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // node:net's SocketAddress and BlockList: native handles (scr_net.c) // whose getters and methods dispatch through the handle. if ( - (psym?.name === "SocketAddress" || psym?.name === "BlockList") && + ((psym !== undefined && ts.symbolName(psym) === "SocketAddress") || + (psym !== undefined && ts.symbolName(psym) === "BlockList")) && psym.declarations.some( (d) => (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return DYN; @@ -2816,11 +2862,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // checked-dynamic objects. if ( psym && - WEBCRYPTO_DYN_TYPES.has(psym.name) && + WEBCRYPTO_DYN_TYPES.has(ts.symbolName(psym)) && psym.declarations.some( (d) => (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return DYN; @@ -2829,20 +2875,21 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // whose getters and methods dispatch through the handle. The JsonWebKey // that export() returns is a plain checked-dynamic object. if ( - (psym?.name === "KeyObject" || psym?.name === "JsonWebKey") && + ((psym !== undefined && ts.symbolName(psym) === "KeyObject") || + (psym !== undefined && ts.symbolName(psym) === "JsonWebKey")) && psym.declarations.some( (d) => (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return DYN; } if ( psym && - (FETCH_AMBIENT_TYPES as readonly string[]).includes(psym.name) && + (FETCH_AMBIENT_TYPES as readonly string[]).includes(ts.symbolName(psym)) && !( - psym.name === "ReadableStream" && + ts.symbolName(psym) === "ReadableStream" && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS"), ) @@ -2850,7 +2897,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return DYN; @@ -2863,12 +2910,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Symbol.asyncDispose method. A DisposableStack is a native handle whose // methods are function values of its checked-dynamic view. if ( - (psym?.name === "Console" || - psym?.name === "Disposable" || - psym?.name === "AsyncDisposable" || - psym?.name === "DisposableStack") && + ((psym !== undefined && ts.symbolName(psym) === "Console") || + (psym !== undefined && ts.symbolName(psym) === "Disposable") || + (psym !== undefined && ts.symbolName(psym) === "AsyncDisposable") || + (psym !== undefined && ts.symbolName(psym) === "DisposableStack")) && psym.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return DYN; @@ -2878,8 +2925,8 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // representation; lowerCipherMethodCall dispatches their methods. if ( psym !== undefined && - CIPHER_TYPE_NAMES.has(psym.name) && - psym.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) + CIPHER_TYPE_NAMES.has(ts.symbolName(psym)) && + psym.declarations.some((d) => ctx.isStdlibFile(ts.getSourceFileOfNode(d))) ) { return DYN; } @@ -3006,11 +3053,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // fallback and by @types/node (whose NonSharedBuffer alias resolves to // the same symbol); provenance-checked like URL. if ( - psym?.name === "Buffer" && + psym !== undefined && + ts.symbolName(psym) === "Buffer" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return bytesOf("u8"); @@ -3021,11 +3069,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // instance type). Provenance, not the name: a user's own URL maps as a // record/class like any other. if ( - psym?.name === "URL" && + psym !== undefined && + ts.symbolName(psym) === "URL" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return { kind: "url" }; @@ -3034,11 +3083,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // interface by the shipped fallback and by @types/node (plus `class // URLSearchParams` in the "url" module). Provenance-checked like URL. if ( - psym?.name === "URLSearchParams" && + psym !== undefined && + ts.symbolName(psym) === "URLSearchParams" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return { kind: "searchParams" }; @@ -3046,11 +3096,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // fs.Stats: @types/node's `class Stats` (module "fs") or the fallback // declarations' interface. Provenance-checked like URL. if ( - psym?.name === "Stats" && + psym !== undefined && + ts.symbolName(psym) === "Stats" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return { kind: "stats" }; @@ -3058,13 +3109,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Capacity snapshots and options keep their live object representation // across numeric/BigInt overloads, runtime options, and callbacks. if ( - (psym?.name === "StatsFs" || - psym?.name === "BigIntStatsFs" || - psym?.name === "StatFsOptions") && + ((psym !== undefined && ts.symbolName(psym) === "StatsFs") || + (psym !== undefined && ts.symbolName(psym) === "BigIntStatsFs") || + (psym !== undefined && ts.symbolName(psym) === "StatFsOptions")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "fs"), ) ) @@ -3074,11 +3125,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // distinguishes it from user interfaces with the same name; both the // fallback and @types/node declare it in "fs/promises". if ( - psym?.name === "FileHandle" && + psym !== undefined && + ts.symbolName(psym) === "FileHandle" && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "fs/promises"), ) ) { @@ -3087,11 +3139,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // fs.Dir: an open directory stream, declared in "fs" by both the // fallback and @types/node. if ( - psym?.name === "Dir" && + psym !== undefined && + ts.symbolName(psym) === "Dir" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "fs"), ) ) { @@ -3102,11 +3155,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // reads work either way) or the fallback declarations' plain interface. // Provenance-checked like Stats. if ( - psym?.name === "SpawnSyncReturns" && + psym !== undefined && + ts.symbolName(psym) === "SpawnSyncReturns" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return { kind: "spawnRes" }; @@ -3116,13 +3170,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // refinements only tell the checker which stdio properties are null. if ( psym !== undefined && - (psym.name === "ChildProcess" || - psym.name === "ChildProcessWithoutNullStreams" || - psym.name === "ChildProcessByStdio") && + (ts.symbolName(psym) === "ChildProcess" || + ts.symbolName(psym) === "ChildProcessWithoutNullStreams" || + ts.symbolName(psym) === "ChildProcessByStdio") && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "child_process"), ) ) { @@ -3136,25 +3190,27 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // that module (http's Server) stays unmapped and fences at its use // site. if ( - (psym?.name === "Server" || psym?.name === "Socket" || psym?.name === "TLSSocket") && + ((psym !== undefined && ts.symbolName(psym) === "Server") || + (psym !== undefined && ts.symbolName(psym) === "Socket") || + (psym !== undefined && ts.symbolName(psym) === "TLSSocket")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "net") || // http.Server extends net.Server in @types/node — and tls.Server // / https.Server extend it transitively: same lowered surface // (listen/close/address/'error'), same handle kind. tls.TLSSocket // extends net.Socket the same way: post-handshake it IS a net // socket to every lowered member. - (psym.name === "Server" && + (ts.symbolName(psym) === "Server" && (isDeclaredInAmbientModule(d, "http") || isDeclaredInAmbientModule(d, "tls") || isDeclaredInAmbientModule(d, "https"))) || - (psym.name === "TLSSocket" && isDeclaredInAmbientModule(d, "tls"))), + (ts.symbolName(psym) === "TLSSocket" && isDeclaredInAmbientModule(d, "tls"))), ) ) { - return psym.name === "Server" ? { kind: "netServer" } : { kind: "netSocket" }; + return ts.symbolName(psym) === "Server" ? { kind: "netServer" } : { kind: "netSocket" }; } // tls.SecureContext: the opaque parsed-cert/key handle SNI callbacks // answer with (tls.createSecureContext({ cert, key }) mints it; the TLS @@ -3162,11 +3218,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // @types/node declares `interface SecureContext` inside `declare module // "tls"`, and the shipped fallback mirrors it. if ( - psym?.name === "SecureContext" && + psym !== undefined && + ts.symbolName(psym) === "SecureContext" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "tls"), ) ) { @@ -3175,11 +3232,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Scrypt's runtime options retain property accessors and aliases. Use // checked values so presence probes and repeated getter reads match Node. if ( - psym?.name === "ScryptOptions" && + psym !== undefined && + ts.symbolName(psym) === "ScryptOptions" && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "crypto"), ) ) @@ -3188,26 +3246,28 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // provenance module-qualified so user classes with these common names // retain their ordinary structural/class representation. if ( - (psym?.name === "Hash" || psym?.name === "Hmac") && + ((psym !== undefined && ts.symbolName(psym) === "Hash") || + (psym !== undefined && ts.symbolName(psym) === "Hmac")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "crypto"), ) ) { - return psym.name === "Hash" ? CRYPTOHASH_T : CRYPTOHMAC_T; + return ts.symbolName(psym) === "Hash" ? CRYPTOHASH_T : CRYPTOHMAC_T; } // fs.FSWatcher: the fs.watch handle (scr_watch.c). Module-checked like // net.Server — @types/node declares `interface FSWatcher extends // EventEmitter` inside `declare module "fs"`, and the shipped fallback // mirrors it. if ( - psym?.name === "FSWatcher" && + psym !== undefined && + ts.symbolName(psym) === "FSWatcher" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "fs"), ) ) { @@ -3233,23 +3293,47 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Under the program's general stream representation (generalNodeStreams) // NodeJS.WritableStream is the runtime Writable instead: the process // streams convert into it where they flow (process.stdioWritable). + // Checked-dynamic stream slots: an intersection over a NodeJS stream + // interface (`NodeJS.ReadableStream & { destroy?(): void }`) holds the + // same values as the interface. + if ( + ctx.dynNodeStreams === true && + (type.flags & ts.TypeFlags.Intersection) !== 0 && + ts.constituentTypes(type).some((part) => { + const sym = part.getSymbol(); + return ( + ((sym !== undefined && ts.symbolName(sym) === "ReadableStream") || + (sym !== undefined && ts.symbolName(sym) === "WritableStream")) && + sym.declarations.some( + (d) => + ts.isInterfaceDeclaration(d) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && + isDeclaredInAmbientNamespace(d, "NodeJS"), + ) + ); + }) + ) { + return DYN; + } const nodeWritableInterface = - psym?.name === "WritableStream" && + psym !== undefined && + ts.symbolName(psym) === "WritableStream" && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientNamespace(d, "NodeJS"), ); if (nodeWritableInterface && ctx.generalNodeStreams === true) { - return { kind: "object", className: "%Writable" }; + return ctx.dynNodeStreams === true ? DYN : { kind: "object", className: "%Writable" }; } if ( - (psym?.name === "WriteStream" && + (psym !== undefined && + ts.symbolName(psym) === "WriteStream" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "tty") || (isDeclaredInAmbientModule(d, "process") && isDeclaredInAmbientNamespace(d, "NodeJS"))), )) || @@ -3262,21 +3346,23 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // stream.Writable. As with stream.Readable below, unsupported producers // of the broad @types class fence where they are constructed. if ( - (psym?.name === "ChildStdin" && + (psym !== undefined && + ts.symbolName(psym) === "ChildStdin" && ctx.generalNodeStreams !== true && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "child_process"), )) || - (psym?.name === "Writable" && + (psym !== undefined && + ts.symbolName(psym) === "Writable" && ctx.generalNodeStreams !== true && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && - isNodeTypesPath(d.getSourceFile().fileName) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && + isNodeTypesPath(ts.getSourceFileOfNode(d).fileName) && isDeclaredInAmbientModule(d, "stream"), )) ) { @@ -3286,11 +3372,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // ChildStdin is the runtime Writable too (child.stdioStream bridges it). if ( ctx.generalNodeStreams === true && - psym?.name === "ChildStdin" && + psym !== undefined && + ts.symbolName(psym) === "ChildStdin" && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "child_process"), ) ) { @@ -3308,12 +3395,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Writable join them under the general representation. for (const [irName, rec] of RUNTIME_STREAM_CLASSES) { if ( - psym?.name === rec.lib && + psym !== undefined && + ts.symbolName(psym) === rec.lib && psym.declarations.some( (d) => ts.isClassDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && - (!isNodeTypesPath(d.getSourceFile().fileName) || + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && + (!isNodeTypesPath(ts.getSourceFileOfNode(d).fileName) || ctx.generalNodeStreams === true || (rec.lib !== "Readable" && rec.lib !== "Writable")) && isDeclaredInAmbientModule(d, "stream"), @@ -3326,12 +3414,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // @types/node's): their instances are runtime Transform streams. if ( psym !== undefined && - ZLIB_STREAM_MODES.has(psym.name) && + ZLIB_STREAM_MODES.has(ts.symbolName(psym)) && psym.declarations.some( (d) => (ts.isClassDeclaration(d) || - (ts.isInterfaceDeclaration(d) && isNodeTypesPath(d.getSourceFile().fileName))) && - ctx.isStdlibFile(d.getSourceFile()) && + (ts.isInterfaceDeclaration(d) && isNodeTypesPath(ts.getSourceFileOfNode(d).fileName))) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "zlib"), ) ) { @@ -3341,35 +3429,46 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // @types/node's): their instances are runtime Readable / Writable // streams (scr_fs_stream.c). if ( - (psym?.name === "ReadStream" || psym?.name === "WriteStream") && + ((psym !== undefined && ts.symbolName(psym) === "ReadStream") || + (psym !== undefined && ts.symbolName(psym) === "WriteStream")) && psym.declarations.some( (d) => ts.isClassDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "fs"), ) ) { - return { kind: "object", className: psym.name === "ReadStream" ? "%Readable" : "%Writable" }; + return { + kind: "object", + className: ts.symbolName(psym) === "ReadStream" ? "%Readable" : "%Writable", + }; } if ( - (psym?.name === "Readable" && + (psym !== undefined && + ts.symbolName(psym) === "Readable" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "stream"), )) || - (psym?.name === "ReadableStream" && + (psym !== undefined && + ts.symbolName(psym) === "ReadableStream" && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientNamespace(d, "NodeJS"), )) ) { // The general representation: the runtime Readable, which child - // stdio reads produce through the child stream bridge. - if (ctx.generalNodeStreams === true) return { kind: "object", className: "%Readable" }; + // stdio reads produce through the child stream bridge. With + // checked-dynamic stream slots, NodeJS.ReadableStream holds any + // stream value (dyn-streams.ts); stream.Readable keeps the class. + if (ctx.generalNodeStreams === true) { + if (ctx.dynNodeStreams === true && ts.symbolName(psym) === "ReadableStream") return DYN; + return { kind: "object", className: "%Readable" }; + } return { kind: "childStream" }; } // dgram.Socket: the same ambiguous "Socket" name disambiguated by its @@ -3377,11 +3476,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // EventEmitter` and the fallback declarations' interface both live // inside `declare module "dgram"`. if ( - psym?.name === "Socket" && + psym !== undefined && + ts.symbolName(psym) === "Socket" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "dgram"), ) ) { @@ -3394,11 +3494,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // `declare module "node:test"` (isDeclaredInAmbientModule answers for // both spellings). if ( - (psym?.name === "TestContext" || psym?.name === "SuiteContext") && + ((psym !== undefined && ts.symbolName(psym) === "TestContext") || + (psym !== undefined && ts.symbolName(psym) === "SuiteContext")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "test"), ) ) { @@ -3408,18 +3509,19 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // records built from F (mock-shapes.ts). Mock then maps as the hybrid // { %call: F, mock } record through mapHybridCallableIntersection. if ( - (psym?.name === "MockFunctionContext" || psym?.name === "MockFunctionCall") && + ((psym !== undefined && ts.symbolName(psym) === "MockFunctionContext") || + (psym !== undefined && ts.symbolName(psym) === "MockFunctionCall")) && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "test"), ) ) { const fnTs = checker.getTypeArguments(widened as ts.TypeReference)[0]; const fn = fnTs ? mapType(fnTs, ctx) : null; if (!fn || fn.kind !== "func") return null; - return psym.name === "MockFunctionContext" + return ts.symbolName(psym) === "MockFunctionContext" ? mockContextType(fn, ctx) : mockCallRecordType(fn, ctx); } @@ -3429,9 +3531,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // above; optional metadata uses the same checked property view. // Provenance-checked like the rest. if ( - psym?.name === "ErrnoException" && + psym !== undefined && + ts.symbolName(psym) === "ErrnoException" && psym.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return { kind: "object", className: "%Error" }; @@ -3443,11 +3546,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // of fencing at the declaration. Module-checked like the rest (https' // Agent extends http's; both live in their ambient modules). if ( - psym?.name === "Agent" && + psym !== undefined && + ts.symbolName(psym) === "Agent" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "http") || isDeclaredInAmbientModule(d, "https")), ) ) { @@ -3457,15 +3561,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // net.Server (the names repeat nowhere today, but provenance stays the // rule). if ( - (psym?.name === "IncomingMessage" || psym?.name === "ServerResponse") && + ((psym !== undefined && ts.symbolName(psym) === "IncomingMessage") || + (psym !== undefined && ts.symbolName(psym) === "ServerResponse")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "http"), ) ) { - return psym.name === "IncomingMessage" ? { kind: "httpReq" } : { kind: "httpRes" }; + return ts.symbolName(psym) === "IncomingMessage" ? { kind: "httpReq" } : { kind: "httpRes" }; } // http2's compatibility surface (the allowHTTP1 lowering): the secure // server IS a net server (listen/close/address/emit ride the same @@ -3476,19 +3581,19 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // member at their use sites (lower-server.ts). Module-checked like the // rest. if ( - (psym?.name === "Http2SecureServer" || - psym?.name === "Http2ServerRequest" || - psym?.name === "Http2ServerResponse") && + ((psym !== undefined && ts.symbolName(psym) === "Http2SecureServer") || + (psym !== undefined && ts.symbolName(psym) === "Http2ServerRequest") || + (psym !== undefined && ts.symbolName(psym) === "Http2ServerResponse")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "http2"), ) ) { - return psym.name === "Http2SecureServer" + return ts.symbolName(psym) === "Http2SecureServer" ? { kind: "netServer" } - : psym.name === "Http2ServerRequest" + : ts.symbolName(psym) === "Http2ServerRequest" ? { kind: "httpReq" } : { kind: "httpRes" }; } @@ -3497,33 +3602,34 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // kinds (client and server flavors share a kind — the runtime handle // carries the role). Module-checked like the rest. if ( - (psym?.name === "Http2Server" || - psym?.name === "Http2Session" || - psym?.name === "ClientHttp2Session" || - psym?.name === "ServerHttp2Session" || - psym?.name === "ClientHttp2Stream" || - psym?.name === "ServerHttp2Stream") && + ((psym !== undefined && ts.symbolName(psym) === "Http2Server") || + (psym !== undefined && ts.symbolName(psym) === "Http2Session") || + (psym !== undefined && ts.symbolName(psym) === "ClientHttp2Session") || + (psym !== undefined && ts.symbolName(psym) === "ServerHttp2Session") || + (psym !== undefined && ts.symbolName(psym) === "ClientHttp2Stream") || + (psym !== undefined && ts.symbolName(psym) === "ServerHttp2Stream")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "http2"), ) ) { - return psym.name === "Http2Server" + return ts.symbolName(psym) === "Http2Server" ? { kind: "netServer" } - : psym.name === "ClientHttp2Stream" || psym.name === "ServerHttp2Stream" + : ts.symbolName(psym) === "ClientHttp2Stream" || ts.symbolName(psym) === "ServerHttp2Stream" ? { kind: "http2Stream" } : { kind: "http2Session" }; } // http.ClientRequest (http.request/http.get results) — module-checked // like IncomingMessage. if ( - psym?.name === "ClientRequest" && + psym !== undefined && + ts.symbolName(psym) === "ClientRequest" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "http"), ) ) { @@ -3538,9 +3644,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // declarations' anonymous env type takes the pureIndexShape path below // and interns the SAME shape. if ( - psym?.name === "ProcessEnv" && + psym !== undefined && + ts.symbolName(psym) === "ProcessEnv" && psym.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { const value = withUndefinedArm(STRING, ctx.unions); @@ -3630,9 +3737,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { { const alias = widened.getAliasSymbol(); if ( - alias?.name === "IteratorResult" && + alias !== undefined && + ts.symbolName(alias) === "IteratorResult" && alias.declarations.some( - (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { const targs = widened.getAliasTypeArguments() ?? []; @@ -3752,10 +3860,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ]; const fields: { name: string; type: IrType }[] = []; for (const property of checker.getPropertiesOfType(widened)) { - if (!known.includes(property.name)) return null; + if (!known.includes(ts.symbolName(property))) return null; const type = mapType(checker.getTypeOfSymbol(property), ctx); if (!type) return null; - fields.push({ name: property.name, type }); + fields.push({ name: ts.symbolName(property), type }); } const order = fields.map((field) => field.name); fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); @@ -3764,23 +3872,24 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // Filesystem options are data records. Keep their declared optional fields // instead of allowing arbitrary standard-library interfaces into records. if ( - (psym?.name === "FileReadOptions" || - psym?.name === "ReadOptions" || - psym?.name === "ReadOptionsWithBuffer" || - psym?.name === "WriteOptions") && + ((psym !== undefined && ts.symbolName(psym) === "FileReadOptions") || + (psym !== undefined && ts.symbolName(psym) === "ReadOptions") || + (psym !== undefined && ts.symbolName(psym) === "ReadOptionsWithBuffer") || + (psym !== undefined && ts.symbolName(psym) === "WriteOptions")) && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "fs/promises") || isDeclaredInAmbientModule(d, "fs")), ) ) { const fields: { name: string; type: IrType }[] = []; for (const property of checker.getPropertiesOfType(widened)) { - if (!["buffer", "offset", "length", "position"].includes(property.name)) return null; + if (!["buffer", "offset", "length", "position"].includes(ts.symbolName(property))) + return null; const type = mapType(checker.getTypeOfSymbol(property), ctx); if (!type) return null; - fields.push({ name: property.name, type }); + fields.push({ name: ts.symbolName(property), type }); } const order = fields.map((field) => field.name); fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); @@ -3791,28 +3900,28 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // record does not model the prototype (unobservable through the lowered // surface); it preserves the caller buffer/string by reference. if ( - (psym?.name === "FileReadResult" || - psym?.name === "FileWriteResult" || - psym?.name === "FileReadvResult" || - psym?.name === "FileWritevResult" || - psym?.name === "ReadVResult" || - psym?.name === "WriteVResult") && + ((psym !== undefined && ts.symbolName(psym) === "FileReadResult") || + (psym !== undefined && ts.symbolName(psym) === "FileWriteResult") || + (psym !== undefined && ts.symbolName(psym) === "FileReadvResult") || + (psym !== undefined && ts.symbolName(psym) === "FileWritevResult") || + (psym !== undefined && ts.symbolName(psym) === "ReadVResult") || + (psym !== undefined && ts.symbolName(psym) === "WriteVResult")) && psym.declarations.some( (d) => ts.isInterfaceDeclaration(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "fs/promises") || - ((psym.name === "ReadVResult" || psym.name === "WriteVResult") && + ((ts.symbolName(psym) === "ReadVResult" || ts.symbolName(psym) === "WriteVResult") && isDeclaredInAmbientModule(d, "fs"))), ) ) { const args = checker.getTypeArguments(widened as ts.TypeReference); if (args.length !== 1) return null; const vector = - psym.name === "FileReadvResult" || - psym.name === "FileWritevResult" || - psym.name === "ReadVResult" || - psym.name === "WriteVResult"; + ts.symbolName(psym) === "FileReadvResult" || + ts.symbolName(psym) === "FileWritevResult" || + ts.symbolName(psym) === "ReadVResult" || + ts.symbolName(psym) === "WriteVResult"; const emptyVector = vector && checker.isArrayType(args[0]!) && @@ -3827,9 +3936,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { return null; } const count = - psym.name === "FileReadResult" || - psym.name === "FileReadvResult" || - psym.name === "ReadVResult" + ts.symbolName(psym) === "FileReadResult" || + ts.symbolName(psym) === "FileReadvResult" || + ts.symbolName(psym) === "ReadVResult" ? "bytesRead" : "bytesWritten"; const payloadName = vector ? "buffers" : "buffer"; @@ -3861,11 +3970,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // declares it concrete (an interface; @types/node uses a class — both // provenance-checked, the Buffer/URL pattern). if ( - psym?.name === "Dirent" && + psym !== undefined && + ts.symbolName(psym) === "Dirent" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { const args = checker.getTypeArguments(widened as ts.TypeReference); @@ -3922,7 +4032,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { d.parent.name.text === "times" && ts.isInterfaceDeclaration(d.parent.parent) && d.parent.parent.name.text === "CpuInfo" && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) return cpuTimes(); @@ -3943,7 +4053,8 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { }; } if (isStdlibInterface("ParsedPath") || isStdlibInterface("FormatInputPathObject")) { - const field = psym!.name === "ParsedPath" ? STRING : withUndefinedArm(STRING, ctx.unions); + const field = + ts.symbolName(psym!) === "ParsedPath" ? STRING : withUndefinedArm(STRING, ctx.unions); if (!field) return null; return { kind: "record", @@ -3994,7 +4105,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym.declarations.some( (d) => ts.isTypeLiteralNode(d) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && (isDeclaredInAmbientModule(d, "child_process") || isDeclaredInAmbientModule(d, "util") || isDeclaredInAmbientModule(d, "dns")), @@ -4009,7 +4120,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (checker.getTypeOfSymbol(p).flags & ts.TypeFlags.String) !== 0, ) ? props - .map((p) => p.name) + .map((p) => ts.symbolName(p)) .sort() .join(",") : ""; @@ -4037,11 +4148,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // JSON.stringify of one shows the three fields where Node's // internal-slot object shows {} — the Dirent %dtype edge's cousin. if ( - psym?.name === "X509Certificate" && + psym !== undefined && + ts.symbolName(psym) === "X509Certificate" && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(d)) && isDeclaredInAmbientModule(d, "crypto"), ) ) { @@ -4068,13 +4180,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // @types/node declares the classes in perf_hooks and global interfaces // over them; the shipped fallback declares the classes. if ( - (psym?.name === "PerformanceEntry" || - psym?.name === "PerformanceMark" || - psym?.name === "PerformanceMeasure") && + ((psym !== undefined && ts.symbolName(psym) === "PerformanceEntry") || + (psym !== undefined && ts.symbolName(psym) === "PerformanceMark") || + (psym !== undefined && ts.symbolName(psym) === "PerformanceMeasure")) && psym.declarations.some( (d) => (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) && - ctx.isStdlibFile(d.getSourceFile()), + ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { return { kind: "record", shapeId: perfEntryShape(ctx.shapes, ctx.unions) }; @@ -4231,7 +4343,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { isStdlibInterface("NetworkInterfaceInfoIPv4") || isStdlibInterface("NetworkInterfaceInfoIPv6") ) { - const v6 = psym!.name === "NetworkInterfaceInfoIPv6"; + const v6 = ts.symbolName(psym!) === "NetworkInterfaceInfoIPv6"; const cidrArms = [NULL_T, STRING].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); const cidr: IrType = { kind: "union", unionId: ctx.unions.intern(cidrArms) }; const scopeid = v6 ? F64 : withUndefinedArm(F64, ctx.unions); @@ -4379,7 +4491,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { psym !== undefined && psym.declarations.length > 0 && psym.declarations.every( - (d) => ts.isInterfaceDeclaration(d) && !ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && !ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) ) { const CHILD_SURFACE = new Set([ @@ -4414,24 +4526,26 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const props = checker.getPropertiesOfType(widened); let core = 0; const childMemberType = (p: ts.Symbol): boolean => { - if (["kill", "on", "once", "off", "removeListener", "unref", "ref"].includes(p.name)) { + if ( + ["kill", "on", "once", "off", "removeListener", "unref", "ref"].includes(ts.symbolName(p)) + ) { return checker.getCallSignatures(checker.getTypeOfSymbol(p)).length > 0; } const expected = - p.name === "stdout" || p.name === "stderr" + ts.symbolName(p) === "stdout" || ts.symbolName(p) === "stderr" ? "childStream" - : p.name === "stdin" + : ts.symbolName(p) === "stdin" ? "childWriter" - : p.name === "killed" + : ts.symbolName(p) === "killed" ? "bool" : "f64"; const unit = - p.name === "pid" + ts.symbolName(p) === "pid" ? "undefinedT" - : p.name === "exitCode" || - p.name === "stdin" || - p.name === "stdout" || - p.name === "stderr" + : ts.symbolName(p) === "exitCode" || + ts.symbolName(p) === "stdin" || + ts.symbolName(p) === "stdout" || + ts.symbolName(p) === "stderr" ? "nullT" : null; const mapped = mapType(checker.getTypeOfSymbol(p), ctx); @@ -4444,7 +4558,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (ctx.generalNodeStreams === true && arm.kind === "object" && ((expected === "childStream" && arm.className === "%Readable") || - (expected === "childWriter" && arm.className === "%Writable"))); + (expected === "childWriter" && arm.className === "%Writable"))) || + // Checked-dynamic stream slots: `NodeJS.ReadableStream | null` + // collapses to dyn (the reads still produce the bridged stream). + (ctx.dynNodeStreams === true && + arm.kind === "dyn" && + (expected === "childStream" || expected === "childWriter")); return ( arms !== undefined && arms.some(isExpected) && @@ -4454,8 +4573,8 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const childShaped = props.length > 0 && props.every((p) => { - if (!CHILD_SURFACE.has(p.name) || !childMemberType(p)) return false; - if (CHILD_CORE.has(p.name)) core++; + if (!CHILD_SURFACE.has(ts.symbolName(p)) || !childMemberType(p)) return false; + if (CHILD_CORE.has(ts.symbolName(p))) core++; return true; }); if (childShaped && core >= 2) return CHILD_T; @@ -4660,7 +4779,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null // arity ABI selected by lambdaSignature. Treating an inferred any[] // declaration as a typed packed-array closure would bypass its boxed // spread/error semantics and change coverage classification. - if (rest && decl !== undefined && isJsSourceFile(decl.getSourceFile())) return null; + if (rest && decl !== undefined && isJsSourceFile(ts.getSourceFileOfNode(decl))) return null; // A fixed-tuple rest (`...args: [A, B]`, `...args: Parameters` over // a bound F) flattens to its positional parameters. Pattern rests keep // their packed-record ABI and stay unmapped as values. @@ -4694,8 +4813,8 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null ((ts.isArrowFunction(decl.parent) || ts.isFunctionExpression(decl.parent)) && decl.initializer !== undefined && decl.type === undefined && - !isJsSourceFile(decl.getSourceFile()))) && - !decl.getSourceFile().isDeclarationFile && + !isJsSourceFile(ts.getSourceFileOfNode(decl)))) && + !ts.getSourceFileOfNode(decl).isDeclarationFile && (checker.getTypeOfSymbol(p).flags & ts.TypeFlags.Any) !== 0 ) pt = DYN; @@ -4703,7 +4822,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null decl && ts.isParameter(decl) && ts.isSetAccessorDeclaration(decl.parent) && - isJsSourceFile(decl.getSourceFile()) + isJsSourceFile(ts.getSourceFileOfNode(decl)) ) pt = DYN; // An inferred JavaScript binding pattern consumes an iterable; it @@ -4723,20 +4842,43 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null ts.isIdentifier(construction.expression) && construction.expression.text === "Promise" && constructorSymbol && - constructorSymbol.declarations.some((site) => ctx.isStdlibFile(site.getSourceFile())); + constructorSymbol.declarations.some((site) => ctx.isStdlibFile(ts.getSourceFileOfNode(site))); if ( !nativeExecutor && decl && ts.isParameter(decl) && !decl.dotDotDotToken && !ts.isSetAccessorDeclaration(decl.parent) && - isJsSourceFile(decl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(decl)) && !decl.type && !/@(?:param|type)\b/.test( - decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()), + ts + .getSourceFileOfNode(decl) + .text.slice(decl.parent?.pos ?? decl.pos, ts.tokenStartOfNode(decl)), ) ) pt = DYN; + // A DEFAULTED program-source parameter of a function, arrow or function + // expression that the checker types `any` — a default under an + // `(...args: any[]) => any` wrapper slot (`wrap((limit = 5) => ...)`), + // where the contextual `any` wins over the default's type: its binding + // is already the checked-dynamic kind inside the body + // (dynFallbackType), so the callable's ABI slot is the same dyn value. + // Other `any` parameters keep their existing paths (an object literal + // holding such a callback stays a checked-dynamic object). + if ( + pt === null && + !rest && + decl !== undefined && + ts.isParameter(decl) && + decl.initializer !== undefined && + (ts.isArrowFunction(decl.parent) || + ts.isFunctionExpression(decl.parent) || + ts.isFunctionDeclaration(decl.parent)) && + !ts.getSourceFileOfNode(decl).isDeclarationFile && + (checker.getTypeOfSymbol(p).flags & ts.TypeFlags.Any) !== 0 + ) + pt = DYN; if (pt) pt = jsOpenObjectType(decl, pt, ctx.shapes, ctx.unions); // Belt and braces for non-strict type worlds: an optional param's // ABI slot is always the undefined-armed union (strictNullChecks @@ -4758,7 +4900,8 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null if ( rest && decl !== undefined && - (!decl.getSourceFile().isDeclarationFile || isNodeTestDefaultFunctionParam(decl, ctx)) && + (!ts.getSourceFileOfNode(decl).isDeclarationFile || + isNodeTestDefaultFunctionParam(decl, ctx)) && (pt?.kind === "dyn" || (!pt && isAnyArrayType(checker.getTypeOfSymbol(p), checker))) ) { dynRest = true; @@ -4783,7 +4926,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null const sigDecl = sig.declaration; const jsUnitReturn = sigDecl !== undefined && - isJsSourceFile(sigDecl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(sigDecl)) && (retT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0; let ret = jsUnitReturn || (retT.flags & ts.TypeFlags.Any) !== 0 @@ -4791,24 +4934,33 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null : retT.flags & ts.TypeFlags.Never ? VOID : mapType(retT, ctx); - if (sigDecl && ts.isGetAccessorDeclaration(sigDecl) && isJsSourceFile(sigDecl.getSourceFile())) + if ( + sigDecl && + ts.isGetAccessorDeclaration(sigDecl) && + isJsSourceFile(ts.getSourceFileOfNode(sigDecl)) + ) + ret = DYN; + if (sigDecl && isJsSourceFile(ts.getSourceFileOfNode(sigDecl)) && ret?.kind === "array") ret = DYN; - if (sigDecl && isJsSourceFile(sigDecl.getSourceFile()) && ret?.kind === "array") ret = DYN; if ( sigDecl && ts.isFunctionLike(sigDecl) && - isJsSourceFile(sigDecl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(sigDecl)) && !sigDecl.type && - !/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) && + !/@returns?\b/.test( + ts.getSourceFileOfNode(sigDecl).text.slice(sigDecl.pos, ts.tokenStartOfNode(sigDecl)), + ) && ret?.kind === "record" ) ret = DYN; if ( sigDecl && ts.isFunctionLike(sigDecl) && - isJsSourceFile(sigDecl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(sigDecl)) && !sigDecl.type && - !/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) && + !/@returns?\b/.test( + ts.getSourceFileOfNode(sigDecl).text.slice(sigDecl.pos, ts.tokenStartOfNode(sigDecl)), + ) && (ret?.kind === "date" || (ret?.kind === "union" && ctx.unions.get(ret.unionId)?.arms.some((arm) => arm.kind === "func"))) @@ -4817,7 +4969,7 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null if ( ret && sigDecl && - isJsSourceFile(sigDecl.getSourceFile()) && + isJsSourceFile(ts.getSourceFileOfNode(sigDecl)) && functionCanReturnUndefined(sigDecl) ) ret = withUndefinedArm(ret, ctx.unions) ?? ret; @@ -4850,12 +5002,12 @@ function unionDiscriminant( // no slot to compare (discriminantField refuses tuple shapes too). if (parts.some((part) => ctx.shapes.get(part.mapped.shapeId)?.tuple)) return undefined; for (const candidate of checker.getPropertiesOfType(parts[0]!.source)) { - if (candidate.name.startsWith("__@")) continue; + if (ts.symbolName(candidate).startsWith("__@")) continue; const byTag = new Map(); const owner = new Map(); let complete = true; for (const part of parts) { - const prop = checker.getPropertyOfType(part.source, candidate.name); + const prop = checker.getPropertyOfType(part.source, ts.symbolName(candidate)); if ( !prop || prop.flags & @@ -4886,7 +5038,7 @@ function unionDiscriminant( } if (complete) { return { - field: candidate.name, + field: ts.symbolName(candidate), cases: [...byTag] .sort(([a], [b]) => a - b) .map(([tag, values]) => ({ @@ -4928,7 +5080,8 @@ function mapClassView( if ( declarations.length === 0 || !declarations.every( - (decl) => ts.isInterfaceDeclaration(decl) && !ctx.isStdlibFile(decl.getSourceFile()), + (decl) => + ts.isInterfaceDeclaration(decl) && !ctx.isStdlibFile(ts.getSourceFileOfNode(decl)), ) ) return undefined; @@ -4966,7 +5119,7 @@ function mapClassView( const classBase = declaration && (ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)) && - !declaration.getSourceFile().isDeclarationFile; + !ts.getSourceFileOfNode(declaration).isDeclarationFile; const mapped = classBase ? mapType(base, viewCtx) : mapClassView(base, viewCtx, seen); if (mapped === null) return null; if (mapped === undefined) continue; @@ -4983,7 +5136,7 @@ function mapClassView( ) return null; for (const property of checker.getPropertiesOfType(type)) { - const original = checker.getPropertyOfType(baseType, property.name); + const original = checker.getPropertyOfType(baseType, ts.symbolName(property)); if (!original) return null; if (original === property) continue; const originalDeclarations = original.declarations; @@ -5029,7 +5182,7 @@ function mapCollectionView( if ( declarations.length === 0 || !declarations.every( - (decl) => ts.isInterfaceDeclaration(decl) && !ctx.isStdlibFile(decl.getSourceFile()), + (decl) => ts.isInterfaceDeclaration(decl) && !ctx.isStdlibFile(ts.getSourceFileOfNode(decl)), ) ) return null; @@ -5077,18 +5230,20 @@ function mapCollectionView( const baseSymbol = base.getSymbol(); const native = baseSymbol && - ["Map", "ReadonlyMap", "Set", "ReadonlySet"].includes(baseSymbol.name) && + ["Map", "ReadonlyMap", "Set", "ReadonlySet"].includes(ts.symbolName(baseSymbol)) && baseSymbol.declarations.some( - (decl) => ts.isInterfaceDeclaration(decl) && ctx.isStdlibFile(decl.getSourceFile()), + (decl) => ts.isInterfaceDeclaration(decl) && ctx.isStdlibFile(ts.getSourceFileOfNode(decl)), ); const mapped = native ? mapType(base, baseCtx) : mapCollectionView(base, baseCtx, seen); if (mapped?.kind !== "map" && mapped?.kind !== "set") return null; const hasType = funcOf([mapped.kind === "map" ? mapped.key : mapped.elem], BOOL); for (const property of checker.getPropertiesOfType(type)) { - const own = property.declarations.filter((decl) => !ctx.isStdlibFile(decl.getSourceFile())); + const own = property.declarations.filter( + (decl) => !ctx.isStdlibFile(ts.getSourceFileOfNode(decl)), + ); if (own.length === 0) continue; if ( - property.name !== "has" || + ts.symbolName(property) !== "has" || property.flags & ts.SymbolFlags.Optional || !own.every((decl) => decl.kind === ts.SyntaxKind.MethodSignature) ) @@ -5198,12 +5353,13 @@ function boundSignatureAlias( const { checker, resolveTypeParamTs } = ctx; if (!resolveTypeParamTs) return null; const alias = type.getAliasSymbol(); - if (!alias || (alias.name !== "Parameters" && alias.name !== "ReturnType")) return null; + const aliasName = alias === undefined ? undefined : ts.symbolName(alias); + if (!alias || (aliasName !== "Parameters" && aliasName !== "ReturnType")) return null; const aliasArgs = type.getAliasTypeArguments(); const arg = aliasArgs[0]; if (aliasArgs.length !== 1 || !arg || !(arg.flags & ts.TypeFlags.TypeParameter)) return null; const isLibAlias = alias.declarations.some( - (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ); if (!isLibAlias) return null; const bound = resolveTypeParamTs(arg); @@ -5211,7 +5367,7 @@ function boundSignatureAlias( const sigs = checker.getCallSignatures(bound); const sig = sigs[0]; if (sigs.length !== 1 || !sig || sig.getTypeParameters().length > 0) return null; - return { name: alias.name, sig }; + return { name: aliasName, sig }; } /** One position of a fixed-arity parameter tuple. An optional position @@ -5414,7 +5570,7 @@ function mapGenericAwaitedAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | nul const { resolveTypeParam } = ctx; if (!resolveTypeParam) return null; const alias = type.getAliasSymbol(); - if (!alias || alias.name !== "Awaited") return null; + if (!alias || ts.symbolName(alias) !== "Awaited") return null; const aliasArgs = type.getAliasTypeArguments(); const arg = aliasArgs[0]; if (aliasArgs.length !== 1 || !arg) return null; @@ -5425,7 +5581,7 @@ function mapGenericAwaitedAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | nul if (!(arg.flags & ts.TypeFlags.TypeParameter) && viaSignature?.name !== "ReturnType") return null; // Provenance: the STANDARD LIBRARY's alias, not a user's shadowing one. const isLibAlias = alias.declarations.some( - (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ); if (!isLibAlias) return null; const bound = viaSignature ? mapBoundSignatureAlias(arg, ctx) : resolveTypeParam(arg); @@ -5569,7 +5725,7 @@ function mapBoundMappedType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null const fields: { name: string; type: IrType }[] = []; const order: string[] = []; for (const property of checker.getPropertiesOfType(bound)) { - const name = property.name; + const name = ts.symbolName(property); const evaluated = evalMappedTemplate(template, new Map([[key, name]]), new Map(), ctx); let type = evaluated ? mapType(evaluated, ctx) : null; if (!type) return null; @@ -5837,8 +5993,8 @@ function matchInferThenable( const symbol = t.getSymbol(); return ( symbol !== undefined && - names.includes(symbol.name) && - symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) + names.includes(ts.symbolName(symbol)) && + symbol.declarations.some((d) => ctx.isStdlibFile(ts.getSourceFileOfNode(d))) ); }; if (!stdlibNamed(extendsType, ["PromiseLike", "Promise"])) return undefined; @@ -5870,11 +6026,11 @@ function mentionsMappedParams( function readonlyClassType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const alias = type.getAliasSymbol(); - if (alias?.name !== "Readonly") return null; + if (alias === undefined || ts.symbolName(alias) !== "Readonly") return null; const args = type.getAliasTypeArguments(); if (args.length !== 1 || !args[0]) return null; const isLibAlias = alias.declarations.some( - (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ); if (!isLibAlias) return null; const mapped = mapType(args[0], ctx); @@ -5885,19 +6041,20 @@ function mapGenericUtilityAlias(widened: ts.Type, ctx: TypeMapperCtx): IrType | const { resolveTypeParam, shapes, unions } = ctx; if (!resolveTypeParam) return null; const alias = widened.getAliasSymbol(); - if (!alias || (alias.name !== "Partial" && alias.name !== "Readonly")) return null; + if (!alias || (ts.symbolName(alias) !== "Partial" && ts.symbolName(alias) !== "Readonly")) + return null; const aliasArgs = widened.getAliasTypeArguments(); const arg = aliasArgs[0]; if (aliasArgs.length !== 1 || !arg) return null; if (!(arg.flags & ts.TypeFlags.TypeParameter)) return null; // Provenance: the STANDARD LIBRARY's alias, not a user's shadowing one. const isLibAlias = alias.declarations.some( - (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ); if (!isLibAlias) return null; const bound = resolveTypeParam(arg); if (!bound || bound.kind !== "record") return null; - if (alias.name === "Readonly") return bound; + if (ts.symbolName(alias) === "Readonly") return bound; const shape = shapes.get(bound.shapeId); if (!shape) return null; // Partial over an index-signature shape would need `V | undefined` @@ -6023,7 +6180,7 @@ export function isUnitOnlyTsType(t: ts.Type, resolveTypeParam?: TypeParamResolve /** `ReturnType` over a type parameter bound to a void-returning * function (a variadic wrapper's result slot): a unit-only value. */ function boundVoidReturnType(p: ts.Type, resolveTypeParam?: TypeParamResolver): boolean { - if (!resolveTypeParam || p.getAliasSymbol()?.name !== "ReturnType") return false; + if (!resolveTypeParam || ts.symbolNameOf(p.getAliasSymbol()) !== "ReturnType") return false; const args = p.getAliasTypeArguments(); const arg = args[0]; if (args.length !== 1 || !arg || !(arg.flags & ts.TypeFlags.TypeParameter)) return false; @@ -6114,8 +6271,8 @@ function mapHybridCallableIntersection(widened: ts.Type, ctx: TypeMapperCtx): Ir const pt = mapType(checker.getTypeOfSymbol(p), ctx); if (!pt || pt.kind === "void") return null; - fields.push({ name: p.name, type: pt }); - declaredOrder.push(p.name); + fields.push({ name: ts.symbolName(p), type: pt }); + declaredOrder.push(ts.symbolName(p)); } if (fields.length === 1) return null; // no properties — it is just F fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); @@ -6157,8 +6314,8 @@ function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType "URIError", "AggregateError", "SuppressedError", - ].includes(symbol.name) && - symbol.declarations.some((decl) => ctx.isStdlibFile(decl.getSourceFile())) + ].includes(ts.symbolName(symbol)) && + symbol.declarations.some((decl) => ctx.isStdlibFile(ts.getSourceFileOfNode(decl))) ); }); if (!hasError) return null; @@ -6179,9 +6336,9 @@ function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType return null; for (const property of ctx.checker.getPropertiesOfType(part)) { const expected = - property.name === "errno" + ts.symbolName(property) === "errno" ? "f64" - : ["code", "syscall", "path", "dest"].includes(property.name) + : ["code", "syscall", "path", "dest"].includes(ts.symbolName(property)) ? "string" : null; if (!expected) { @@ -6209,11 +6366,11 @@ function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType for (const property of extra) { const field = mapType(ctx.checker.getTypeOfSymbol(property), ctx); if (!field || field.kind === "void") return null; - if (property.name === "message" || property.name === "name") { + if (ts.symbolName(property) === "message" || ts.symbolName(property) === "name") { if (field.kind !== "string") return null; continue; } - if (property.name === "stack" || property.name === "cause") return null; + if (ts.symbolName(property) === "stack" || ts.symbolName(property) === "cause") return null; if ( !canDynCheckTo( field, @@ -6333,7 +6490,8 @@ function immutableSourceType(type: ts.Type, ctx: TypeMapperCtx): ts.Type | null const { checker } = ctx; const symbol = type.getSymbol(); const decl = symbol ? symbol.declarations[0] : undefined; - if (decl === undefined || !CONTRACT_TYPES_FILE.test(decl.getSourceFile().fileName)) return null; + if (decl === undefined || !CONTRACT_TYPES_FILE.test(ts.getSourceFileOfNode(decl).fileName)) + return null; const props = checker.getPropertiesOfType(type); const seen = new Set(); for (const prop of props) { @@ -6413,14 +6571,14 @@ function recordProvenanceOk(t: ts.Type, ctx: TypeMapperCtx): boolean { .every((property) => property.declarations.every( (decl) => - !decl.getSourceFile().isDeclarationFile || - ctx.isExternalTypeFile(decl.getSourceFile()) || - packageTypeFile(decl.getSourceFile(), ctx), + !ts.getSourceFileOfNode(decl).isDeclarationFile || + ctx.isExternalTypeFile(ts.getSourceFileOfNode(decl)) || + packageTypeFile(ts.getSourceFileOfNode(decl), ctx), ), ); } return !decls.some((d) => { - const sf = d.getSourceFile(); + const sf = ts.getSourceFileOfNode(d); return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx); }); } @@ -6497,7 +6655,7 @@ export function erasedGenericMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType const sigs = ctx.checker.getCallSignatures(t); if (sigs.length !== 1) return undefined; const sig = sigs[0]!; - const own = new Set(sig.getTypeParameters()); + const own = new Set(sig.getTypeParameters()); const outer = ctx.resolveTypeParam; const erased = mapType(t, { ...ctx, @@ -6529,7 +6687,7 @@ export function isOverloadedCallableMemberType(t: ts.Type, checker: ts.TypeCheck if (decl === undefined) return false; if (!ts.isMethodSignatureDeclaration(decl) && !ts.isCallSignatureDeclaration(decl)) return false; - const file = decl.getSourceFile(); + const file = ts.getSourceFileOfNode(decl); return !file.isDeclarationFile && !isJsSourceFile(file); }); } @@ -6591,7 +6749,7 @@ function joinOverloadTypes(types: readonly IrType[], ctx: TypeMapperCtx): IrType export function erasedOverloadMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType | undefined { const funcs: (IrType & { kind: "func" })[] = []; for (const sig of ctx.checker.getCallSignatures(t)) { - const own = new Set(sig.getTypeParameters()); + const own = new Set(sig.getTypeParameters()); const outer = ctx.resolveTypeParam; const mapped = mapCallSignature( sig, @@ -6637,7 +6795,7 @@ export function erasedOverloadMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType function describeOverloadSetBlocker(t: ts.Type, ctx: TypeMapperCtx): string { const { checker } = ctx; for (const sig of checker.getCallSignatures(t)) { - const own = new Set(sig.getTypeParameters()); + const own = new Set(sig.getTypeParameters()); const outer = ctx.resolveTypeParam; const sigCtx: TypeMapperCtx = own.size === 0 @@ -6650,14 +6808,14 @@ function describeOverloadSetBlocker(t: ts.Type, ctx: TypeMapperCtx): string { const mapped = mapCallSignature(sig, sigCtx); if (mapped?.kind === "func" && mapped.rest !== true && mapped.argumentsAll !== true) continue; const decl = sig.declaration; - const spelled = decl ? decl.getText().replace(/\s+/g, " ").replace(/;$/, "") : ""; + const spelled = decl ? ts.getTextOfNode(decl).replace(/\s+/g, " ").replace(/;$/, "") : ""; const which = spelled ? `overload '${spelled}'` : "one overload"; if (mapped?.kind === "func") return `is an overload set, and its ${which} has a rest parameter (an overload set stores one fixed-arity function)`; for (const param of sig.getParameters()) { const paramTs = checker.getTypeOfSymbol(param); if (!mapType(paramTs, sigCtx)) - return `is an overload set, and its ${which} has parameter '${param.name}' of type '${checker.typeToString(paramTs)}', which does not compile`; + return `is an overload set, and its ${which} has parameter '${ts.symbolName(param)}' of type '${checker.typeToString(paramTs)}', which does not compile`; } const retTs = checker.getReturnTypeOfSignature(sig); return `is an overload set, and its ${which} returns '${checker.typeToString(retTs)}', which does not compile`; @@ -6843,9 +7001,7 @@ function dictSpreadLiteralShape( const infos = checker.getIndexInfosOfType(contextual); const unknownValued = infos.length > 0 && - infos.every( - (info) => (info.valueType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0, - ); + infos.every((info) => (info.type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0); if (!unknownValued) return null; } const kinds = props.map((p) => @@ -6857,7 +7013,7 @@ function dictSpreadLiteralShape( if (checker.getPropertiesOfType(t).length === 0) { const infos = checker.getIndexInfosOfType(t); if (infos.length !== 1 || !(infos[0]!.keyType.flags & ts.TypeFlags.String)) return null; - return mapType(infos[0]!.valueType, ctx); + return mapType(infos[0]!.type, ctx); } // Another such literal's type (`{ ...tags, repo }` over a binding // initialized by `{ ...base, plan }`) is stored as the same kind of @@ -6984,7 +7140,7 @@ function spreadMemberNamesTs(checker: ts.TypeChecker, e: ts.Expression): string[ const names: string[] = []; for (const l of literals) { const t = checker.getNonNullableType(checker.getTypeAtLocation(l)); - for (const property of checker.getPropertiesOfType(t)) names.push(property.name); + for (const property of checker.getPropertiesOfType(t)) names.push(ts.symbolName(property)); } return names; } @@ -7036,11 +7192,11 @@ function refinedConstituentShape( ): string | null { if (!widened.isIntersectionType()) return null; const { checker, shapes } = ctx; - const names = checker.getPropertiesOfType(widened).map((prop) => prop.name); + const names = checker.getPropertiesOfType(widened).map((prop) => ts.symbolName(prop)); let found: string | null = null; for (const part of ts.constituentTypes(widened)) { if ((part.flags & ts.TypeFlags.Object) === 0 || shapes.inProgress.has(part)) continue; - const own = new Set(checker.getPropertiesOfType(part).map((prop) => prop.name)); + const own = new Set(checker.getPropertiesOfType(part).map((prop) => ts.symbolName(prop))); if (own.size !== names.length || !names.every((name) => own.has(name))) continue; const mapped = mapType(part, ctx); if (mapped?.kind !== "record") continue; @@ -7063,6 +7219,30 @@ function refinedConstituentShape( return found; } +/** The optional `undefined` members widening adds to an object literal's + * type for properties only its sibling literals declare (`[{ a: 1 }, { b: 2 + * }]` gives the first `b?: undefined`). They are never own keys of the + * literal's value, so they go after the declared members, by name. Where + * the checker lists them is not observable: TypeScript 7.0.2 orders them + * by the literal its checker widened first, TypeScript 7.1 by the sibling + * that declares them. */ +function wideningAddedProperties( + type: ts.Type, + props: readonly ts.Symbol[], + ctx: TypeMapperCtx, +): Set { + const added = new Set(); + const literal = type.getSymbol()?.declarations.find(ts.isObjectLiteralExpression); + if (literal === undefined || literal.properties.nodes.some(ts.isSpreadAssignment)) return added; + for (const prop of props) { + if ((prop.flags & ts.SymbolFlags.Optional) === 0 || prop.declarations.length === 0) continue; + if (prop.declarations.some((d) => d.parent === literal)) continue; + if ((ctx.checker.getTypeOfSymbol(prop).flags & ts.TypeFlags.Undefined) === 0) continue; + added.add(ts.symbolName(prop)); + } + return added; +} + function mapRecordTypeInner( widened: ts.Type, ctx: TypeMapperCtx, @@ -7117,7 +7297,7 @@ function mapRecordTypeInner( let v: IrType | null = null; for (const info of indexInfos) { if (!stringKey(info.keyType) && !(info.keyType.flags & ts.TypeFlags.Number)) return null; - const iv = mapType(info.valueType, ctx); + const iv = mapType(info.type, ctx); // JSONValue>` (a package's own JSON alias) and `Record` @@ -7213,7 +7393,7 @@ function mapRecordTypeInner( if (ctx.classImplementedProtocol?.(widened)) return DYN; // Symbol-keyed records use the checked object's descriptor table. A // checker-internal __@ name is not a string field in a native struct. - if (props.some((property) => property.name.startsWith("__@"))) return DYN; + if (props.some((property) => ts.symbolName(property).startsWith("__@"))) return DYN; // A computed shape with NO members is not a real empty record: inside a // generic body `Partial` resolves to no members at all (keyof T is // still unknown there) — interning `{}` would give a wrong type. The @@ -7272,13 +7452,13 @@ function mapRecordTypeInner( (ts.isObjectLiteralExpression(d.parent) || ts.isInterfaceDeclaration(d.parent) || d.parent.kind === ts.SyntaxKind.TypeLiteral) && - !d.getSourceFile().isDeclarationFile && - !isJsSourceFile(d.getSourceFile()), + !ts.getSourceFileOfNode(d).isDeclarationFile && + !isJsSourceFile(ts.getSourceFileOfNode(d)), ); if (!accessorOwned || (!getDecl && !setDecl)) return null; // Symbol-keyed accessors have no foldable literal name to fill at // the literal — no slot to make. - if (p.name.startsWith("__@")) return null; + if (ts.symbolName(p).startsWith("__@")) return null; // An index-signature shape stores accessor NAMES nowhere the keyed // read/walk machinery can answer (the overflow would miss where // Node dispatches the getter) — those shapes stay unmapped. @@ -7300,8 +7480,9 @@ function mapRecordTypeInner( if (readT !== null && typeKey(readT) !== typeKey(writeT)) return null; } if (readT !== null || getDecl) - fields.push({ name: `%get:${p.name}`, type: funcOf([], readT ?? VOID) }); - if (writeT !== null) fields.push({ name: `%set:${p.name}`, type: funcOf([writeT], VOID) }); + fields.push({ name: `%get:${ts.symbolName(p)}`, type: funcOf([], readT ?? VOID) }); + if (writeT !== null) + fields.push({ name: `%set:${ts.symbolName(p)}`, type: funcOf([writeT], VOID) }); continue; } // Computed shapes carry provenance per MEMBER: a utility type over a @@ -7311,7 +7492,7 @@ function mapRecordTypeInner( if ( computed && p.declarations.some((d) => { - const sf = d.getSourceFile(); + const sf = ts.getSourceFileOfNode(d); return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx); }) ) { @@ -7361,11 +7542,11 @@ function mapRecordTypeInner( // A DATA property spelled like a reserved accessor slot (`{ "%get:x": // v }` — a string-literal key): mapping it would collide with the // accessor dispatch, so the shape stays unmapped. - if (accessorSlotProp(p.name) !== null) return null; + if (accessorSlotProp(ts.symbolName(p)) !== null) return null; // an options record with `signal?: AbortSignal` must not turn its - fields.push({ name: p.name, type: pt }); + fields.push({ name: ts.symbolName(p), type: pt }); } // getPropertiesOfType yields DECLARATION order (interface/alias/literal // source order) — captured before the canonical sort so Object.keys/ @@ -7376,8 +7557,12 @@ function mapRecordTypeInner( // indices, from folded numeric computed keys and quoted numeric keys) // enumerate FIRST ascending, then string keys in insertion order // (OrdinaryOwnPropertyKeys) — Object.keys/JSON/inspect all read it. + const widenedAbsent = wideningAddedProperties(widened, props, ctx); const declaredOrder = esOwnKeyOrder( - fields.filter((f) => accessorSlotProp(f.name) === null).map((f) => f.name), + fields + .filter((f) => accessorSlotProp(f.name) === null && !widenedAbsent.has(f.name)) + .map((f) => f.name) + .concat([...widenedAbsent].sort()), ); // With accessors: every own key in that order, an accessor property // named once for its get/set pair (the checked-dynamic copy lists it). @@ -7460,7 +7645,7 @@ function isNodeTestDefaultFunctionParam(decl: ts.Declaration, ctx: TypeMapperCtx fnType.parent !== undefined && ts.isTypeParameterDeclaration(fnType.parent) && fnType.parent.defaultType === fnType && - ctx.isStdlibFile(decl.getSourceFile()) && + ctx.isStdlibFile(ts.getSourceFileOfNode(decl)) && isDeclaredInAmbientModule(decl, "test") ); } @@ -7536,11 +7721,11 @@ export function describeComponentBlocker(widened: ts.Type, ctx: TypeMapperCtx): const psym = widened.getSymbol(); const container = psym !== undefined && - Object.prototype.hasOwnProperty.call(STDLIB_CONTAINERS, psym.name) && + Object.prototype.hasOwnProperty.call(STDLIB_CONTAINERS, ts.symbolName(psym)) && psym.declarations.some( - (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()), + (d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)), ) - ? psym.name + ? ts.symbolName(psym) : undefined; if (container !== undefined) { const spec = STDLIB_CONTAINERS[container]!; @@ -7682,7 +7867,7 @@ export function describeComponentBlocker(widened: ts.Type, ctx: TypeMapperCtx): for (const p of sig.getParameters()) { const pTs = checker.getTypeOfSymbol(p); if (!mapType(pTs, ctx)) { - return `the function shape is supported, but its parameter '${p.name}' has type '${text(pTs)}', which does not compile`; + return `the function shape is supported, but its parameter '${ts.symbolName(p)}' has type '${text(pTs)}', which does not compile`; } } const retTs = checker.getReturnTypeOfSignature(sig); @@ -7714,14 +7899,14 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx // Accessor members and symbol-keyed members have their own multi-shaped // rules (mapRecordTypeInner) — no single-member claim is honest there. if ((p.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)) !== 0) continue; - if (p.name.startsWith("__@")) continue; + if (ts.symbolName(p).startsWith("__@")) continue; // Computed shapes over LIBRARY members (`Readonly`): the record // fence there is per-member PROVENANCE (mapRecordTypeInner's rule), not // any one member's type — that story stays with the residual fence. if ( computed && p.declarations.some((d) => { - const sf = d.getSourceFile(); + const sf = ts.getSourceFileOfNode(d); return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx); }) ) { @@ -7730,7 +7915,7 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx const fieldTs = checker.getTypeOfSymbol(p); if (isOverloadedCallableMemberType(fieldTs, checker)) { if (erasedOverloadMemberType(fieldTs, ctx) !== undefined) continue; - return `the record shape is supported, but its member '${p.name}' ${describeOverloadSetBlocker(fieldTs, ctx)}`; + return `the record shape is supported, but its member '${ts.symbolName(p)}' ${describeOverloadSetBlocker(fieldTs, ctx)}`; } let pt = isGenericCallableMemberType(fieldTs, checker) ? erasedGenericMemberType(fieldTs, ctx) @@ -7739,7 +7924,7 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)) pt = unitOnlyUnion(ctx.unions); if (!pt || pt.kind === "void") { - return `the record shape is supported, but its member '${p.name}' has type '${checker.typeToString(fieldTs)}', which does not compile`; + return `the record shape is supported, but its member '${ts.symbolName(p)}' has type '${checker.typeToString(fieldTs)}', which does not compile`; } } return null; diff --git a/packages/compiler/src/frontend/worker-target.ts b/packages/compiler/src/frontend/worker-target.ts index 99c97dded..5193f0539 100644 --- a/packages/compiler/src/frontend/worker-target.ts +++ b/packages/compiler/src/frontend/worker-target.ts @@ -84,7 +84,8 @@ export function isWorkerConstructor(program: ts.Program, expression: ts.Expressi export function workerModulePath(program: ts.Program, expression: ts.Expression): string | null { if (ts.isIdentifier(expression) && expression.text === "__filename") { - if (isNodeGlobal(program, expression)) return resolve(expression.getSourceFile().fileName); + if (isNodeGlobal(program, expression)) + return resolve(ts.getSourceFileOfNode(expression).fileName); } const value = staticForkString(program, expression); if (value === null) return null; diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 7b1ef7109..981d14bd3 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -545,6 +545,9 @@ async function compileExecutableNative( dc: features.dc, dynAsync: features.dynAsync, ...(features.dynTier ? { dynTier: true } : {}), + ...(features.streamDyn ? { streamDyn: true } : {}), + ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}), + ...(features.dynBufferNum ? { dynBufferNum: true } : {}), events: features.events, emitter: features.emitter, symbol: features.symbol, @@ -552,6 +555,7 @@ async function compileExecutableNative( searchParams: features.searchParams, qs: features.qs, dynBuiltinClass: features.dynBuiltinClass, + dynBuiltinStream: features.dynBuiltinStream, parseArgs: features.parseArgs, stream: features.stream, net: features.net, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 142b1e25c..00ff5bce1 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -77,6 +77,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "procStream.write", "child.stdioStream", "stream.unboxDyn", + "stream.pipeDyn", + "stream.unpipeDyn", "weakMap.new", "weakSet.new", "dyn.fromEntries", @@ -767,6 +769,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "buffer.poolFromBytes", "buffer.poolFromArray", "buffer.poolFromDyn", + "buffer.poolFromDynRef", // The checked-dynamic compare/equals validators: Node's argument // ladders throw ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE catchably. "buffer.compareChk", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 99ca04ae4..92e6b81ba 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2684,6 +2684,24 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ argTypes: [DYN, STRING], result: { kind: "object", className: "%Duplex" }, }, + /** Checked-dynamic stream slots (dyn-streams.ts): the native stream a + * checked-dynamic value boxes (+1) — a runtime stream with the readable + * (1) or writable (2) side — or null for any other value. Never throws; + * the result union is `%Readable | null` or `%Writable | null`. */ + "stream.dynNative": { argTypes: [DYN, F64], result: VOID }, + /** stream.dynNative for a pipe or unpipe destination: also a + * constructor-function stream's runtime stream, in programs that link + * that unit, unless it overrides write() or end(). */ + "stream.dynNativePipe": { argTypes: [DYN, F64], result: VOID }, + /** readable.pipe(destination, end) into a checked-dynamic destination + * that is not a native stream: [source, the source boxed, destination, + * end]. Node's pipe listener protocol over the destination's members + * (scr_stream_dyn.c); throws what those members throw. */ + "stream.pipeDyn": { argTypes: [null, DYN, DYN, BOOL], result: VOID }, + /** readable.unpipe(destination) for a checked-dynamic destination that + * is not a native stream: [source, destination]. Unpipes the pipe + * stream.pipeDyn registered into it, if any (scr_stream_dyn.c). */ + "stream.unpipeDyn": { argTypes: [null, DYN], result: VOID }, "stream.stdioWritable": { argTypes: [PROCSTREAM_T], result: { kind: "object", className: "%Writable" }, @@ -2861,6 +2879,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "buffer.poolFromBytes": { argTypes: [BYTES_U8], result: BYTES_U8 }, "buffer.poolFromArray": { argTypes: [arrayOf(F64)], result: BYTES_U8 }, "buffer.poolFromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, + /** poolFromDyn that also copies an array crossing by reference. */ + "buffer.poolFromDynRef": { argTypes: [DYN, STRING], result: BYTES_U8 }, "buffer.poolSize": { argTypes: [], result: F64 }, "buffer.setPoolSize": { argTypes: [F64], result: VOID }, /** Buffer.byteLength(string, enc) — enc a NORMALIZED literal like @@ -3589,6 +3609,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * Transform (5) and PassThrough (6) as untyped constructor values, one * per thread (scr_dyn_builtin_class.c). */ "dyn.builtinClass": { argTypes: [F64], result: DYN }, + /** The stream classes (2-6 above) with their stream half: constructor + * functions over them get native stream state + * (scr_dyn_builtin_stream.c). */ + "dyn.builtinStreamClass": { argTypes: [F64], result: DYN }, /** `value instanceof C` for those constructors, over the value's * prototype chain. */ "dyn.builtinInstanceOf": { argTypes: [DYN, F64], result: BOOL }, @@ -4154,6 +4178,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * (inspect, Object.keys/values/entries) then answer through its live * checked view. The receiver is any such record type. Never throws. */ "record.sourceHas": { argTypes: [null], result: BOOL }, + /** A checked-cast fallback site's reads right after the check + * (dynCheck.fallback): whether its value failed validation, and that + * value (+1), taken once. */ + "checkedCast.fallbackTaken": { argTypes: [], result: BOOL }, + "checkedCast.fallbackTake": { argTypes: [], result: DYN }, "insp.key": { argTypes: [STRING], result: STRING }, "insp.moreItems": { argTypes: [F64], result: STRING }, "insp.end": { argTypes: [STRING, STRING, STRING, F64, BOOL, BOOL], result: STRING }, @@ -4307,6 +4336,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * errorBoxer, listenForError] → a pending promise of any[]; the boxers * turn the event's and the 'error' tuple into that array. */ "events.once": { argTypes: [null, STRING, null, null, BOOL], result: VOID }, + /** events.once(emitter, name, { signal }) (scr_events_once_signal.c): + * events.once's arguments plus the checked-dynamic signal (an + * AbortSignal or undefined). */ + "events.onceSignal": { argTypes: [null, STRING, null, null, BOOL, DYN], result: VOID }, /** The resolve-value forms: [ms?, value, ref] → a pending promise of the * value's type that copies the borrowed already-resolved `value` promise * when the timer (or immediate) fires; ref false unrefs it. The result diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 4c5bbd183..499a279b5 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -3824,11 +3824,20 @@ export type IrExpr = /** `exact`: the value is known to be a capsule this program boxed from * exactly the class `type` names (a `dyn.typedRefKeyIs` test guards the * expression), so the check is one load of the capsule's instance. */ + /** `fallback`: a checked-cast fallback site (frontend + * checked-fallback.ts). A value that fails validation does not throw: the + * result is the dummy, and the checked dyn value parks for the + * `checkedCast.fallbackTaken`/`checkedCast.fallbackTake` reads the + * frontend emits right after the site, whose code then continues in the + * dynamic lowering of the region the value flows through. Other + * exceptions (a JSON syntax error) still throw. Only record, array, + * number, string and boolean targets carry it. */ | { kind: "dynCheck"; value: IrExpr; preserveRefs?: true; exact?: true; + fallback?: true; type: IrType; loc: SrcLoc; }; @@ -4214,6 +4223,10 @@ export function canConvertToDyn( (a) => a.kind === "undefinedT" || isJsonSafeType(a, getRecord, getUnion) || + // Composite arms (records holding dyn, Map or Set members) box + // through the walker beside the promise arm: a method answering + // either its result or a promise of it. + canBoxDynComposite(a, getRecord, getUnion, visiting) || isDynTypedRefType(a) || a.kind === "classval" || DYN_HANDLE_KINDS.has(a.kind) || diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index b52afafcb..bb165af26 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -9,6 +9,27 @@ import { } from "./ir.js"; import { everyModuleNode } from "./traverse.js"; +/** The fixed-width Buffer read/write method names (both "UInt" and "Uint" + * spellings) a checked-dynamic method call routes to scr_bytes_dyn.c. */ +export const DYN_BUFFER_NUM_METHODS: ReadonlySet = new Set( + ["read", "write"].flatMap((rw) => [ + ...["UInt", "Uint"].flatMap((u) => + ["8", "16LE", "16BE", "32LE", "32BE"].map((w) => `${rw}${u}${w}`), + ), + ...[ + "Int8", + "Int16LE", + "Int16BE", + "Int32LE", + "Int32BE", + "FloatLE", + "FloatBE", + "DoubleLE", + "DoubleBE", + ].map((n) => `${rw}${n}`), + ]), +); + const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ // DEP0040 rides the dyn-async unit's warning machinery. "punycode.warn", @@ -130,6 +151,7 @@ const LEGACY_TEXT_DECODER_LIB_FNS: ReadonlySet = new Set([ /** Signal-taking readline, stream and timers/promises entry points, which register abort listeners. */ const ABORT_SIGNAL_LIB_FNS: ReadonlySet = new Set([ + "events.onceSignal", "http.clientSignal", "http.requestDyn", "http.requestDynCb", @@ -168,6 +190,10 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "stream.destroyErr", "stream.errored", "stream.prop", + "stream.dynNative", + "stream.dynNativePipe", + "stream.pipeDyn", + "stream.unpipeDyn", "stream.stdioWritable", "stream.unboxDyn", "stream.workerStdio", @@ -204,6 +230,10 @@ const READABLE_DYN_ASYNC_PREFIXES: readonly string[] = ["readable.fromGen", "rea /** Entry points that link the dyn-invoke unit. */ const DYN_INVOKE_LIB_FNS: ReadonlySet = new Set([ + "stream.dynNative", + "stream.dynNativePipe", + "stream.pipeDyn", + "stream.unpipeDyn", "dyn.arrayProtoCall", "dyn.arrayPrototype", "dyn.builtinFunction", @@ -248,6 +278,8 @@ const CHILD_PROCESS_LIB_PREFIXES: readonly string[] = ["child.", "cp.", "spawnRe * emitter unit, so each of them links all five. */ const BUILTIN_CLASS_LIB_FNS: readonly IrLibFn[] = [ "dyn.builtinClass", + // the stream classes, through scr_dyn_builtin_stream.c + "dyn.builtinStreamClass", "dyn.builtinInstanceOf", "dyn.functionInstanceOf", ]; @@ -539,6 +571,10 @@ const RUNTIME_FEATURE_RULES = { dynBuiltinClass: rule({ fns: [...BUILTIN_CLASS_LIB_FNS], }), + /** The stream classes as untyped constructor values with their stream + * half (scr_dyn_builtin_stream.c): native stream state for constructor + * functions, over the stream unit. */ + dynBuiltinStream: rule({ fns: ["dyn.builtinStreamClass"] }), dynInvoke: rule({ initial: (mod) => mod.workers === true, fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS], @@ -553,6 +589,16 @@ const RUNTIME_FEATURE_RULES = { /** The program has dynamic-tier jsval operations (js.* builtins, the * jsval pass): links the scr_js_* units (two-tier design I3). */ dynTier: rule({ prefixes: ["js."] }), + /** Checked-dynamic stream slots test values and pipe into JavaScript + * stream objects (scr_stream_dyn.c). */ + streamDyn: rule({ + fns: ["stream.dynNative", "stream.dynNativePipe", "stream.pipeDyn", "stream.unpipeDyn"], + }), + /** Fixed-width Buffer read/write methods called on checked-dynamic + * values (scr_bytes_dyn.c). */ + dynBufferNum: rule({ + expr: (node, _tables) => node.kind === "dynInvoke" && DYN_BUFFER_NUM_METHODS.has(node.method), + }), /** The stream unit emits through the emitter unit, also when the * program constructs no stream (a pipeline of functions). */ emitter: rule({ @@ -571,6 +617,8 @@ const RUNTIME_FEATURE_RULES = { fns: ["events.addAbortListener", ...ABORT_SIGNAL_LIB_FNS], prefixes: ["event."], }), + /** events.once(emitter, name, { signal }) (scr_events_once_signal.c). */ + eventsOnceSignal: rule({ fns: ["events.onceSignal"] }), fetch: rule({ prefixes: ["fetch."] }), fileHandle: rule({ types: ["fileHandle"] }), fsWatch: rule({ prefixes: ["fs.watch", "watcher."], types: ["fsWatcher"] }), @@ -663,7 +711,7 @@ const RUNTIME_FEATURE_RULES = { }), stream: rule({ initial: (mod) => (mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name)), - fns: [...STREAM_LIB_FNS], + fns: [...STREAM_LIB_FNS, "dyn.builtinStreamClass"], prefixes: STREAM_LIB_PREFIXES, }), /** A Dirent row converted to dyn carries Node's Symbol(type) key @@ -708,10 +756,12 @@ function noRuntimeFeatures(): RuntimeFeatures { dnsPromises: false, dynAsync: false, dynBuiltinClass: false, + dynBuiltinStream: false, dynInvoke: false, dynTier: false, emitter: false, eventTarget: false, + eventsOnceSignal: false, fetch: false, fileHandle: false, fsWatch: false, @@ -731,6 +781,8 @@ function noRuntimeFeatures(): RuntimeFeatures { searchParams: false, stdioExtended: false, stream: false, + streamDyn: false, + dynBufferNum: false, symbol: false, tls: false, tlsCa: false, @@ -873,6 +925,14 @@ export function moduleUsesStream(mod: IrModule): boolean { return scanRuntimeFeatures(mod, "stream").stream; } +/** True when the program links scr_dyn_builtin_stream.c (stream classes + * as untyped constructor values): its runtime streams box through that + * unit's view and checked casts to the stream classes accept constructor- + * function streams. Programs without it keep their exact code. */ +export function moduleUsesDynBuiltinStream(mod: IrModule): boolean { + return scanRuntimeFeatures(mod, "dynBuiltinStream").dynBuiltinStream; +} + export const NPM_COMPRESS_MIN = 1024; export function moduleUsesZlib(mod: IrModule): boolean { diff --git a/packages/compiler/src/ir/traverse.test.ts b/packages/compiler/src/ir/traverse.test.ts index 874e7846e..fbe84644a 100644 --- a/packages/compiler/src/ir/traverse.test.ts +++ b/packages/compiler/src/ir/traverse.test.ts @@ -28,7 +28,8 @@ const program = createProgram([path], { noLib: true, noResolve: true, types: [] const source = program.getSourceFile(path)!; afterAll(() => program.dispose()); function childType(type: ts.TypeNode): boolean { - if (ts.isTypeReferenceNode(type)) return ["IrExpr", "IrStmt"].includes(type.typeName.getText()); + if (ts.isTypeReferenceNode(type)) + return ["IrExpr", "IrStmt"].includes(ts.getTextOfNode(type.typeName)); if (ts.isArrayTypeNode(type)) return childType(type.elementType); if (ts.isUnionTypeNode(type)) return type.types.nodes.some(childType); if (ts.isTypeLiteralNode(type)) @@ -47,15 +48,15 @@ for (const name of ["IrExpr", "IrStmt"]) { for (const variant of alias.type.types.nodes) { if (!ts.isTypeLiteralNode(variant)) throw new Error("IR variants must be records"); const members = variant.members.nodes.filter(ts.isPropertySignature); - const tag = members.find((member) => member.name.getText() === "kind")!.type!; - const kind = JSON.parse(tag.getText()) as string; + const tag = members.find((member) => ts.getTextOfNode(member.name) === "kind")!.type!; + const kind = JSON.parse(ts.getTextOfNode(tag)) as string; test(`${name}.${kind}: every declared child is visited and rewritten, metadata is preserved`, () => { for (const presence of [true, false]) { const expected: (IrExpr | IrStmt)[] = []; function value(type: ts.TypeNode): unknown { if (ts.isTypeReferenceNode(type)) { const node = - type.typeName.getText() === "IrExpr" + ts.getTextOfNode(type.typeName) === "IrExpr" ? num(expected.length) : statement(expected.length); expected.push(node); @@ -72,7 +73,7 @@ for (const name of ["IrExpr", "IrStmt"]) { }; for (const member of type.members.nodes) { if (ts.isPropertySignature(member) && member.type && childType(member.type)) - result[member.name.getText()] = value(member.type); + result[ts.getTextOfNode(member.name)] = value(member.type); } return result; } @@ -86,7 +87,7 @@ for (const name of ["IrExpr", "IrStmt"]) { }; for (const member of members) { if (member.type && childType(member.type) && (presence || !member.postfixToken)) - input[member.name.getText()] = value(member.type); + input[ts.getTextOfNode(member.name)] = value(member.type); } const before = structuredClone(input); const seen: (IrExpr | IrStmt)[] = []; @@ -135,7 +136,7 @@ for (const name of ["IrExpr", "IrStmt"]) { expect(output.loc).toBe(loc); expect((output as unknown as Record)["metadata"]).toBe(input["metadata"]); for (const member of members) { - const key = member.name.getText(); + const key = ts.getTextOfNode(member.name); if (member.postfixToken && !presence) expect(Object.hasOwn(output, key)).toBe(false); } } @@ -217,7 +218,7 @@ test("module traversal covers every declared type slot", () => { const active = new Set(); function slots(node: ts.TypeNode): unknown { if (ts.isTypeReferenceNode(node)) { - const name = node.typeName.getText(); + const name = ts.getTextOfNode(node.typeName); if (name === "IrType") { const value: IrType = { kind: "regex" }; expected.push(value); @@ -268,7 +269,7 @@ test("module traversal covers every declared type slot", () => { for (const member of members) { if (!ts.isPropertySignature(member) || !member.type) continue; const value = slots(member.type); - if (value !== undefined) result[member.name.getText()] = value; + if (value !== undefined) result[ts.getTextOfNode(member.name)] = value; } return result; } @@ -316,20 +317,20 @@ test("type traversal visits all structural children and leaves named shapes as r if (!ts.isTypeLiteralNode(variant)) throw new Error("IrType variants must be records"); const members = variant.members.nodes.filter(ts.isPropertySignature); const kind = JSON.parse( - members.find((member) => member.name.getText() === "kind")!.type!.getText(), + ts.getTextOfNode(members.find((member) => ts.getTextOfNode(member.name) === "kind")!.type!), ) as string; const node: Record = { kind }; const expected: IrType[] = []; for (const member of members) { if (!member.type) continue; const child = - ts.isTypeReferenceNode(member.type) && member.type.typeName.getText() === "IrType"; + ts.isTypeReferenceNode(member.type) && ts.getTextOfNode(member.type.typeName) === "IrType"; const array = - ts.isArrayTypeNode(member.type) && member.type.elementType.getText() === "IrType"; + ts.isArrayTypeNode(member.type) && ts.getTextOfNode(member.type.elementType) === "IrType"; if (!child && !array) continue; const value: IrType = { kind: "fileHandle" }; expected.push(value); - node[member.name.getText()] = array ? [value] : value; + node[ts.getTextOfNode(member.name)] = array ? [value] : value; } const seen: IrType[] = []; expect( diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 835dfa8ba..f75f6f46e 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -4159,7 +4159,7 @@ function validateFunction( if (!ok) err(`libCall ${e.fn} must be a promise of its ${cryptoPromise} value`, e.loc); return true; } - if (e.fn === "events.once") { + if (e.fn === "events.once" || e.fn === "events.onceSignal") { const [recv, , boxer, errorBoxer] = e.args; const arrayT = e.type.kind === "promise" ? e.type.inner : null; const boxes = (b: IrExpr | undefined) => @@ -4170,7 +4170,7 @@ function validateFunction( !boxes(boxer) || !boxes(errorBoxer) ) { - err(`libCall events.once must box its event tuples into the promised array`, e.loc); + err(`libCall ${e.fn} must box its event tuples into the promised array`, e.loc); } return true; } @@ -5233,6 +5233,21 @@ function validateFunction( } return true; } + if (e.fn === "stream.dynNative" || e.fn === "stream.dynNativePipe") { + const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined; + const side = e.args[1]; + const className = + side?.kind === "numLit" ? (side.value === 1 ? "%Readable" : "%Writable") : null; + const ok = + def && + className !== null && + e.args[0]?.type.kind === "dyn" && + def.arms.length === 2 && + def.arms.some((a) => a.kind === "object" && a.className === className) && + def.arms.some((a) => a.kind === "nullT"); + if (!ok) err(`libCall ${e.fn} must return its side's stream class or null`, e.loc); + return true; + } if (e.fn === "child.stdioStream") { const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined; const slot = e.args[1]; diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index 3fa934da1..a9056a093 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -90,6 +90,7 @@ async function main(): Promise { searchParams: false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index a2a15179f..a6b4be63c 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -144,6 +144,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "searchParams", "qs", "dynBuiltinClass", + "dynBuiltinStream", "parseArgs", "stream", "net", diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index ea9690ce7..d32d8564c 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -77,6 +77,9 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( eventTarget: false, wasm: false, dynTier: false, + streamDyn: false, + eventsOnceSignal: false, + dynBufferNum: false, warningReplace: false, stdioExtended: false, regex: true, @@ -97,6 +100,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( searchParams: false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false, diff --git a/packages/compiler/src/wasm/discover.ts b/packages/compiler/src/wasm/discover.ts index 931bc1dac..de38bf64f 100644 --- a/packages/compiler/src/wasm/discover.ts +++ b/packages/compiler/src/wasm/discover.ts @@ -159,7 +159,7 @@ class Discovery { if (!paths && !base64 && !bytes) return; const file = sf.fileName; const lineOf = (node: ts.Node): number => - ts.getLineAndCharacterOfPosition(sf, node.getStart()).line + 1; + ts.getLineAndCharacterOfPosition(sf, ts.tokenStartOfNode(node)).line + 1; ts.walkPreorder( sf, (node) => { diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index b7cb3ab94..cb908d408 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -1,6 +1,6 @@ { "schemaVersion": 2, - "compilerVersion": "0.3.6", + "compilerVersion": "0.4.0", "coverage": [ "Entries are projected mechanically from the compiler's own decision tables: the diagnostics registry, the unsupported-syntax dispatch tables, the stdlib and node-builtin lowering tables, and the supported-builtin-module list. Nothing is hand-maintained; the manifest regenerates byte-identically from the source tree at this version.", "Absence from this manifest means 'not projected', never 'unsupported'. Surfaces lowered through dedicated code paths rather than tables are not yet projected: console, JSON, Promise/async and the timer surface, the net/http/tls/https/dgram/dns/assert/test/stream/readline module member surfaces, template literals, the regex slice, global functions (parseInt, parseFloat, isNaN, isFinite), and the process surface outside its ambient slice.", diff --git a/packages/compiler/test/checked-cast-fallback.test.ts b/packages/compiler/test/checked-cast-fallback.test.ts new file mode 100644 index 000000000..1eda2a7af --- /dev/null +++ b/packages/compiler/test/checked-cast-fallback.test.ts @@ -0,0 +1,172 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule } from "../src/index.js"; +import type { IrExpr, IrFunction, IrModule, IrStmt } from "../src/ir/ir.js"; +import { everyStmtList } from "../src/ir/traverse.js"; +import type { TierMode } from "../src/frontend/tier-analysis.js"; + +async function lowerSource(source: string, tier?: TierMode): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-cast-fallback-")); + try { + const entry = join(dir, "main.ts"); + await writeFile(entry, source); + const outPath = join(dir, "main.ir.json"); + const result = await compile(entry, { + outPath, + outDir: dir, + outputKind: "ir", + ...(tier ? { tier } : {}), + }); + if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n")); + return deserializeModule(await readFile(outPath, "utf8")); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function fnNamed(mod: IrModule, name: string): IrFunction { + const fn = mod.functions.find((f) => f.name === name); + if (!fn) throw new Error(`no function ${name}`); + return fn; +} + +function checks(fn: IrFunction): (IrExpr & { kind: "dynCheck" })[] { + const found: (IrExpr & { kind: "dynCheck" })[] = []; + everyStmtList(fn.body, { + expr: (e) => { + if (e.kind === "dynCheck") found.push(e); + return true; + }, + stmt: () => true, + }); + return found; +} + +/** The `if (flag) dynamic else static` split a fallback tail lowers to. */ +function splits(fn: IrFunction): (IrStmt & { kind: "if" })[] { + const found: (IrStmt & { kind: "if" })[] = []; + everyStmtList(fn.body, { + expr: () => true, + stmt: (s) => { + if ( + s.kind === "if" && + s.cond.kind === "varRef" && + s.cond.localId.startsWith("%fb.") && + s.else_ !== null + ) + found.push(s); + return true; + }, + }); + return found; +} + +const listener = ` +interface Order { deployment: { id: string; plan: string } } +interface Req { buildOrder: Order; traceCarrier?: Record } +function accept(body: string): string { + let request: Partial; + try { + request = JSON.parse(body) as Partial; + } catch { + return "400"; + } + const id = request.buildOrder?.deployment?.id; + return id ? "200 " + id : "400 missing"; +} +console.log(accept('{"buildOrder":{"deployment":{"id":"d"}}}')); +`; + +test("a block-local checked cast continues in a dynamic tail behind one flag", async () => { + const fn = fnNamed(await lowerSource(listener), "accept"); + const [check] = checks(fn); + expect(check?.fallback).toBe(true); + const split = splits(fn); + expect(split).toHaveLength(1); + // The static tail reads the record natively; the dynamic one reads the + // failed value's shadow. + const shadowReads = (body: IrStmt[]): number => { + let n = 0; + everyStmtList(body, { + expr: (e) => { + if (e.kind === "varRef" && e.localId.startsWith("%fbv.")) n++; + return true; + }, + stmt: () => true, + }); + return n; + }; + expect(shadowReads(split[0]!.then)).toBeGreaterThan(0); + expect(shadowReads(split[0]!.else_!)).toBe(0); +}); + +test("--tier=static-only keeps the throwing cast", async () => { + const fn = fnNamed(await lowerSource(listener, "static-only"), "accept"); + expect(checks(fn).some((c) => c.fallback)).toBe(false); + expect(splits(fn)).toHaveLength(0); + expect(fn.locals.some((l) => l.id.startsWith("%fb"))).toBe(false); +}); + +test("casts whose binding is read outside the code after them, or written again, keep throwing", async () => { + const mod = await lowerSource(` +interface P { x: number } +const top = JSON.parse('{"x":1}') as P; +console.log(top.x); +function before(text: string): number { + let p: P | undefined; + for (let i = 0; i < 2; i++) { + if (p) console.log(p.x); + p = JSON.parse(text) as P; + } + return p?.x ?? 0; +} +function rewritten(text: string): number { + let p = JSON.parse(text) as P; + p = { x: 2 }; + return p.x; +} +function hoisted(text: string): number { + const p = JSON.parse(text) as P; + function read(): number { return p.x; } + return read(); +} +console.log(before('{"x":1}'), rewritten('{"x":1}'), hoisted('{"x":1}')); +`); + for (const name of ["before", "rewritten", "hoisted"]) { + const fn = fnNamed(mod, name); + expect( + checks(fn).some((c) => c.fallback), + name, + ).toBe(false); + expect( + fn.locals.some((l) => l.id.startsWith("%fb")), + name, + ).toBe(false); + } + expect(mod.functions.flatMap(checks).some((c) => c.fallback)).toBe(false); +}); + +test("sequential casts split the code after them at most twice", async () => { + const source = ` +interface P { x: number } +function sum(a: string, b: string, c: string, d: string): number { + const p = JSON.parse(a) as P; + const q = JSON.parse(b) as P; + const r = JSON.parse(c) as P; + const s = JSON.parse(d) as P; + return p.x + q.x + r.x + s.x; +} +console.log(sum('{"x":1}', '{"x":2}', '{"x":3}', '{"x":4}')); +`; + const fn = fnNamed(await lowerSource(source), "sum"); + // p splits the code after it, and q splits each copy of that code; r and + // s keep the throwing cast instead of doubling the copies again. + expect(splits(fn)).toHaveLength(3); + const binding = (c: { loc: { start: number } }) => + /const (\w) = [^\n]*$/.exec(source.slice(0, c.loc.start))?.[1]; + const casts = checks(fn); + expect(new Set(casts.filter((c) => c.fallback).map(binding))).toEqual(new Set(["p", "q"])); + expect(new Set(casts.filter((c) => !c.fallback).map(binding))).toEqual(new Set(["r", "s"])); +}); diff --git a/packages/compiler/test/json-callbacks.test.ts b/packages/compiler/test/json-callbacks.test.ts index 308d626e3..d10721d0b 100644 --- a/packages/compiler/test/json-callbacks.test.ts +++ b/packages/compiler/test/json-callbacks.test.ts @@ -53,7 +53,8 @@ for (const backend of ["llvm"] as const) { } catch (error) { if (error instanceof Error) console.log(error.message); } - // A checked cast still validates the result AFTER revival. + // A checked cast validates the result AFTER revival; a revived value + // that does not fit continues as that value (checked-cast fallback). try { const value = JSON.parse('{"n":1}', (key: string, value: unknown): unknown => key === "n" ? "wrong" : value) as { n: number }; console.log(value.n); @@ -75,6 +76,8 @@ for (const backend of ["llvm"] as const) { console.log(result === undefined, typeof result); } local(); + // Values that do not fit continue as their parsed values, every + // iteration (and release cleanly under the sanitizer lane). for (let i = 0; i < 20; i++) { try { const record = JSON.parse('{"opaque":{"deep":[1,2]},"typed":"wrong"}') as { opaque: unknown; typed: number }; @@ -105,13 +108,12 @@ for (const backend of ["llvm"] as const) { expect(result.stdout.toString()).toBe( [ "JSON.parse reviver deleting array elements is not supported yet", - "true", + "wrong", "true", "true", "true", "true undefined", - "true", - "true", + ...Array.from({ length: 20 }, () => ["wrong", "2"]).flat(), "1", "", ].join("\n"), diff --git a/packages/compiler/test/speculative-hoisted-functions.test.ts b/packages/compiler/test/speculative-hoisted-functions.test.ts index a58310b2a..79a56dcde 100644 --- a/packages/compiler/test/speculative-hoisted-functions.test.ts +++ b/packages/compiler/test/speculative-hoisted-functions.test.ts @@ -9,7 +9,9 @@ import type { IrModule } from "../src/ir/ir.js"; // list above the constructor. The initializer lowers speculatively first, // which hoists the constructor and the helper it calls. Those bodies are // kept, so their failing statements must keep runtime fences; dropping -// them left `isNum` without its return (an SC9002 trap at load). +// them left `isNum` without its return (an SC9002 trap at load). isNum's +// return reads the prototype through Reflect.getPrototypeOf, which has no +// lowering, so it fences. const source = ` function clone(config) { var P = (Num.prototype = { constructor: Num, toString: null }), @@ -20,7 +22,7 @@ function clone(config) { x.v = isNum(v) ? 0 : v * unit; } function isNum(v) { - return v instanceof Num || (!!v && v._isNum === true); + return Reflect.getPrototypeOf(v) === P || (!!v && v._isNum === true); } P._isNum = true; return Num; @@ -59,6 +61,6 @@ test("speculatively hoisted JavaScript functions keep their statement fences", a const mod = await lower(); const isNum = mod.functions.find((fn) => fn.name.endsWith("_isNum")); expect(isNum).toBeDefined(); - expect(fenceCodes(isNum!.body)).toEqual(["SC1090"]); + expect(fenceCodes(isNum!.body)).toEqual(["SC2020"]); expect(mod.functions.flatMap((fn) => fenceCodes(fn.body))).not.toContain("SC9002"); }); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5819-node-stream-dyn-direct-pipe/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5819-node-stream-dyn-direct-pipe/main.ts.json new file mode 100644 index 000000000..85cf02e2e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5819-node-stream-dyn-direct-pipe/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/5819-node-stream-dyn-direct-pipe/sources.js", + "/tests/corpus/5819-node-stream-dyn-direct-pipe/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5820-node-stream-dyn-constructor-overrides/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5820-node-stream-dyn-constructor-overrides/main.ts.json new file mode 100644 index 000000000..49f4052b1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5820-node-stream-dyn-constructor-overrides/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/5820-node-stream-dyn-constructor-overrides/sources.js", + "/tests/corpus/5820-node-stream-dyn-constructor-overrides/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6419-untyped-async-package/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6419-untyped-async-package/main.ts.json new file mode 100644 index 000000000..ab4ee5819 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6419-untyped-async-package/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6419-untyped-async-package/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-any-destructuring.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-any-destructuring.ts.json new file mode 100644 index 000000000..eb1120cae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-any-destructuring.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6470-any-destructuring.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-any-method-chains.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-any-method-chains.ts.json new file mode 100644 index 000000000..463bc3a67 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-any-method-chains.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6471-any-method-chains.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-namespace-destructuring/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-namespace-destructuring/main.ts.json new file mode 100644 index 000000000..ec90234c8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-namespace-destructuring/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6472-namespace-destructuring/lib.ts", + "/tests/corpus/6472-namespace-destructuring/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6473-any-parameter-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6473-any-parameter-values.ts.json new file mode 100644 index 000000000..1747655f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6473-any-parameter-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6473-any-parameter-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6474-async-any-callbacks/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6474-async-any-callbacks/main.ts.json new file mode 100644 index 000000000..6a7e7a966 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6474-async-any-callbacks/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6474-async-any-callbacks/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6475-js-promise-resolve-adoption/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6475-js-promise-resolve-adoption/main.ts.json new file mode 100644 index 000000000..d27bf5fd6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6475-js-promise-resolve-adoption/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6475-js-promise-resolve-adoption/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-node-stream-dyn-readable/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-node-stream-dyn-readable/main.ts.json new file mode 100644 index 000000000..b3698b91c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-node-stream-dyn-readable/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6480-node-stream-dyn-readable/sources.js", + "/tests/corpus/6480-node-stream-dyn-readable/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6481-node-stream-dyn-writable/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6481-node-stream-dyn-writable/main.ts.json new file mode 100644 index 000000000..45b8ebb8a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6481-node-stream-dyn-writable/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6481-node-stream-dyn-writable/sources.js", + "/tests/corpus/6481-node-stream-dyn-writable/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6482-node-stream-dyn-helpers/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6482-node-stream-dyn-helpers/main.ts.json new file mode 100644 index 000000000..b45958c33 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6482-node-stream-dyn-helpers/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6482-node-stream-dyn-helpers/sources.js", + "/tests/corpus/6482-node-stream-dyn-helpers/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-events-once-signal.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-events-once-signal.ts.json new file mode 100644 index 000000000..9c99e2ab0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-events-once-signal.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6490-events-once-signal.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-js-named-promise-executor/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-js-named-promise-executor/main.ts.json new file mode 100644 index 000000000..dd95018f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6490-js-named-promise-executor/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6490-js-named-promise-executor/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-catch-any-bindings.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-catch-any-bindings.ts.json new file mode 100644 index 000000000..bc69a66e3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-catch-any-bindings.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6491-catch-any-bindings.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-events-once-streams.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-events-once-streams.ts.json new file mode 100644 index 000000000..4fdbe7a1d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6491-events-once-streams.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6491-events-once-streams.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6492-promise-race-combined.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6492-promise-race-combined.ts.json new file mode 100644 index 000000000..8fa2ceb9a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6492-promise-race-combined.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6492-promise-race-combined.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6493-stream-function-callbacks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6493-stream-function-callbacks.ts.json new file mode 100644 index 000000000..bb755b9a7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6493-stream-function-callbacks.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6493-stream-function-callbacks.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-default-export-class-members/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-default-export-class-members/main.ts.json new file mode 100644 index 000000000..24634cff1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-default-export-class-members/main.ts.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/6494-default-export-class-members/lib/date.js", + "/tests/corpus/6494-default-export-class-members/lib/error.js", + "/tests/corpus/6494-default-export-class-members/lib/index.js", + "/tests/corpus/6494-default-export-class-members/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-events-once-dyn-stream-slot/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-events-once-dyn-stream-slot/main.ts.json new file mode 100644 index 000000000..77f43443e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6494-events-once-dyn-stream-slot/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6494-events-once-dyn-stream-slot/sources.js", + "/tests/corpus/6494-events-once-dyn-stream-slot/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6550-npm-boolean-bitwise/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6550-npm-boolean-bitwise/main.ts.json new file mode 100644 index 000000000..a34eeedc0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6550-npm-boolean-bitwise/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6550-npm-boolean-bitwise/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-constructor-function-readable.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-constructor-function-readable.cjs.json new file mode 100644 index 000000000..48964b0a6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-constructor-function-readable.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6560-constructor-function-readable.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-npm-js-capability-probes/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-npm-js-capability-probes/main.ts.json new file mode 100644 index 000000000..62b900d3c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6560-npm-js-capability-probes/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6560-npm-js-capability-probes/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6561-constructor-function-writable.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6561-constructor-function-writable.cjs.json new file mode 100644 index 000000000..603b083d1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6561-constructor-function-writable.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6561-constructor-function-writable.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6562-constructor-function-transform.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6562-constructor-function-transform.cjs.json new file mode 100644 index 000000000..c5d3d1a62 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6562-constructor-function-transform.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6562-constructor-function-transform.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6563-constructor-function-stream-errors.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6563-constructor-function-stream-errors.cjs.json new file mode 100644 index 000000000..6e7f693c1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6563-constructor-function-stream-errors.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6563-constructor-function-stream-errors.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6564-constructor-function-stream-late-hooks.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6564-constructor-function-stream-late-hooks.cjs.json new file mode 100644 index 000000000..52d8e55ff --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6564-constructor-function-stream-late-hooks.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6564-constructor-function-stream-late-hooks.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6565-rebindable-function-import-cycle/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6565-rebindable-function-import-cycle/main.ts.json new file mode 100644 index 000000000..f0d46c0bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6565-rebindable-function-import-cycle/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6565-rebindable-function-import-cycle/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-require-mixed-declarations.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-require-mixed-declarations.cjs.json new file mode 100644 index 000000000..fd070103c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6570-require-mixed-declarations.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6570-require-mixed-declarations.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6592-promise-armed-method-values/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6592-promise-armed-method-values/main.ts.json new file mode 100644 index 000000000..1fb3f21bc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6592-promise-armed-method-values/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6592-promise-armed-method-values/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/761-any-objects.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/761-any-objects.ts.json new file mode 100644 index 000000000..aa907b2ae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/761-any-objects.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/761-any-objects.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-id-instances-as-streams.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-id-instances-as-streams.ts.json new file mode 100644 index 000000000..01d871cb3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-id-instances-as-streams.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-id-instances-as-streams.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-extra-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-extra-keys.ts.json new file mode 100644 index 000000000..8f5694905 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-extra-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-parse-fallback-extra-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-flows.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-flows.ts.json new file mode 100644 index 000000000..2084d8a65 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-flows.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-parse-fallback-flows.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-missing.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-missing.ts.json new file mode 100644 index 000000000..17562fe47 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-missing.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-parse-fallback-missing.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-nested.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-nested.ts.json new file mode 100644 index 000000000..3585687e2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-nested.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-parse-fallback-nested.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-partial.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-partial.ts.json new file mode 100644 index 000000000..26bae0445 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-partial.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-parse-fallback-partial.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-wrong-types.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-wrong-types.ts.json new file mode 100644 index 000000000..fd445ea1c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/json-parse-fallback-wrong-types.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/json-parse-fallback-wrong-types.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-761-any-objects.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-761-any-objects.ts.json deleted file mode 100644 index ed04bcd16..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/retired-engine-761-any-objects.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/retired-engine-761-any-objects.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/events-once-signal-race.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/events-once-signal-race.ts.json new file mode 100644 index 000000000..e5c444b78 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/events-once-signal-race.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/events-once-signal-race.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/process-versions-absent.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/process-versions-absent.ts.json new file mode 100644 index 000000000..1d842a3fe --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/process-versions-absent.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/process-versions-absent.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/parity.test.ts b/packages/compiler/test/ts7/parity.test.ts index 4904f2720..978de5cc2 100644 --- a/packages/compiler/test/ts7/parity.test.ts +++ b/packages/compiler/test/ts7/parity.test.ts @@ -1,4 +1,5 @@ import { Ts7Host } from "../../src/frontend/ts7/program-adapter.js"; +import { symbolName, symbolNameOf } from "../../src/frontend/ts7/semantic-model.js"; /* The two-world agreement battery: every fixture program is parsed and * checked through BOTH worlds — typescript@5.9.3 in-process and the TS7 * adapter over tsgo — and the worlds must agree on structure (walk order, @@ -331,7 +332,7 @@ for (const battery of ALL_BATTERIES) { if (s5[a] !== undefined && isFixtureSymbol(w(), s5[a]!) && s5[a]!.name !== "export=") { // finding 5: the module.exports receiver's symbol NAME differs in // JS files (5.9.3 "export=", tsgo "module") — pinned separately. - expect(s7[a]!.name, `${file}#${ids[a]} symbol name`).toBe(s5[a]!.name); + expect(symbolName(s7[a]!), `${file}#${ids[a]} symbol name`).toBe(s5[a]!.name); } for (let b = a + 1; b < ids.length; b++) { if (s5[a] === undefined || s5[b] === undefined) continue; @@ -432,7 +433,7 @@ test("finding pinned: tsgo does not synthesize CJS expando members; module resol const moduleIdx = n5.findIndex((n) => ts5.isIdentifier(n) && n.getText() === "module"); expect(moduleIdx).toBeGreaterThanOrEqual(0); expect(w.c5.getSymbolAtLocation(n5[moduleIdx]!)?.name).toBe("export="); - expect(w.c7.getSymbolAtLocation(n7[moduleIdx]!)?.name).toBe("module"); + expect(symbolNameOf(w.c7.getSymbolAtLocation(n7[moduleIdx]!))).toBe("module"); }); test("getAwaitedType shim agrees with 5.9.3 across the async battery (pinned limitation aside)", () => { @@ -487,7 +488,7 @@ test("symbol declarations resolve back into the same client AST", () => { const sym7 = w.c7.getSymbolAtLocation(n7[i]!); expect(sym7).toBeDefined(); const aliased = w.c7.getAliasedSymbol(sym7!); - expect(aliased.name).toBe("answer"); + expect(symbolName(aliased)).toBe("answer"); const declHandle = aliased.declarationHandles[0]; expect(declHandle).toBeDefined(); const decl = declHandle!.resolve(w.p7.project); diff --git a/packages/compiler/test/ts7/rpc-api.test.ts b/packages/compiler/test/ts7/rpc-api.test.ts index a8b4aebce..a39c71321 100644 --- a/packages/compiler/test/ts7/rpc-api.test.ts +++ b/packages/compiler/test/ts7/rpc-api.test.ts @@ -14,7 +14,21 @@ import { SemanticSignature, SemanticSymbol, SemanticType, + symbolName, } from "../../src/frontend/ts7/semantic-model.js"; +import { ts7Diagnostic, type Ts7Diagnostic } from "../../src/frontend/ts7/diagnostic.js"; +import type { Ts7DiagnosticData } from "../../src/frontend/ts7/session-schema.generated.js"; + +// The SDK's symbols have `name`; scriptc's give it with symbolName, which reads `escapedName`, as the +// SDK's symbols also have it. +function nameOf(symbol: SdkSymbol): string { + return symbolName(symbol as unknown as SemanticSymbol); +} + +// scriptc's diagnostics have every member set; the SDK's leave absent ones out. +function diagnostics(list: readonly unknown[]): Ts7Diagnostic[] { + return list.map((item) => ts7Diagnostic(item as Ts7DiagnosticData)); +} // scriptc's symbols keep their declarations' node handles in // `declarationHandles`, and the SDK's in `declarations`. @@ -51,12 +65,12 @@ function programFacts(snapshot: Snapshot, config: string, path: string) { options: project.program.getCompilerOptions(), text: file.text, type: checker.typeToString(type), - symbol: symbol.name, + symbol: nameOf(symbol), declaration: declarationHandlesOf(symbol)[0]!.resolve()!.getText(file), - syntactic: project.program.getSyntacticDiagnostics(path), - semantic: project.program.getSemanticDiagnostics(path), - globals: project.program.getGlobalDiagnostics(), - config: project.program.getConfigFileParsingDiagnostics(), + syntactic: diagnostics(project.program.getSyntacticDiagnostics(path)), + semantic: diagnostics(project.program.getSemanticDiagnostics(path)), + globals: diagnostics(project.program.getGlobalDiagnostics()), + config: diagnostics(project.program.getConfigFileParsingDiagnostics()), defaultLibrary: project.program.isSourceFileDefaultLibrary(file), externalLibrary: project.program.isSourceFileFromExternalLibrary(file), }; @@ -233,7 +247,7 @@ test("native semantic registries preserve project scope and snapshot lifetimes", .declarationList.declarations, )[0], texts: [a.checker.typeToString(typeA), b.checker.typeToString(typeB)], - parameters: signatureA.getParameters().map((symbol) => symbol.name), + parameters: signatureA.getParameters().map(nameOf), }, symbolA, typeA, @@ -258,7 +272,7 @@ test("native semantic registries preserve project scope and snapshot lifetimes", first.dispose(); expect(() => actual.typeA.getSymbol()).toThrow("disposed"); expect(() => declarationHandlesOf(actual.symbolA)[0]!.resolve()).toThrow("disposed"); - expect(next.signatureA.getParameters()[0]!.name).toBe("value"); + expect(nameOf(next.signatureA.getParameters()[0]!)).toBe("value"); expect(second.getProject(firstConfig)!.checker.typeToString(next.typeA)).toBe("42"); second.dispose(); expect(() => next.signatureA.getParameters()).toThrow("disposed"); diff --git a/packages/experimental/package.json b/packages/experimental/package.json index 327f6fc97..f32a1085b 100644 --- a/packages/experimental/package.json +++ b/packages/experimental/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/experimental", - "version": "0.3.6", + "version": "0.4.0", "description": "Experimental scriptc APIs, with Node.js implementations: lifetime regions for ProgramLifetime objects, intern tables shared between threads, and per-thread slots on shared objects", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-darwin-arm64/package.json b/packages/llvm-darwin-arm64/package.json index 28865dc35..5330a5397 100644 --- a/packages/llvm-darwin-arm64/package.json +++ b/packages/llvm-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-darwin-arm64", - "version": "0.3.6", + "version": "0.4.0", "description": "Pinned LLVM code-generation helper for scriptc on macOS arm64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-darwin-x64/package.json b/packages/llvm-darwin-x64/package.json index 8d03e25cb..6282710b9 100644 --- a/packages/llvm-darwin-x64/package.json +++ b/packages/llvm-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-darwin-x64", - "version": "0.3.6", + "version": "0.4.0", "description": "Pinned LLVM code-generation helper for scriptc on macOS x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-arm64-gnu/package.json b/packages/llvm-linux-arm64-gnu/package.json index a6782dce1..59b4af11f 100644 --- a/packages/llvm-linux-arm64-gnu/package.json +++ b/packages/llvm-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-arm64-gnu", - "version": "0.3.6", + "version": "0.4.0", "description": "Pinned LLVM code-generation helper for scriptc on Linux arm64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-arm64-musl/package.json b/packages/llvm-linux-arm64-musl/package.json index c4ac49e9b..5ea482253 100644 --- a/packages/llvm-linux-arm64-musl/package.json +++ b/packages/llvm-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-arm64-musl", - "version": "0.3.6", + "version": "0.4.0", "description": "Statically linked LLVM code-generation helper for scriptc on Linux arm64 musl", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-x64-gnu/package.json b/packages/llvm-linux-x64-gnu/package.json index 6136a8e07..a17b6f393 100644 --- a/packages/llvm-linux-x64-gnu/package.json +++ b/packages/llvm-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-x64-gnu", - "version": "0.3.6", + "version": "0.4.0", "description": "Pinned LLVM code-generation helper for scriptc on Linux x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-linux-x64-musl/package.json b/packages/llvm-linux-x64-musl/package.json index 02d994cdd..161ca0cb7 100644 --- a/packages/llvm-linux-x64-musl/package.json +++ b/packages/llvm-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-linux-x64-musl", - "version": "0.3.6", + "version": "0.4.0", "description": "Statically linked LLVM code-generation helper for scriptc on Linux x64 musl", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/llvm-win32-x64-msvc/package.json b/packages/llvm-win32-x64-msvc/package.json index fe1329515..cfcc28a01 100644 --- a/packages/llvm-win32-x64-msvc/package.json +++ b/packages/llvm-win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/llvm-win32-x64-msvc", - "version": "0.3.6", + "version": "0.4.0", "description": "Pinned LLVM code-generation helper for scriptc on Windows x64", "license": "Apache-2.0 WITH LLVM-exception", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-android-arm64/package.json b/packages/runtime-android-arm64/package.json index 155c6ce87..2d8e22c92 100644 --- a/packages/runtime-android-arm64/package.json +++ b/packages/runtime-android-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-android-arm64", - "version": "0.3.6", + "version": "0.4.0", "description": "Precompiled scriptc library runtime pack for Android arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-darwin-arm64/package.json b/packages/runtime-darwin-arm64/package.json index a36cd273b..68c81b246 100644 --- a/packages/runtime-darwin-arm64/package.json +++ b/packages/runtime-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-darwin-arm64", - "version": "0.3.6", + "version": "0.4.0", "description": "Precompiled scriptc runtime pack for macOS arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-darwin-x64/package.json b/packages/runtime-darwin-x64/package.json index f81f21139..54c0d7835 100644 --- a/packages/runtime-darwin-x64/package.json +++ b/packages/runtime-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-darwin-x64", - "version": "0.3.6", + "version": "0.4.0", "description": "Precompiled scriptc runtime pack for macOS x64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-ios-arm64/package.json b/packages/runtime-ios-arm64/package.json index c76226c6f..eb3d0ee27 100644 --- a/packages/runtime-ios-arm64/package.json +++ b/packages/runtime-ios-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-ios-arm64", - "version": "0.3.6", + "version": "0.4.0", "description": "Precompiled scriptc library runtime pack for iOS arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-ios-simulator-arm64/package.json b/packages/runtime-ios-simulator-arm64/package.json index ec916922e..3b805bb6f 100644 --- a/packages/runtime-ios-simulator-arm64/package.json +++ b/packages/runtime-ios-simulator-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-ios-simulator-arm64", - "version": "0.3.6", + "version": "0.4.0", "description": "Precompiled scriptc library runtime pack for iOS simulator arm64", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-arm64-gnu/package.json b/packages/runtime-linux-arm64-gnu/package.json index bb972ebbe..ad50f2fe3 100644 --- a/packages/runtime-linux-arm64-gnu/package.json +++ b/packages/runtime-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-arm64-gnu", - "version": "0.3.6", + "version": "0.4.0", "description": "Precompiled scriptc runtime pack for Linux arm64 glibc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-arm64-musl/package.json b/packages/runtime-linux-arm64-musl/package.json index 69110991a..4d4c23fdd 100644 --- a/packages/runtime-linux-arm64-musl/package.json +++ b/packages/runtime-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-arm64-musl", - "version": "0.3.6", + "version": "0.4.0", "description": "Precompiled scriptc runtime pack for Linux arm64 musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-x64-gnu/package.json b/packages/runtime-linux-x64-gnu/package.json index ceb7b975f..ab8780c39 100644 --- a/packages/runtime-linux-x64-gnu/package.json +++ b/packages/runtime-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-x64-gnu", - "version": "0.3.6", + "version": "0.4.0", "description": "Precompiled scriptc runtime pack for Linux x64 glibc", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-linux-x64-musl/package.json b/packages/runtime-linux-x64-musl/package.json index 21f83ccee..862b46187 100644 --- a/packages/runtime-linux-x64-musl/package.json +++ b/packages/runtime-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-linux-x64-musl", - "version": "0.3.6", + "version": "0.4.0", "description": "Precompiled scriptc runtime pack for Linux x64 musl", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 28ccf0f59..fc56fbe91 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -61,6 +61,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_inspect.c", "inspect"], ["scr_console_native.c", "inspect"], ["scr_dyn_invoke.c", any("dynInvoke", "nativeFetch")], + // Fixed-width Buffer methods called on checked-dynamic values. + ["scr_bytes_dyn.c", all("dynBufferNum", "dynInvoke")], ["scr_dc.c", "dc"], // The dynamic tier's jsval values (two-tier design I3): linked only into // programs whose IR has dynamic-tier operations. @@ -78,6 +80,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_readline.c", "events"], ["scr_readline_tty.c", all("events", "regex")], ["scr_events_emitter.c", "emitter"], + // events.once with options registers through the emitter and event-target units. + ["scr_events_once_signal.c", "eventsOnceSignal"], ["scr_dyn_handle.c", any("emitter", "netEffective")], ["scr_symbol.c", "symbol"], ["scr_bigint_assert.c", all("assert", "bigint")], @@ -94,11 +98,26 @@ export const OPTIONAL_RUNTIME_SOURCES = [ // through the dyn-invoke unit, inspect unhandled 'error' payloads and // create EventEmitter's private symbols. ["scr_dyn_builtin_class.c", all("dynBuiltinClass", "inspect", "dynInvoke", "symbol", "emitter")], + // Their stream half also references the stream unit. + [ + "scr_dyn_builtin_stream.c", + all( + "dynBuiltinStream", + "dynBuiltinClass", + "stream", + "inspect", + "dynInvoke", + "symbol", + "emitter", + ), + ], ["scr_stream.c", "stream"], // The zlib stream classes reference both the zlib engine and the stream unit. ["scr_zlib_stream.c", all("zlib", "stream")], // The Readable operators await through the dyn-async unit. ["scr_stream_ops.c", all("stream", "dynAsync")], + // Checked-dynamic stream slots call JavaScript members through the dyn-invoke unit. + ["scr_stream_dyn.c", "streamDyn"], // fs.ReadStream / fs.WriteStream reference only the stream unit and base units. ["scr_fs_stream.c", "stream"], // FileHandle streams reference both the stream unit and FileHandle. diff --git a/packages/runtime-wasm32-wasi/package.json b/packages/runtime-wasm32-wasi/package.json index fa073ecf2..48c57d59a 100644 --- a/packages/runtime-wasm32-wasi/package.json +++ b/packages/runtime-wasm32-wasi/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-wasm32-wasi", - "version": "0.3.6", + "version": "0.4.0", "description": "Precompiled scriptc runtime pack for WASI Preview 1", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime-win32-x64-msvc/package.json b/packages/runtime-win32-x64-msvc/package.json index 4f6017b29..fe9ba6892 100644 --- a/packages/runtime-win32-x64-msvc/package.json +++ b/packages/runtime-win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime-win32-x64-msvc", - "version": "0.3.6", + "version": "0.4.0", "description": "Precompiled scriptc runtime pack for Windows x64 MSVC", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 0b9559931..b63f4fad3 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/runtime", - "version": "0.3.6", + "version": "0.4.0", "description": "scriptc native runtime sources and vendored dependencies", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index b7b913d7f..c0f6ed120 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -389,6 +389,7 @@ ScrBytes *scr_buffer_from_str_checked(const ScrStr *s, const ScrStr *enc); ScrBytes *scr_buffer_from_bytes(const ScrBytes *src); /* a copy of any typed array's values */ ScrBytes *scr_buffer_from_arr(const ScrArr *arr); /* number[] */ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding); +ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *encoding); ScrBytes *scr_buffer_concat(const ScrArr *list); ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total); /* The untyped-list forms: Node's ERR_INVALID_ARG_TYPE checks on the list diff --git a/packages/runtime/src/scr_bytes_dyn.c b/packages/runtime/src/scr_bytes_dyn.c new file mode 100644 index 000000000..d4b29574b --- /dev/null +++ b/packages/runtime/src/scr_bytes_dyn.c @@ -0,0 +1,101 @@ +/* The fixed-width read/write methods of a Buffer held as a checked-dynamic + * value: a JavaScript dependency's `buf.writeUInt32LE(v, 0)` on a buffer it + * got back untyped (yazl). The compiler routes only the dyn method calls + * that name one of these methods here, so programs without them link + * nothing new; any other receiver takes the ordinary dispatch. */ +#include "scr_runtime.h" + +#include +#include +#include + +static const struct { + const char *name; + ScrBytesNumKind kind; + bool le; +} scr_bytes_dyn_names[] = { + { "UInt8", SCR_BN_U8, false }, { "Uint8", SCR_BN_U8, false }, + { "Int8", SCR_BN_I8, false }, { "UInt16LE", SCR_BN_U16, true }, + { "Uint16LE", SCR_BN_U16, true }, { "UInt16BE", SCR_BN_U16, false }, + { "Uint16BE", SCR_BN_U16, false }, { "Int16LE", SCR_BN_I16, true }, + { "Int16BE", SCR_BN_I16, false }, { "UInt32LE", SCR_BN_U32, true }, + { "Uint32LE", SCR_BN_U32, true }, { "UInt32BE", SCR_BN_U32, false }, + { "Uint32BE", SCR_BN_U32, false }, { "Int32LE", SCR_BN_I32, true }, + { "Int32BE", SCR_BN_I32, false }, { "FloatLE", SCR_BN_F32, true }, + { "FloatBE", SCR_BN_F32, false }, { "DoubleLE", SCR_BN_F64, true }, + { "DoubleBE", SCR_BN_F64, false }, +}; + +static size_t scr_bytes_dyn_width(ScrBytesNumKind kind) { + switch (kind) { + case SCR_BN_U8: + case SCR_BN_I8: + return 1; + case SCR_BN_U16: + case SCR_BN_I16: + return 2; + case SCR_BN_F64: + return 8; + default: + return 4; + } +} + +/* Node's checkInt range test and message, before the offset is looked at. */ +static bool scr_bytes_dyn_value_in_range(double value, size_t width, bool sign) { + double max = sign ? exp2((double)(8 * width - 1)) - 1 : exp2((double)(8 * width)) - 1; + double min = sign ? -exp2((double)(8 * width - 1)) : 0; + if (!(value > max || value < min)) return true; + char recv[48]; + scr_num_received(value, recv); + char minb[32], maxb[32]; + scr_f64_to_str(min, minb); + scr_f64_to_str(max, maxb); + char msg[160]; + int mlen = snprintf(msg, sizeof msg, + "The value of \"value\" is out of range. It must be >= %s and <= %s. Received %s", + minb, maxb, recv); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)mlen, "ERR_OUT_OF_RANGE"); + return false; +} + +/* Node's order: a write converts the value with ToNumber and checks its + * range, then the offset must be a number (absent means 0), then the + * bounds. +1, or NULL with the exception pending. */ +static ScrDyn *scr_bytes_dyn_num_method(ScrBytes *b, bool read, size_t at, ScrDyn *const *args, + size_t argc) { + ScrBytesNumKind kind = scr_bytes_dyn_names[at].kind; + double value = 0; + if (!read) { + if (!scr_dyn_number_coerce_js(argc > 0 ? args[0] : scr_dyn_undefined(), &value)) return NULL; + if (kind != SCR_BN_F32 && kind != SCR_BN_F64) { + bool sign = kind == SCR_BN_I8 || kind == SCR_BN_I16 || kind == SCR_BN_I32; + if (!scr_bytes_dyn_value_in_range(value, scr_bytes_dyn_width(kind), sign)) return NULL; + } + } + size_t offset_at = read ? 0 : 1; + double offset = 0; + if (argc > offset_at && args[offset_at]->kind != SCR_DYN_UNDEF) { + if (args[offset_at]->kind != SCR_DYN_NUM) { + scr_dyn_arg_type_fail("offset", "of type number", args[offset_at]); + return NULL; + } + offset = args[offset_at]->v.num; + } + bool le = scr_bytes_dyn_names[at].le; + double out = read ? scr_bytes_read_num(b, offset, kind, le) : scr_bytes_write_num(b, value, offset, kind, le); + return scr_exc_pending() ? NULL : scr_dyn_new_num(out); +} + +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { + if (recv->kind == SCR_DYN_BYTES && recv->buffer && !recv->v.bytes->is_data_view) { + bool read = !strncmp(method, "read", 4); + const char *rest = method + (read ? 4 : 5); + for (size_t at = 0; at < sizeof scr_bytes_dyn_names / sizeof scr_bytes_dyn_names[0]; at++) { + if (!strcmp(rest, scr_bytes_dyn_names[at].name)) + return scr_bytes_dyn_num_method(recv->v.bytes, read, at, args, argc); + } + } + return scr_dyn_invoke_prepared(recv, callee, method, args, argc, what); +} diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 4d7ef5f67..c852388e9 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1350,6 +1350,24 @@ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding return scr_buffer_pool_adopt(out); } +/* scr_buffer_from_dyn_pooled for values that can be a typed array crossing + * an untyped slot by reference (a number[] passed to an untyped parameter, + * as yazl's Buffer.from(something, encoding) receives it): the array copies + * like the array it is. Used by the runtime-encoding Buffer.from forms. */ +ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *encoding) { + if (value->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(value) != NULL) { + ScrDyn *materialized = scr_dyn_typed_ref_materialize(value); + if (scr_exc_pending()) { + scr_dyn_release(materialized); + return NULL; + } + ScrBytes *out = scr_buffer_from_dyn_pooled(materialized, encoding); + scr_dyn_release(materialized); + return out; + } + return scr_buffer_from_dyn_pooled(value, encoding); +} + /* ── the checked-dynamic Buffer compare/equals validators ────────────── * Node's argument ladders for buf.equals / buf.compare / Buffer.compare * over dyn-boxed arguments (the invalid-input probes: string needles, diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index fd5d64c81..bed185097 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -165,8 +165,9 @@ void scr_weak_dispose(void *object) { #include #include -/* Every heap object's first member is `size_t rc`. */ -#define SCR_RC(obj) (*(size_t *)(obj)) +/* Every heap object's first member is `size_t rc`; the collector works on + * its count (SCR_RC_COUNT). */ +#define SCR_RC(obj) SCR_RC_COUNT(obj) static void scr_cyc_oom(void) { scr_trap("scriptc: out of memory\n"); @@ -1171,7 +1172,7 @@ void scr_cyc_on_release(void *obj) { #define SCR_CYC_WALK_GEN(h) scr_cyc_gen(h) #endif #define SCR_CYC_SKIP(child) \ - ((child) == NULL || SCR_RC(child) == SIZE_MAX \ + ((child) == NULL || SCR_RC(child) == SCR_RC_IMMORTAL \ || SCR_CYC_WALK_GEN(scr_cyc_hdr(child)) > scr_gen_limit) /* The walks are depth-first and recursive up to SCR_CYC_WALK_DEPTH, where @@ -1213,7 +1214,7 @@ SCR_CYC_ARRAY_WALK(scr_scan_array, scr_scan_visit) static inline __attribute__((always_inline)) void scr_mark_gray(void *obj, uintptr_t depth); static void scr_mg_visit(void *child, void *ctx) { - if (child == NULL || SCR_RC(child) == SIZE_MAX) return; + if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return; ScrCycHdr *h = scr_cyc_hdr(child); if (SCR_CYC_WALK_GEN(h) > scr_gen_limit) { scr_cyc_push_work(&scr_xgen_edges, scr_gray_holder); @@ -1372,7 +1373,7 @@ static SCR_TL size_t scr_xg_freed = 0; static void scr_xg_release(void *obj); static void scr_xg_child_visit(void *child, void *ctx) { (void)ctx; - if (child == NULL || SCR_RC(child) == SIZE_MAX) return; + if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return; scr_xg_release(child); } @@ -1597,7 +1598,7 @@ static void scr_yin_leave(ScrCycHdr *h, void *obj) { static void scr_yin_count_visit(void *child, void *ctx) { (void)ctx; - if (child == NULL || SCR_RC(child) == SIZE_MAX) return; + if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return; ScrCycHdr *h = scr_cyc_hdr(child); if (scr_cyc_gen(h) == SCR_CYC_YOUNG) scr_yin_counts[scr_cyc_buf_index(h)]++; } @@ -1605,7 +1606,7 @@ static void scr_yin_count_visit(void *child, void *ctx) { /* Marked young objects are GRAY (young objects are otherwise BLACK). */ static void scr_yin_mark_visit(void *child, void *ctx) { (void)ctx; - if (child == NULL || SCR_RC(child) == SIZE_MAX) return; + if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return; ScrCycHdr *h = scr_cyc_hdr(child); if (scr_cyc_gen(h) != SCR_CYC_YOUNG || scr_cyc_color(h) == SCR_CYC_GRAY) return; scr_cyc_set_color(h, SCR_CYC_GRAY); @@ -1617,7 +1618,7 @@ static void scr_yin_mark_visit(void *child, void *ctx) { * the count until every teardown has run. */ static void scr_yin_dead_visit(void *child, void *ctx) { (void)ctx; - if (child == NULL || SCR_RC(child) == SIZE_MAX) return; + if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return; ScrCycHdr *h = scr_cyc_hdr(child); if (scr_cyc_gen(h) == SCR_CYC_YOUNG && scr_cyc_color(h) != SCR_CYC_GRAY) return; scr_yin_push(&scr_yin_xrel, child); diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 28d642501..0ac280436 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1134,6 +1134,10 @@ bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b); * result owned (+1). MAY THROW (NULL with the exception pending). */ ScrDyn *scr_dyn_prepare_method(ScrDyn *recv, const char *method); ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +/* scr_dyn_invoke_prepared for the fixed-width Buffer read/write method names + * (scr_bytes_dyn.c): a Buffer receiver runs the method, any other takes the + * ordinary dispatch. */ +ScrDyn *scr_dyn_invoke_buffer_num(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what); /* Keyed read on a FUNC node: the own-property table first (defineProperties * writes land there), then "name" (the box's best-effort static name; "" @@ -1159,6 +1163,43 @@ ScrDyn *scr_dyn_new_class(ScrClassObj *cls, const char *type_key); ScrDyn *scr_dyn_builtin_class(double kind); bool scr_dyn_builtin_instanceof(ScrDyn *value, double kind); bool scr_dyn_function_instanceof(ScrDyn *value, ScrDyn *callee); +/* The stream half of those constructors (scr_dyn_builtin_stream.c, linked + * with the dynBuiltinStream feature), registered with + * scr_dyn_builtin_class_streams before the first stream class value is + * read: construct runs `Readable.call(this, opts)` and the other stream + * kinds (+1 or NULL with an exception pending), prototype installs a stream + * kind's methods, and event answers EventEmitter.prototype's methods for a + * receiver with stream state (true when it handled the call; *result +1 or + * NULL with an exception pending). */ +enum { + SCR_EE_M_ON, SCR_EE_M_PREPEND, SCR_EE_M_ONCE, SCR_EE_M_PREPEND_ONCE, SCR_EE_M_OFF, + SCR_EE_M_REMOVE_ALL, SCR_EE_M_EMIT, SCR_EE_M_LISTENERS, SCR_EE_M_RAW_LISTENERS, + SCR_EE_M_LISTENER_COUNT, SCR_EE_M_EVENT_NAMES, SCR_EE_M_SET_MAX, SCR_EE_M_GET_MAX, +}; +typedef struct ScrDynStreamOps { + ScrDyn *(*construct)(int kind, ScrDyn *self, ScrDyn *const *args, size_t argc); + void (*prototype)(int kind, ScrDyn *prototype); + bool (*event)(int method, ScrDyn *self, ScrDyn *const *args, size_t argc, ScrDyn **result); +} ScrDynStreamOps; +void scr_dyn_builtin_class_streams(const ScrDynStreamOps *ops); +/* A stream class value with its stream half (scr_dyn_builtin_stream.c), + * +1; the stream a JavaScript value stands for (a constructor-function + * stream or a boxed runtime stream), +1 or NULL; and the constructor- + * function object of a runtime stream, +1 or NULL. */ +ScrDyn *scr_dyn_builtin_stream_class(double kind); +struct ScrStream *scr_dyn_builtin_stream_of(const ScrDyn *value); +ScrDyn *scr_dyn_builtin_stream_object(struct ScrStream *s); +/* The untyped view of a runtime stream in programs that link that unit + * (+1), a checked cast to a runtime stream class kind (+1 or NULL), and + * the stream.unboxDyn libCall in those programs (scr_stream_unbox_dyn's + * contract, also taking constructor-function streams). */ +ScrDyn *scr_dyn_builtin_stream_view(struct ScrStream *s); +struct ScrStream *scr_dyn_builtin_stream_check(const ScrDyn *value, double kind); +struct ScrStream *scr_dyn_builtin_stream_unbox(const ScrDyn *value, const ScrStr *message); +/* stream.dynNative in those programs: scr_stream_dyn_native's contract, + * also taking constructor-function streams that keep the runtime's write + * and end (+1 or NULL). */ +struct ScrStream *scr_dyn_builtin_stream_dyn_native(const ScrDyn *value, double side); bool scr_dyn_class_is_key(const ScrDyn *value, const ScrStr *type_key); bool scr_dyn_class_is(const ScrDyn *value, const char *type_key); /* Calls a dyn value: a non-function kind throws the catchable TypeError @@ -1204,6 +1245,16 @@ ScrClassObj *scr_dyn_class_check(const ScrDyn *value, const char *type_key, cons * scriptc-specific behavior — JS `as` never checks (SEMANTICS.md). */ void scr_dyn_check_fail(const ScrDynPath *path, const char *want, const ScrDyn *got); +/* Checked-cast fallback (a `JSON.parse(text) as T` whose value fails + * validation keeps its dynamic value instead of throwing). Right after a + * fallback site's check, scr_dyn_check_recover takes back the exception the + * check raised (Node runs nothing at a cast, so nothing it raises is + * observable) and parks the checked value, which the lowered code reads + * with scr_dyn_check_fallback_taken and takes (+1) with _take. */ +bool scr_dyn_check_recover(ScrDyn *d); +bool scr_dyn_check_fallback_taken(void); +ScrDyn *scr_dyn_check_fallback_take(void); + /* Matchers and builders of recursive shapes (records and arrays whose type * reaches itself) track the dyn values they visit, so cyclic values match * and convert without unbounded recursion. Every matcher and builder of a diff --git a/packages/runtime/src/scr_dyn_builtin_class.c b/packages/runtime/src/scr_dyn_builtin_class.c index 6d894615b..2f947048a 100644 --- a/packages/runtime/src/scr_dyn_builtin_class.c +++ b/packages/runtime/src/scr_dyn_builtin_class.c @@ -15,9 +15,13 @@ * Stream is Node's legacy constructor: EventEmitter.call(this, opts) and a * prototype that inherits EventEmitter's. Readable, Writable, Duplex, * Transform and PassThrough have prototypes chained as in Node, so - * util.inherits onto them and instanceof work, but their native state has - * no dyn object form: calling one with a receiver throws the "not - * supported yet" Error where the construction runs. + * util.inherits onto them and instanceof work. Their stream half (the + * prototypes' methods and `Readable.call(this, opts)`) lives in + * scr_dyn_builtin_stream.c, which registers itself through + * scr_dyn_builtin_class_streams in programs that link it; without it, + * calling one with a receiver throws the "not supported yet" Error where + * the construction runs. EventEmitter's methods hand a receiver with + * stream state to that unit, whose listeners live in the runtime stream. * * One constructor per kind and thread, so every boxing of the same class * value is the same function. All arguments are borrowed. */ @@ -59,6 +63,14 @@ static SCR_TL ScrDyn *bc_symbols[BC_SYMBOLS]; static ScrDyn *bc_class(int kind); +/* The stream half, once registered (scr_dyn_builtin_stream.c). */ +static SCR_TL const ScrDynStreamOps *bc_stream_ops; + +/* EventEmitter.prototype's method on a receiver with stream state. */ +static bool bc_stream_event(int method, ScrDyn *self, ScrDyn *const *args, size_t argc, ScrDyn **result) { + return bc_stream_ops != NULL && bc_stream_ops->event(method, self, args, argc, result); +} + /* ── property helpers ───────────────────────────────────────────────── */ static ScrDyn *bc_key(const char *name) { @@ -585,7 +597,12 @@ static ScrDyn *bc_once_wrap(ScrDyn *target, ScrDyn *type, ScrDyn *listener) { static ScrDyn *bc_on(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; ScrDyn *self = bc_this(); - ScrDyn *result = bc_require_object(self, "_events") + ScrDyn *result; + if (bc_stream_event(SCR_EE_M_ON, self, args, argc, &result)) { + scr_dyn_release(self); + return result; + } + result = bc_require_object(self, "_events") ? bc_add_listener(self, bc_arg(args, argc, 0), bc_arg(args, argc, 1), false) : NULL; scr_dyn_release(self); return result; @@ -594,7 +611,12 @@ static ScrDyn *bc_on(ScrClosure *closure, ScrDyn *const *args, size_t argc) { static ScrDyn *bc_prepend(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; ScrDyn *self = bc_this(); - ScrDyn *result = bc_require_object(self, "_events") + ScrDyn *result; + if (bc_stream_event(SCR_EE_M_PREPEND, self, args, argc, &result)) { + scr_dyn_release(self); + return result; + } + result = bc_require_object(self, "_events") ? bc_add_listener(self, bc_arg(args, argc, 0), bc_arg(args, argc, 1), true) : NULL; scr_dyn_release(self); return result; @@ -605,6 +627,10 @@ static ScrDyn *bc_once_common(ScrDyn *const *args, size_t argc, bool prepend) { ScrDyn *type = bc_arg(args, argc, 0); ScrDyn *listener = bc_arg(args, argc, 1); ScrDyn *result = NULL; + if (bc_stream_event(prepend ? SCR_EE_M_PREPEND_ONCE : SCR_EE_M_ONCE, self, args, argc, &result)) { + scr_dyn_release(self); + return result; + } if (bc_check_listener(listener) && bc_require_object(self, "_events")) { ScrDyn *wrapper = bc_once_wrap(self, type, listener); if (wrapper) { @@ -629,7 +655,12 @@ static ScrDyn *bc_prepend_once(ScrClosure *closure, ScrDyn *const *args, size_t static ScrDyn *bc_off(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; ScrDyn *self = bc_this(); - ScrDyn *result = bc_require_object(self, "_events") + ScrDyn *result; + if (bc_stream_event(SCR_EE_M_OFF, self, args, argc, &result)) { + scr_dyn_release(self); + return result; + } + result = bc_require_object(self, "_events") ? bc_remove_listener(self, bc_arg(args, argc, 0), bc_arg(args, argc, 1)) : NULL; scr_dyn_release(self); return result; @@ -641,6 +672,11 @@ static ScrDyn *bc_off(ScrClosure *closure, ScrDyn *const *args, size_t argc) { static ScrDyn *bc_remove_all(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; ScrDyn *self = bc_this(); + ScrDyn *handled; + if (bc_stream_event(SCR_EE_M_REMOVE_ALL, self, args, argc, &handled)) { + scr_dyn_release(self); + return handled; + } if (!bc_require_object(self, "_events")) { scr_dyn_release(self); return NULL; @@ -759,6 +795,11 @@ fail: static ScrDyn *bc_emit(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; ScrDyn *self = bc_this(); + ScrDyn *handled; + if (bc_stream_event(SCR_EE_M_EMIT, self, args, argc, &handled)) { + scr_dyn_release(self); + return handled; + } if (!bc_require_object(self, "_events")) { scr_dyn_release(self); return NULL; @@ -844,6 +885,11 @@ fail_self: /* _listeners(target, type, unwrap): a fresh array. */ static ScrDyn *bc_listeners_of(bool unwrap, ScrDyn *const *args, size_t argc) { ScrDyn *self = bc_this(); + ScrDyn *handled; + if (bc_stream_event(unwrap ? SCR_EE_M_LISTENERS : SCR_EE_M_RAW_LISTENERS, self, args, argc, &handled)) { + scr_dyn_release(self); + return handled; + } if (!bc_require_object(self, "_events")) { scr_dyn_release(self); return NULL; @@ -937,7 +983,12 @@ static ScrDyn *bc_count_listeners(ScrDyn *self, ScrDyn *type, ScrDyn *only) { static ScrDyn *bc_listener_count(ScrClosure *closure, ScrDyn *const *args, size_t argc) { (void)closure; ScrDyn *self = bc_this(); - ScrDyn *result = bc_require_object(self, "_events") + ScrDyn *result; + if (bc_stream_event(SCR_EE_M_LISTENER_COUNT, self, args, argc, &result)) { + scr_dyn_release(self); + return result; + } + result = bc_require_object(self, "_events") ? bc_count_listeners(self, bc_arg(args, argc, 0), argc > 1 && !bc_undefined(args[1]) && args[1]->kind != SCR_DYN_NULL ? args[1] : NULL) : NULL; @@ -964,6 +1015,11 @@ static ScrDyn *bc_event_names(ScrClosure *closure, ScrDyn *const *args, size_t a (void)args; (void)argc; ScrDyn *self = bc_this(); + ScrDyn *handled; + if (bc_stream_event(SCR_EE_M_EVENT_NAMES, self, args, argc, &handled)) { + scr_dyn_release(self); + return handled; + } if (!bc_require_object(self, "_eventsCount")) { scr_dyn_release(self); return NULL; @@ -1008,6 +1064,11 @@ static ScrDyn *bc_set_max(ScrClosure *closure, ScrDyn *const *args, size_t argc) scr_dyn_release(self); return NULL; } + ScrDyn *handled; + if (bc_stream_event(SCR_EE_M_SET_MAX, self, args, argc, &handled)) { + scr_dyn_release(self); + return handled; + } if (!bc_require_object(self, "_maxListeners") || !bc_set(self, "_maxListeners", scr_dyn_retain(n))) { scr_dyn_release(self); return NULL; @@ -1056,6 +1117,11 @@ static ScrDyn *bc_get_max(ScrClosure *closure, ScrDyn *const *args, size_t argc) (void)args; (void)argc; ScrDyn *self = bc_this(); + ScrDyn *handled; + if (bc_stream_event(SCR_EE_M_GET_MAX, self, args, argc, &handled)) { + scr_dyn_release(self); + return handled; + } double max = bc_require_object(self, "_maxListeners") ? bc_max_listeners(self) : -1; scr_dyn_release(self); if (scr_exc_pending()) return NULL; @@ -1321,6 +1387,8 @@ static ScrDyn *bc_construct(ScrClosure *closure, ScrDyn *const *args, size_t arg ScrDyn *result = NULL; if (kind == BC_EMITTER || kind == BC_STREAM) { if (bc_emitter_init(self, bc_arg(args, argc, 0))) result = scr_dyn_retain(scr_dyn_undefined()); + } else if (bc_stream_ops) { + result = bc_stream_ops->construct(kind, self, args, argc); } else { char message[160]; snprintf(message, sizeof message, @@ -1414,6 +1482,8 @@ static ScrDyn *bc_new_class(int kind, ScrDyn *parent) { } else if (kind == BC_STREAM) { ScrDyn *pipe = scr_dyn_new_func(scr_closure_new(NULL, 0), bc_pipe, 2, "native:stream", "pipe"); bc_set(prototype, "pipe", pipe); + } else if (bc_stream_ops) { + bc_stream_ops->prototype(kind, prototype); } scr_dyn_release(prototype); if (scr_exc_pending()) scr_exc_clear(); @@ -1449,6 +1519,22 @@ static ScrDyn *bc_class(int kind) { return bc_ctors[kind]; } +/* Registers the stream half; constructors that already exist get their + * stream prototypes' methods now. */ +void scr_dyn_builtin_class_streams(const ScrDynStreamOps *ops) { + if (bc_stream_ops) return; + bc_stream_ops = ops; + for (int kind = BC_READABLE; kind < BC_COUNT; kind++) { + if (!bc_ctors[kind]) continue; + ScrDyn *prototype = bc_get(bc_ctors[kind], "prototype"); + if (prototype) { + ops->prototype(kind, prototype); + scr_dyn_release(prototype); + } + if (scr_exc_pending()) scr_exc_clear(); + } +} + /* The constructor value for kind (the BC_* order), +1. */ ScrDyn *scr_dyn_builtin_class(double kind) { int index = (int)kind; diff --git a/packages/runtime/src/scr_dyn_builtin_stream.c b/packages/runtime/src/scr_dyn_builtin_stream.c new file mode 100644 index 000000000..209c8c39a --- /dev/null +++ b/packages/runtime/src/scr_dyn_builtin_stream.c @@ -0,0 +1,1719 @@ +/* Native stream state for JavaScript constructor functions that inherit + * from the node:stream classes the pre-class way: + * + * function Source(opts) { Readable.call(this, opts); } + * util.inherits(Source, Readable); + * Source.prototype._read = function (n) { this.push(...); }; + * + * scr_dyn_builtin_class.c provides Readable, Writable, Duplex, Transform + * and PassThrough as untyped constructors with Node's prototype chain. + * This unit (the dynBuiltinStream feature) gives them their stream half: + * + * - `Readable.call(this, opts)` (and the other four) creates a runtime + * stream (scr_stream.c) over the receiver. The receiver keeps it in a + * hidden slot (an entry of its symbol table that no symbol can name), + * so JavaScript sees only the properties Node's constructor adds: + * `_readableState` / `_writableState` (live read-only views of the + * runtime state), `allowHalfOpen` on the duplex kinds, and the option + * callbacks copied to `_read`, `_write`, `_final`, `_destroy`, + * `_transform` and `_flush`. + * - The runtime stream calls the receiver's `_read`, `_write`, + * `_transform`, `_flush`, `_final` and `_destroy` through its property + * lookup, with the receiver as `this`, as Node's machinery does. + * - The stream prototypes carry Node's methods and accessors (push, read, + * write, end, pipe, destroy, readable, destroyed, ...) over the hidden + * stream, and EventEmitter's methods register listeners with the runtime + * stream's own emitter (scr_dyn_builtin_class.c forwards to them), so the + * runtime's events and pipes reach JavaScript listeners with the + * receiver as `this`. + * + * Lifetime: the receiver holds its stream through a traced capsule, and + * the stream's hook closures hold the receiver through traced boxes, so + * the pair lives while either is reachable and the cycle collector + * reclaims it afterwards. The stream's native record points back at the + * receiver without a reference; the hook closures outlive the record. + * + * Not supported here (each throws the "not supported yet" Error where it + * runs): objectMode and the other options scr_stream.c's dyn option walk + * refuses, `_writev` and `_construct`, symbol event names, listeners() / + * rawListeners(), `wrap()`, and pipes into values that are neither runtime + * streams nor constructor-function streams. */ +#include "scr_runtime.h" + +#include +#include +#include + +enum { BCS_READABLE = 2, BCS_WRITABLE, BCS_DUPLEX, BCS_TRANSFORM, BCS_PASSTHROUGH }; + +static const char bcs_hidden_key[] = "scriptc:stream"; +#define BCS_HIDDEN_LEN (sizeof bcs_hidden_key - 1) + +/* The runtime stream's native record. */ +struct BcsRec; +typedef struct BcsRec { + ScrDyn *object; /* borrowed: the hook closures keep it alive */ + ScrDyn **destroy_cbs; /* destroy(err, cb) callbacks waiting for _destroy */ + size_t destroy_n, destroy_cap; + char cls[64]; /* the constructor's name, for the leak warning */ +} BcsRec; + +static void bcs_rec_release(void *p) { + BcsRec *rec = p; + for (size_t i = 0; i < rec->destroy_n; i++) scr_dyn_release(rec->destroy_cbs[i]); + free(rec->destroy_cbs); + free(rec); +} + +static BcsRec *bcs_rec(ScrStream *s) { + return scr_stream_native(s, bcs_rec_release); +} + +/* ── small helpers ──────────────────────────────────────────────────── */ + +static ScrDyn *bcs_key(const char *name) { + ScrStr *text = scr_str_new(name, strlen(name)); + ScrDyn *key = scr_dyn_new_str(text); + scr_str_release(text); + return key; +} + +/* object[name] with object as the receiver: +1, or NULL with an exception + * pending. */ +static ScrDyn *bcs_get(ScrDyn *object, const char *name) { + ScrDyn *key = bcs_key(name); + ScrDyn *value = scr_dyn_reflect_get(object, key, object); + scr_dyn_release(key); + if (!value && !scr_exc_pending()) value = scr_dyn_retain(scr_dyn_undefined()); + return value; +} + +/* object[name] = value; the value moves in. */ +static bool bcs_set(ScrDyn *object, const char *name, ScrDyn *value) { + ScrDyn *key = bcs_key(name); + scr_dyn_key_set_computed(object, key, value); + scr_dyn_release(key); + scr_dyn_release(value); + return !scr_exc_pending(); +} + +static ScrDyn *bcs_arg(ScrDyn *const *args, size_t argc, size_t index) { + return index < argc ? args[index] : scr_dyn_undefined(); +} + +static bool bcs_nullish(const ScrDyn *value) { + return value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL; +} + +static ScrDyn *bcs_undefined(void) { + return scr_dyn_retain(scr_dyn_undefined()); +} + +static ScrDyn *bcs_str(const char *text) { + return bcs_key(text); +} + +static void bcs_unsupported(const char *message) { + scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020"); +} + +/* fn.call(self, ...args): +1 or NULL. */ +static ScrDyn *bcs_apply(ScrDyn *fn, ScrDyn *self, ScrDyn *const *args, size_t argc, const char *what) { + scr_dyn_this_push_dyn(self); + ScrDyn *result = scr_dyn_call(fn, args, argc, what); + scr_dyn_this_pop(); + return result; +} + +/* An error argument of a completion callback or destroy(): NULL for a + * falsy value (Node tests `if (err)`), else the runtime error that boxes + * back to the same JavaScript value (+1). */ +static ScrError *bcs_error_of(const ScrDyn *value) { + if (!scr_dyn_truthy(value)) return NULL; + if (value->kind == SCR_DYN_STR) return scr_error_new(SCR_ERR_ERROR, value->v.str); + return scr_error_from_dyn(value); +} + +/* ── the hidden slot ────────────────────────────────────────────────── */ + +static void *bcs_capsule_retain(void *p) { return scr_stream_retain(p); } +/* Also the capsule's tag. */ +static void bcs_capsule_release(void *p) { scr_stream_release(p); } + +/* Whether `stream`, boxed as an "object:" typed ref, is a runtime stream. */ +static bool bcs_stream_vt(const ScrStream *stream) { + const ScrVt *vt = scr_stream_vt_of(stream); + return vt != NULL && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || + (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post)); +} + +static void bcs_rec_release(void *p); + +/* The stream of a constructor-function stream or one of its state views, + * or of a typed capsule of such a stream (a typed stream value that + * crossed back into JavaScript), borrowed; NULL for other values. */ +static ScrStream *bcs_of(const ScrDyn *value) { + if (value != NULL && value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && + memcmp(value->v.typed_ref.type_key, "object:", 7) == 0) { + ScrStream *stream = value->v.typed_ref.ptr; + return bcs_stream_vt(stream) && scr_stream_native(stream, bcs_rec_release) ? stream : NULL; + } + if (value == NULL || value->kind != SCR_DYN_OBJ || value->symbol_properties == NULL) return NULL; + ScrDyn *capsule = scr_dyn_obj_get(value->symbol_properties, bcs_hidden_key, BCS_HIDDEN_LEN); + if (capsule == NULL || capsule->kind != SCR_DYN_TYPED_REF || + capsule->v.typed_ref.release != bcs_capsule_release) + return NULL; + return capsule->v.typed_ref.ptr; +} + +/* Stores the stream in the object's symbol table under a storage name no + * symbol produces (symbols store under their address), outside the key + * list, so no reflection reaches it. */ +static void bcs_hide(ScrDyn *object, ScrStream *s) { + scr_dyn_region_store(object, NULL); + if (!object->symbol_properties) object->symbol_properties = scr_dyn_new_obj_null_proto(); + if (!object->symbol_keys) object->symbol_keys = scr_dyn_new_arr(); + ScrDyn *capsule = scr_dyn_new_typed_ref_traced(s, bcs_capsule_retain, bcs_capsule_release, + bcs_hidden_key, BCS_HIDDEN_LEN, + scr_dyn_class_view_unavailable, NULL); + scr_dyn_obj_set(object->symbol_properties, bcs_hidden_key, BCS_HIDDEN_LEN, capsule); +} + +/* A runtime stream boxed as a typed capsule (a typed stream value that + * crossed into JavaScript): +1, or NULL. */ +static ScrStream *bcs_native_of(const ScrDyn *value) { + if (value->kind != SCR_DYN_TYPED_REF || value->v.typed_ref.type_key_len <= 7 || + memcmp(value->v.typed_ref.type_key, "object:", 7) != 0) + return NULL; + ScrStream *stream = scr_dyn_typed_ref_unbox(value); + if (bcs_stream_vt(stream)) return stream; + value->v.typed_ref.release(stream); + return NULL; +} + +/* Any stream a JavaScript value stands for (+1), or NULL. */ +ScrStream *scr_dyn_builtin_stream_of(const ScrDyn *value) { + ScrStream *own = bcs_of(value); + if (own) return scr_stream_retain(own); + return bcs_native_of(value); +} + +/* The JavaScript value of a runtime stream: the constructor-function + * object it belongs to (+1), or NULL for other streams. */ +ScrDyn *scr_dyn_builtin_stream_object(ScrStream *s) { + BcsRec *rec = bcs_rec(s); + return rec ? scr_dyn_retain(rec->object) : NULL; +} + +/* A stream as a listener argument: its constructor-function object, or a + * live view of another runtime stream whose pipe and unpipe take those + * objects (scr_dyn_builtin_stream_view). +1. */ +static ScrDyn *bcs_stream_value(ScrStream *s) { + return scr_dyn_builtin_stream_view(s); +} + +/* The receiver's stream for a prototype method, borrowed; throws Node's + * TypeError (reading the state of a non-stream) and answers NULL. */ +static ScrStream *bcs_need(ScrDyn *self, const char *reading) { + ScrStream *s = bcs_of(self); + if (s) return s; + char message[128]; + if (bcs_nullish(self)) { + snprintf(message, sizeof message, "Cannot read properties of %s (reading '%s')", + self->kind == SCR_DYN_NULL ? "null" : "undefined", reading); + scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message)); + } else { + snprintf(message, sizeof message, + "stream method '%s' on an object without stream state is not supported yet", reading); + bcs_unsupported(message); + } + return NULL; +} + +/* ── completion callbacks ───────────────────────────────────────────── */ + +enum { BCS_DONE_WRITE, BCS_DONE_FINAL, BCS_DONE_DESTROY, BCS_DONE_TRANSFORM, BCS_DONE_FLUSH }; + +static void bcs_run_destroy_cbs(ScrStream *s, ScrError *err) { + BcsRec *rec = bcs_rec(s); + if (!rec || rec->destroy_n == 0) return; + ScrDyn **cbs = rec->destroy_cbs; + size_t n = rec->destroy_n; + rec->destroy_cbs = NULL; + rec->destroy_n = rec->destroy_cap = 0; + ScrDyn *arg = err ? scr_dyn_from_error(err) : scr_dyn_new_null(); + ScrDyn *object = rec->object; + scr_dyn_retain(object); + for (size_t i = 0; i < n; i++) { + if (!scr_exc_pending()) { + ScrDyn *result = bcs_apply(cbs[i], object, &arg, 1, "callback"); + if (result) scr_dyn_release(result); + } + scr_dyn_release(cbs[i]); + } + free(cbs); + scr_dyn_release(object); + scr_dyn_release(arg); +} + +static ScrDyn *bcs_done_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrStream *s = scr_box_get_ref(closure->caps[0]); + int which = (int)scr_box_get_f64(closure->caps[1]); + ScrError *err = bcs_error_of(bcs_arg(args, argc, 0)); + switch (which) { + case BCS_DONE_WRITE: + scr_stream_write_done(s, err); + break; + case BCS_DONE_FINAL: + scr_stream_final_done(s, err); + break; + case BCS_DONE_DESTROY: { + ScrError *kept = err ? scr_error_retain(err) : NULL; + scr_stream_destroy_done(s, err); + if (!scr_exc_pending()) bcs_run_destroy_cbs(s, kept); + if (kept) scr_error_release(kept); + break; + } + default: { + ScrDyn *data = bcs_arg(args, argc, 1); + ScrBytes *bytes = data->kind == SCR_DYN_BYTES ? scr_bytes_raw_view(data->v.bytes) : NULL; + ScrStr *text = data->kind == SCR_DYN_STR ? scr_str_retain(data->v.str) : NULL; + if (which == BCS_DONE_TRANSFORM) scr_stream_transform_done(s, err, bytes, text); + else scr_stream_flush_done(s, err, bytes, text); + break; + } + } + scr_stream_release(s); + if (scr_exc_pending()) return NULL; + return bcs_undefined(); +} + +static ScrDyn *bcs_done(ScrStream *s, int which) { + ScrClosure *closure = scr_closure_new((void *)bcs_done_call, 2); + closure->caps[0] = scr_box_new_obj(scr_stream_retain_v, scr_stream_release_v, scr_stream_trace); + scr_box_set_ref(closure->caps[0], scr_stream_retain(s)); + closure->caps[1] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[1], which); + uint32_t arity = which == BCS_DONE_TRANSFORM || which == BCS_DONE_FLUSH ? 2 : 1; + return scr_dyn_new_func(closure, bcs_done_call, arity, "native:stream", ""); +} + +/* ── the hooks: the runtime calls the receiver's underscore methods ─── */ + +static ScrBox *bcs_box(ScrDyn *value /* moves in */) { + ScrBox *box = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(box, value); + return box; +} + +static ScrClosure *bcs_hook(ScrDyn *object, void *inv) { + ScrClosure *closure = scr_closure_new(inv, 1); + closure->caps[0] = bcs_box(scr_dyn_retain(object)); + return closure; +} + +/* receiver[method](...args) with the receiver as `this`: +1 or NULL. */ +static ScrDyn *bcs_invoke(ScrDyn *object, const char *method, ScrDyn *const *args, size_t argc) { + ScrDyn *fn = bcs_get(object, method); + if (!fn) return NULL; + ScrDyn *result = NULL; + if (scr_dyn_is_callable(fn)) { + result = bcs_apply(fn, object, args, argc, method); + } else { + char message[96]; + snprintf(message, sizeof message, "this.%s is not a function", method); + scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message)); + } + scr_dyn_release(fn); + return result; +} + +/* The thrown value of a hook Node calls inside try/catch, as the runtime + * error its catch hands on (+1); clears the exception. */ +static ScrError *bcs_take_error(void) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *value = scr_caught_to_dyn(caught); + scr_caught_release(caught); + ScrError *err = NULL; + if (value) { + err = scr_dyn_truthy(value) ? bcs_error_of(value) : NULL; + scr_dyn_release(value); + } + if (!err) { + ScrStr *empty = scr_str_new("", 0); + err = scr_error_new(SCR_ERR_ERROR, empty); + scr_str_release(empty); + } + return err; +} + +/* Readable.read: `try { this._read(hwm) } catch (err) { errorOrDestroy }` */ +static void bcs_read_inv(ScrClosure *cb, ScrStream *s, double size) { + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + ScrDyn *n = scr_dyn_new_num(size); + ScrDyn *result = bcs_invoke(object, "_read", &n, 1); + scr_dyn_release(n); + if (result) scr_dyn_release(result); + else if (scr_exc_pending()) { + ScrError *err = bcs_take_error(); + scr_stream_fail(s, err); + scr_error_release(err); + } + scr_dyn_release(object); +} + +/* _write(chunk, 'buffer', cb) and _transform(chunk, 'buffer', cb): a + * throw propagates to the write() caller, as in Node. */ +static void bcs_chunk_call(ScrClosure *cb, ScrStream *s, ScrBytes *chunk, const char *method, int done) { + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + ScrDyn *args[3]; + args[0] = scr_dyn_new_buffer(chunk); + args[1] = bcs_str("buffer"); + args[2] = bcs_done(s, done); + ScrDyn *result = bcs_invoke(object, method, args, 3); + for (int i = 0; i < 3; i++) scr_dyn_release(args[i]); + if (result) scr_dyn_release(result); + scr_dyn_release(object); +} + +static void bcs_write_inv(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { + bcs_chunk_call(cb, s, chunk, "_write", BCS_DONE_WRITE); +} + +static void bcs_transform_inv(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { + bcs_chunk_call(cb, s, chunk, "_transform", BCS_DONE_TRANSFORM); +} + +/* callFinal: `try { stream._final(onFinish) } catch (err) { onFinish(err) }`; + * a Transform's _flush runs inside the same try. */ +static void bcs_plain_call(ScrClosure *cb, ScrStream *s, const char *method, int done) { + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + ScrDyn *callback = bcs_done(s, done); + ScrDyn *result = bcs_invoke(object, method, &callback, 1); + scr_dyn_release(callback); + if (result) scr_dyn_release(result); + else if (scr_exc_pending()) { + ScrError *err = bcs_take_error(); + if (done == BCS_DONE_FINAL) scr_stream_final_done(s, err); + else scr_stream_flush_done(s, err, NULL, NULL); + } + scr_dyn_release(object); +} + +static bool bcs_is_function(ScrDyn *object, const char *name, bool *failed); + +/* prefinish reads `stream._final` when the writable side finishes, so a + * method assigned after the constructor runs; without one the finish goes + * on as Node's does when `typeof stream._final !== 'function'`. */ +static void bcs_final_inv(ScrClosure *cb, ScrStream *s) { + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + bool failed = false; + bool present = bcs_is_function(object, "_final", &failed); + scr_dyn_release(object); + if (failed) return; + if (present) bcs_plain_call(cb, s, "_final", BCS_DONE_FINAL); + else scr_stream_final_done(s, NULL); +} + +/* Transform's final reads `this._flush` the same way: without one it ends + * the readable side. */ +static void bcs_flush_inv(ScrClosure *cb, ScrStream *s) { + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + bool failed = false; + bool present = bcs_is_function(object, "_flush", &failed); + if (!failed && bcs_is_function(object, "_final", &failed)) { + bcs_unsupported("_final on a constructor-function Transform is not supported yet"); + failed = true; + } + scr_dyn_release(object); + if (failed) return; + if (present) bcs_plain_call(cb, s, "_flush", BCS_DONE_FLUSH); + else scr_stream_flush_done(s, NULL, NULL, NULL); +} + +static ScrDyn *bcs_default_method(int kind, const char *name); + +/* _destroy(err || null, onDestroy) inside try/catch. The prototype's + * default (cb(err)) completes without a call. */ +static void bcs_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { + ScrDyn *object = scr_box_get_ref(cb->caps[0]); + ScrDyn *fn = bcs_get(object, "_destroy"); + ScrDyn *fallback = bcs_default_method(BCS_READABLE, "_destroy"); + if (fn && fallback && scr_dyn_strict_eq(fn, fallback)) { + ScrError *kept = err ? scr_error_retain(err) : NULL; + scr_stream_destroy_done(s, err ? scr_error_retain(err) : NULL); + if (!scr_exc_pending()) bcs_run_destroy_cbs(s, kept); + if (kept) scr_error_release(kept); + } else if (fn) { + ScrDyn *args[2]; + args[0] = err ? scr_dyn_from_error(err) : scr_dyn_new_null(); + args[1] = bcs_done(s, BCS_DONE_DESTROY); + ScrDyn *result = NULL; + if (scr_dyn_is_callable(fn)) { + result = bcs_apply(fn, object, args, 2, "_destroy"); + } else { + static const char message[] = "this._destroy is not a function"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } + scr_dyn_release(args[0]); + scr_dyn_release(args[1]); + if (result) scr_dyn_release(result); + else if (scr_exc_pending()) { + ScrError *thrown = bcs_take_error(); + ScrError *kept = scr_error_retain(thrown); + scr_stream_destroy_done(s, thrown); + if (!scr_exc_pending()) bcs_run_destroy_cbs(s, kept); + scr_error_release(kept); + } + } + if (fallback) scr_dyn_release(fallback); + if (fn) scr_dyn_release(fn); + scr_dyn_release(object); +} + +/* ── listeners: the runtime emitter, the receiver as `this` ─────────── */ + +enum { BCS_L_FN, BCS_L_SELF, BCS_L_CAPS }; + +static void bcs_listener_call(ScrClosure *adapter, ScrDyn *const *args, size_t argc) { + ScrDyn *fn = scr_box_get_ref(adapter->caps[BCS_L_FN]); + ScrDyn *self = scr_box_get_ref(adapter->caps[BCS_L_SELF]); + ScrDyn *result = bcs_apply(fn, self, args, argc, "listener"); + if (result) scr_dyn_release(result); + scr_dyn_release(self); + scr_dyn_release(fn); +} + +static void bcs_listener0(ScrClosure *adapter) { + bcs_listener_call(adapter, NULL, 0); +} + +static void bcs_listener_data(ScrClosure *adapter, ScrBytes *bytes, ScrStr *text) { + ScrDyn *arg = text ? scr_dyn_new_str(text) : scr_dyn_new_buffer(bytes); + bcs_listener_call(adapter, &arg, 1); + scr_dyn_release(arg); +} + +static void bcs_listener_error(ScrClosure *adapter, ScrError *error) { + ScrDyn *arg = scr_dyn_from_error(error); + bcs_listener_call(adapter, &arg, 1); + scr_dyn_release(arg); +} + +static void bcs_listener_stream(ScrClosure *adapter, ScrStream *stream) { + ScrDyn *arg = bcs_stream_value(stream); + bcs_listener_call(adapter, &arg, 1); + scr_dyn_release(arg); +} + +static void bcs_listener_name(ScrClosure *adapter, ScrStr *name) { + ScrDyn *arg = scr_dyn_new_str(name); + bcs_listener_call(adapter, &arg, 1); + scr_dyn_release(arg); +} + +/* Computed events: the listener's checked-dynamic call, with every + * argument emit passed. */ +static ScrDyn *bcs_listener_flex(ScrClosure *adapter, ScrDyn *const *args, size_t argc) { + bcs_listener_call(adapter, args, argc); + if (scr_exc_pending()) return NULL; + return bcs_undefined(); +} + +static ScrClosure *bcs_adapter(ScrDyn *fn, ScrDyn *self, void *call) { + ScrClosure *adapter = scr_closure_new(call, BCS_L_CAPS); + adapter->caps[BCS_L_FN] = bcs_box(scr_dyn_retain(fn)); + adapter->caps[BCS_L_SELF] = bcs_box(scr_dyn_retain(self)); + scr_dyn_adopt_identity(adapter, fn); + return adapter; +} + +static bool bcs_is(const ScrStr *name, const char *text) { + size_t length = strlen(text); + return name->len == length && memcmp(name->data, text, length) == 0; +} + +/* The runtime's fixed event tuples: 0 none, 1 data, 2 error, 3 a stream + * (pipe/unpipe), 4 an event name (the meta events); -1 computed. */ +static int bcs_event_shape(const ScrStr *name) { + if (bcs_is(name, "data")) return 1; + if (bcs_is(name, "error")) return 2; + if (bcs_is(name, "pipe") || bcs_is(name, "unpipe")) return 3; + if (bcs_is(name, "newListener") || bcs_is(name, "removeListener")) return 4; + static const char *const empty[] = { + "end", "close", "finish", "drain", "readable", "resume", "pause", "prefinish", + }; + for (size_t i = 0; i < sizeof empty / sizeof *empty; i++) + if (bcs_is(name, empty[i])) return 0; + return -1; +} + +/* The event name argument as a string, or NULL with an exception + * pending. */ +static ScrStr *bcs_event_name(const ScrDyn *type) { + if (type->kind == SCR_DYN_STR) return scr_str_retain(type->v.str); + if (type->kind == SCR_DYN_SYMBOL) { + bcs_unsupported("symbol event names on a constructor-function stream are not supported yet"); + return NULL; + } + return scr_dyn_string_coerce_js(type); +} + +static bool bcs_check_listener(const ScrDyn *listener) { + if (listener->kind == SCR_DYN_FUNC) return true; + if (scr_dyn_is_callable(listener)) { + bcs_unsupported("listeners other than functions on a constructor-function stream are not supported yet"); + return false; + } + scr_dyn_arg_type_fail("listener", "of type function", listener); + return false; +} + +static bool bcs_add_listener(ScrStream *s, ScrDyn *self, ScrDyn *type, ScrDyn *listener, bool once, + bool prepend) { + if (!bcs_check_listener(listener)) return false; + ScrStr *name = bcs_event_name(type); + if (!name) return false; + ScrEmitter *em = (ScrEmitter *)s; + ScrEmitter *chained = NULL; + switch (bcs_event_shape(name)) { + case 0: + chained = scr_emitter_on_dyn(em, name, listener, bcs_adapter(listener, self, (void *)bcs_listener0), + scr_ee_inv_fixed0, once, prepend); + break; + case 1: + chained = scr_emitter_on_dyn(em, name, listener, bcs_adapter(listener, self, (void *)bcs_listener_data), + scr_ee_inv_fixed2, once, prepend); + break; + case 2: + chained = scr_emitter_on_dyn(em, name, listener, bcs_adapter(listener, self, (void *)bcs_listener_error), + scr_ee_inv_fixed1, once, prepend); + break; + case 3: + chained = scr_emitter_on_dyn(em, name, listener, bcs_adapter(listener, self, (void *)bcs_listener_stream), + scr_ee_inv_fixed1, once, prepend); + break; + case 4: + chained = scr_emitter_on_dyn(em, name, listener, bcs_adapter(listener, self, (void *)bcs_listener_name), + scr_ee_inv_fixed1, once, prepend); + break; + default: { + ScrClosure *adapter = bcs_adapter(listener, self, (void *)bcs_listener_flex); + ScrDyn *wrapper = scr_dyn_new_func(adapter, bcs_listener_flex, 0, "native:events", "listener"); + chained = scr_emitter_on_flex(em, name, wrapper, once, prepend); + scr_dyn_release(wrapper); + break; + } + } + if (chained) scr_emitter_release(chained); + scr_str_release(name); + return !scr_exc_pending(); +} + +/* this.emit(type, ...args) on the runtime emitter: the fixed tuples the + * runtime's own listeners read, computed events with every argument. */ +static ScrDyn *bcs_emit(ScrStream *s, ScrDyn *const *args, size_t argc) { + ScrStr *name = bcs_event_name(bcs_arg(args, argc, 0)); + if (!name) return NULL; + ScrEmitter *em = (ScrEmitter *)s; + ScrDyn *const *rest = argc > 1 ? args + 1 : NULL; + size_t restc = argc > 1 ? argc - 1 : 0; + ScrDyn *first = restc ? rest[0] : scr_dyn_undefined(); + bool had = false; + switch (bcs_event_shape(name)) { + case 0: + had = scr_emitter_emit(em, name); + break; + case 1: + if (first->kind == SCR_DYN_BYTES) { + ScrBytes *bytes = scr_bytes_raw_view(first->v.bytes); + had = scr_emitter_emit(em, name, bytes, (ScrStr *)NULL); + scr_bytes_release(bytes); + } else if (first->kind == SCR_DYN_STR) { + had = scr_emitter_emit(em, name, (ScrBytes *)NULL, first->v.str); + } else { + bcs_unsupported("emit('data') with a chunk other than a Buffer or string on a constructor-function stream is not supported yet"); + } + break; + case 2: + if (!scr_emitter_has(em, "error")) { + /* Node's unhandled 'error': an Error throws itself, anything else + * ERR_UNHANDLED_ERROR. */ + if (scr_dyn_err_instanceof(first, SCR_ERR_ERROR)) { + scr_dyn_throw(scr_dyn_retain(first)); + } else { + ScrStr *shown = scr_insp_dyn(first, 0, 2); + ScrJsonBuf text; + scr_jb_init(&text); + scr_jb_puts(&text, "Unhandled error. ("); + scr_jb_put_str(&text, shown); + scr_jb_putc(&text, ')'); + scr_str_release(shown); + ScrStr *message = scr_jb_finish(&text); + scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "ERR_UNHANDLED_ERROR"); + scr_str_release(message); + } + } else { + ScrError *err = bcs_error_of(first); + if (!err) { + ScrStr *empty = scr_str_new("", 0); + err = scr_error_new(SCR_ERR_ERROR, empty); + scr_str_release(empty); + } + had = scr_emitter_emit_error(em, name, err); + scr_error_release(err); + } + break; + case 3: { + ScrStream *stream = scr_dyn_builtin_stream_of(first); + if (stream) { + had = scr_emitter_emit(em, name, stream); + scr_stream_release(stream); + } else { + bcs_unsupported("emit('pipe') or emit('unpipe') with a value other than a stream on a constructor-function stream is not supported yet"); + } + break; + } + case 4: + if (first->kind == SCR_DYN_STR) had = scr_emitter_emit(em, name, first->v.str); + else bcs_unsupported("emit('newListener') or emit('removeListener') with a name other than a string on a constructor-function stream is not supported yet"); + break; + default: + had = scr_emitter_emit_flex(em, name, rest, restc); + break; + } + scr_str_release(name); + if (scr_exc_pending()) return NULL; + return scr_dyn_new_bool(had); +} + +static ScrDyn *bcs_event_names(ScrStream *s) { + ScrArr *names = scr_emitter_event_names((ScrEmitter *)s); + ScrDyn *result = scr_dyn_new_arr(); + double length = scr_arr_len(names); + for (double i = 0; i < length; i++) { + ScrStr *name = scr_arr_get_ref(names, i); + scr_dyn_arr_push(result, scr_dyn_new_str(name)); + scr_str_release(name); + } + scr_arr_release(names); + return result; +} + +/* EventEmitter.prototype's methods on a constructor-function stream + * (called by scr_dyn_builtin_class.c): true when self has a stream. */ +static bool bcs_event(int method, ScrDyn *self, ScrDyn *const *args, size_t argc, ScrDyn **result) { + ScrStream *s = bcs_of(self); + if (!s) return false; + ScrDyn *type = bcs_arg(args, argc, 0); + ScrEmitter *em = (ScrEmitter *)s; + ScrDyn *out = NULL; + switch (method) { + case SCR_EE_M_ON: + case SCR_EE_M_PREPEND: + case SCR_EE_M_ONCE: + case SCR_EE_M_PREPEND_ONCE: + if (bcs_add_listener(s, self, type, bcs_arg(args, argc, 1), + method == SCR_EE_M_ONCE || method == SCR_EE_M_PREPEND_ONCE, + method == SCR_EE_M_PREPEND || method == SCR_EE_M_PREPEND_ONCE)) + out = scr_dyn_retain(self); + break; + case SCR_EE_M_OFF: { + ScrDyn *listener = bcs_arg(args, argc, 1); + if (!bcs_check_listener(listener)) break; + ScrStr *name = bcs_event_name(type); + if (!name) break; + scr_emitter_release(scr_emitter_off_dyn(em, name, listener)); + scr_str_release(name); + if (!scr_exc_pending()) out = scr_dyn_retain(self); + break; + } + case SCR_EE_M_REMOVE_ALL: { + ScrStr *name = argc == 0 || type->kind == SCR_DYN_UNDEF ? NULL : bcs_event_name(type); + if (argc > 0 && type->kind != SCR_DYN_UNDEF && !name) break; + if (!name) name = scr_str_new("", 0); + scr_emitter_release(scr_emitter_remove_all(em, name, argc == 0 || type->kind == SCR_DYN_UNDEF)); + scr_str_release(name); + if (!scr_exc_pending()) out = scr_dyn_retain(self); + break; + } + case SCR_EE_M_EMIT: + out = bcs_emit(s, args, argc); + break; + case SCR_EE_M_LISTENER_COUNT: { + ScrStr *name = bcs_event_name(type); + if (!name) break; + ScrDyn *only = bcs_arg(args, argc, 1); + double count = bcs_nullish(only) ? scr_emitter_listener_count(em, name) + : only->kind == SCR_DYN_FUNC ? scr_emitter_listener_count_dyn(em, name, only) : 0; + scr_str_release(name); + if (!scr_exc_pending()) out = scr_dyn_new_num(count); + break; + } + case SCR_EE_M_EVENT_NAMES: + out = bcs_event_names(s); + break; + case SCR_EE_M_SET_MAX: + scr_emitter_release(scr_emitter_set_max(em, bcs_arg(args, argc, 0)->v.num)); + out = scr_dyn_retain(self); + break; + case SCR_EE_M_GET_MAX: + out = scr_dyn_new_num(scr_emitter_get_max(em)); + break; + default: + bcs_unsupported("listeners() and rawListeners() on a constructor-function stream are not supported yet"); + break; + } + *result = scr_exc_pending() ? (out ? (scr_dyn_release(out), NULL) : NULL) : out; + return true; +} + +/* ── construction ───────────────────────────────────────────────────── */ + +static bool bcs_is_function(ScrDyn *object, const char *name, bool *failed) { + ScrDyn *value = bcs_get(object, name); + if (!value) { + *failed = true; + return false; + } + bool callable = scr_dyn_is_callable(value); + scr_dyn_release(value); + return callable; +} + +/* Node's constructors copy these options onto the instance. */ +static bool bcs_copy_option(ScrDyn *self, ScrDyn *opts, const char *option, const char *property) { + ScrDyn *value = bcs_get(opts, option); + if (!value) return false; + if (scr_dyn_is_callable(value)) return bcs_set(self, property, value); + scr_dyn_release(value); + return true; +} + +static void bcs_class_name(ScrDyn *self, char *out, size_t size) { + snprintf(out, size, "%s", "Object"); + ScrDyn *ctor = bcs_get(self, "constructor"); + if (!ctor) { + scr_exc_clear(); + return; + } + if (scr_dyn_is_callable(ctor)) { + ScrDyn *name = bcs_get(ctor, "name"); + if (name && name->kind == SCR_DYN_STR && name->v.str->len > 0) + snprintf(out, size, "%.*s", (int)name->v.str->len, name->v.str->data); + if (name) scr_dyn_release(name); + else scr_exc_clear(); + } + scr_dyn_release(ctor); +} + +static ScrDyn *bcs_state_proto(bool readable); + +/* Listeners added before the constructor ran (bl registers its 'pipe' + * listener first) sit in the instance's own _events, where Node's + * constructor keeps them: they move to the runtime emitter, in order. */ +static bool bcs_adopt_listeners(ScrStream *s, ScrDyn *self) { + ScrDyn *events = bcs_get(self, "_events"); + if (!events) return false; + if (events->kind != SCR_DYN_OBJ) { + scr_dyn_release(events); + return true; + } + ScrDyn *keys = scr_dyn_own_keys(events); + bool ok = keys != NULL; + size_t count = ok ? (size_t)scr_dyn_arr_len(keys) : 0; + for (size_t i = 0; ok && i < count; i++) { + ScrDyn *key = scr_dyn_index_get(keys, (double)i); + ScrDyn *list = key ? scr_dyn_reflect_get(events, key, events) : NULL; + if (!list) { + if (key) scr_dyn_release(key); + ok = false; + break; + } + if (list->kind == SCR_DYN_ARR) { + size_t length = (size_t)scr_dyn_arr_len(list); + for (size_t j = 0; ok && j < length; j++) { + ScrDyn *listener = scr_dyn_index_get(list, (double)j); + ok = listener && bcs_add_listener(s, self, key, listener, false, false); + if (listener) scr_dyn_release(listener); + } + } else if (scr_dyn_is_callable(list)) { + ok = bcs_add_listener(s, self, key, list, false, false); + } + scr_dyn_release(list); + scr_dyn_release(key); + } + if (keys) scr_dyn_release(keys); + scr_dyn_release(events); + if (ok && count > 0) + ok = bcs_set(self, "_events", scr_dyn_new_obj_null_proto()) && bcs_set(self, "_eventsCount", scr_dyn_new_num(0)); + return ok; +} + +static bool bcs_state_view(ScrDyn *self, ScrStream *s, bool readable) { + ScrDyn *view = scr_dyn_obj_create(bcs_state_proto(readable)); + bcs_hide(view, s); + return bcs_set(self, readable ? "_readableState" : "_writableState", view); +} + +static ScrDyn *bcs_construct(int kind, ScrDyn *self, ScrDyn *const *args, size_t argc) { + static const char *const names[] = { "", "", "Readable", "Writable", "Duplex", "Transform", "PassThrough" }; + char message[192]; + if (self->kind != SCR_DYN_OBJ || !scr_dyn_builtin_instanceof(self, kind)) { + if (scr_exc_pending()) return NULL; + snprintf(message, sizeof message, + "%s.call() on a receiver that does not inherit %s.prototype is not supported yet", + names[kind], names[kind]); + bcs_unsupported(message); + return NULL; + } + if (bcs_of(self)) { + snprintf(message, sizeof message, + "%s.call() on a receiver that already has stream state is not supported yet", names[kind]); + bcs_unsupported(message); + return NULL; + } + bool failed = false; + bool writes = kind != BCS_READABLE; + if (writes && bcs_is_function(self, "_writev", &failed)) { + bcs_unsupported("_writev on a constructor-function stream is not supported yet"); + return NULL; + } + if (!failed && bcs_is_function(self, "_construct", &failed)) { + bcs_unsupported("_construct on a constructor-function stream is not supported yet"); + return NULL; + } + if (failed) return NULL; + ScrDyn *opts = bcs_arg(args, argc, 0); + ScrStream *s = kind == BCS_READABLE ? scr_stream_new_readable_dyn(opts) + : kind == BCS_WRITABLE ? scr_stream_new_writable_dyn(opts) + : kind == BCS_DUPLEX ? scr_stream_new_duplex_dyn(opts) + : kind == BCS_TRANSFORM ? scr_stream_new_transform_dyn(opts) + : scr_stream_new_passthrough_dyn(opts); + if (!s) return NULL; + bool ok = true; + if (opts->kind == SCR_DYN_OBJ) { + bool transformish = kind == BCS_TRANSFORM || kind == BCS_PASSTHROUGH; + if (!transformish && kind != BCS_WRITABLE) ok = ok && bcs_copy_option(self, opts, "read", "_read"); + if (!transformish && kind != BCS_READABLE) { + ok = ok && bcs_copy_option(self, opts, "write", "_write"); + ok = ok && bcs_copy_option(self, opts, "final", "_final"); + } + ok = ok && bcs_copy_option(self, opts, "destroy", "_destroy"); + if (transformish) { + ok = ok && bcs_copy_option(self, opts, "transform", "_transform"); + ok = ok && bcs_copy_option(self, opts, "flush", "_flush"); + } + } + if (ok && kind >= BCS_DUPLEX) + ok = bcs_set(self, "allowHalfOpen", scr_dyn_new_bool(scr_stream_prop(s, "allowHalfOpen") != 0)); + if (!ok) { + scr_stream_release(s); + return NULL; + } + BcsRec *rec = calloc(1, sizeof *rec); + if (!rec) scr_trap("out of memory"); + rec->object = self; + bcs_class_name(self, rec->cls, sizeof rec->cls); + scr_stream_set_native(s, rec, bcs_rec_release); + s->cls = rec->cls; + /* The runtime calls the instance's underscore methods. */ + if (kind == BCS_READABLE || kind == BCS_DUPLEX) + scr_stream_set_read(s, bcs_hook(self, (void *)bcs_read_inv), bcs_read_inv); + if (kind == BCS_WRITABLE || kind == BCS_DUPLEX) { + scr_stream_set_write(s, bcs_hook(self, (void *)bcs_write_inv), bcs_write_inv); + /* _final is read when the writable side finishes (bcs_final_inv). */ + scr_stream_set_final(s, bcs_hook(self, (void *)bcs_final_inv), bcs_final_inv); + } + if (kind == BCS_TRANSFORM || kind == BCS_PASSTHROUGH) { + /* _transform is read per chunk (a PassThrough's default passes the + * chunk on) and _flush when the writable side finishes. */ + scr_stream_set_transform(s, bcs_hook(self, (void *)bcs_transform_inv), bcs_transform_inv); + scr_stream_set_flush(s, bcs_hook(self, (void *)bcs_flush_inv), bcs_flush_inv); + if (!failed && bcs_is_function(self, "_final", &failed)) { + bcs_unsupported("_final on a constructor-function Transform is not supported yet"); + failed = true; + } + } + scr_stream_set_destroy(s, bcs_hook(self, (void *)bcs_destroy_inv), bcs_destroy_inv); + bcs_hide(self, s); + if (!failed) failed = !bcs_adopt_listeners(s, self); + if (!failed && kind != BCS_WRITABLE) failed = !bcs_state_view(self, s, true); + if (!failed && kind != BCS_READABLE) failed = !bcs_state_view(self, s, false); + scr_stream_release(s); /* the capsule holds it */ + if (failed || scr_exc_pending()) return NULL; + return bcs_undefined(); +} + +/* ── prototype methods ──────────────────────────────────────────────── */ + +/* Node's encoding names, canonical; NULL with ERR_UNKNOWN_ENCODING. */ +static ScrStr *bcs_encoding(const ScrDyn *value) { + static const struct { const char *from, *to; } map[] = { + {"utf8", "utf8"}, {"utf-8", "utf8"}, {"hex", "hex"}, {"base64", "base64"}, + {"base64url", "base64url"}, {"latin1", "latin1"}, {"binary", "latin1"}, + {"ascii", "ascii"}, {"utf16le", "utf16le"}, {"utf-16le", "utf16le"}, + {"ucs2", "utf16le"}, {"ucs-2", "utf16le"}, + }; + ScrStr *raw = value->kind == SCR_DYN_STR ? scr_str_retain(value->v.str) : scr_dyn_string_coerce_js(value); + if (!raw) return NULL; + for (size_t i = 0; i < sizeof map / sizeof *map; i++) { + if (bcs_is(raw, map[i].from)) { + scr_str_release(raw); + return scr_str_new(map[i].to, strlen(map[i].to)); + } + } + char message[128]; + snprintf(message, sizeof message, "Unknown encoding: %.*s", (int)(raw->len < 64 ? raw->len : 64), raw->data); + scr_str_release(raw); + scr_throw_error_msg_code(SCR_ERR_TYPE, message, strlen(message), "ERR_UNKNOWN_ENCODING"); + return NULL; +} + +enum { + BCS_M_PUSH, BCS_M_UNSHIFT, BCS_M_READ, BCS_M_SET_ENCODING, BCS_M_PAUSE, BCS_M_RESUME, + BCS_M_IS_PAUSED, BCS_M_PIPE, BCS_M_UNPIPE, BCS_M_DESTROY, BCS_M_WRITE, BCS_M_END, + BCS_M_CORK, BCS_M_UNCORK, BCS_M_SET_DEFAULT_ENCODING, BCS_M_WRITABLE_PIPE, BCS_M_WRAP, + BCS_M_READ_DEFAULT, BCS_M_WRITE_DEFAULT, BCS_M_TRANSFORM_DEFAULT, BCS_M_DESTROY_DEFAULT, + BCS_M_PASSTHROUGH_TRANSFORM, BCS_M_ON, BCS_M_OFF, BCS_M_REMOVE_ALL, +}; + +enum { BCS_CB_STREAM = BCS_L_CAPS, BCS_CB_CAPS }; + +/* A write or end callback: null after a successful write and at 'finish'; + * once the stream failed, its error, or Node's ERR_STREAM_DESTROYED for a + * write a destroy cut off. */ +static void bcs_callback_fire(ScrClosure *closure) { + ScrDyn *fn = scr_box_get_ref(closure->caps[BCS_L_FN]); + ScrDyn *self = scr_box_get_ref(closure->caps[BCS_L_SELF]); + ScrStream *s = scr_box_get_ref(closure->caps[BCS_CB_STREAM]); + ScrError *err = scr_stream_errored(s); + ScrDyn *arg; + if (err) { + arg = scr_dyn_from_error(err); + scr_error_release(err); + } else if (scr_stream_prop(s, "destroyed") != 0) { + ScrStr *message = scr_str_new("Cannot call write after a stream was destroyed", 46); + ScrError *destroyed = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_error_set_code(destroyed, "ERR_STREAM_DESTROYED"); + arg = scr_dyn_from_error(destroyed); + scr_error_release(destroyed); + } else { + arg = scr_dyn_new_null(); + } + scr_stream_release(s); + ScrDyn *result = bcs_apply(fn, self, &arg, 1, "callback"); + if (result) scr_dyn_release(result); + scr_dyn_release(arg); + scr_dyn_release(self); + scr_dyn_release(fn); +} + +static ScrClosure *bcs_callback(ScrDyn *fn, ScrDyn *self, ScrStream *s) { + if (!scr_dyn_is_callable(fn)) return NULL; + ScrClosure *closure = scr_closure_new((void *)bcs_callback_fire, BCS_CB_CAPS); + closure->caps[BCS_L_FN] = bcs_box(scr_dyn_retain(fn)); + closure->caps[BCS_L_SELF] = bcs_box(scr_dyn_retain(self)); + closure->caps[BCS_CB_STREAM] = scr_box_new_obj(scr_stream_retain_v, scr_stream_release_v, scr_stream_trace); + scr_box_set_ref(closure->caps[BCS_CB_STREAM], scr_stream_retain(s)); + return closure; +} + +/* Strings in another encoding than utf8 have no write path here. */ +static bool bcs_utf8_only(const ScrDyn *encoding) { + if (encoding->kind != SCR_DYN_STR) return true; + ScrStr *canonical = bcs_encoding(encoding); + if (!canonical) return false; + bool utf8 = bcs_is(canonical, "utf8"); + scr_str_release(canonical); + if (!utf8) bcs_unsupported("writing strings in an encoding other than utf8 to a constructor-function stream is not supported yet"); + return utf8; +} + +static ScrDyn *bcs_write(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t argc) { + ScrDyn *chunk = bcs_arg(args, argc, 0); + ScrDyn *encoding = bcs_arg(args, argc, 1); + ScrDyn *cb = bcs_arg(args, argc, 2); + if (scr_dyn_is_callable(encoding)) { + cb = encoding; + encoding = scr_dyn_undefined(); + } + if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) return NULL; + bool below = scr_stream_write_dyn(s, chunk, bcs_callback(cb, self, s)); + if (scr_exc_pending()) return NULL; + return scr_dyn_new_bool(below); +} + +static ScrDyn *bcs_end(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t argc) { + ScrDyn *chunk = bcs_arg(args, argc, 0); + ScrDyn *encoding = bcs_arg(args, argc, 1); + ScrDyn *cb = bcs_arg(args, argc, 2); + if (scr_dyn_is_callable(chunk)) { + cb = chunk; + chunk = scr_dyn_undefined(); + encoding = scr_dyn_undefined(); + } else if (scr_dyn_is_callable(encoding)) { + cb = encoding; + encoding = scr_dyn_undefined(); + } + if (chunk->kind == SCR_DYN_STR && !bcs_utf8_only(encoding)) return NULL; + ScrStream *chained = NULL; + if (bcs_nullish(chunk)) { + chained = scr_stream_end(s, NULL, NULL, bcs_callback(cb, self, s)); + } else if (chunk->kind == SCR_DYN_STR) { + chained = scr_stream_end(s, NULL, chunk->v.str, bcs_callback(cb, self, s)); + } else if (chunk->kind == SCR_DYN_BYTES) { + ScrBytes *bytes = scr_bytes_raw_view(chunk->v.bytes); + chained = scr_stream_end(s, bytes, NULL, bcs_callback(cb, self, s)); + scr_bytes_release(bytes); + } else { + scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer, TypedArray, or DataView", chunk); + } + if (chained) scr_stream_release(chained); + if (scr_exc_pending()) return NULL; + return scr_dyn_retain(self); +} + +static ScrDyn *bcs_destroy(ScrStream *s, ScrDyn *self, ScrDyn *const *args, size_t argc) { + ScrDyn *errv = bcs_arg(args, argc, 0); + ScrDyn *cb = bcs_arg(args, argc, 1); + if (scr_stream_prop(s, "destroyed") != 0) { + if (scr_dyn_is_callable(cb)) { + ScrDyn *result = bcs_apply(cb, self, NULL, 0, "callback"); + if (!result) return NULL; + scr_dyn_release(result); + } + return scr_dyn_retain(self); + } + if (scr_dyn_is_callable(cb)) { + BcsRec *rec = bcs_rec(s); + if (rec->destroy_n == rec->destroy_cap) { + rec->destroy_cap = rec->destroy_cap ? rec->destroy_cap * 2 : 2; + rec->destroy_cbs = realloc(rec->destroy_cbs, rec->destroy_cap * sizeof *rec->destroy_cbs); + if (!rec->destroy_cbs) scr_trap("out of memory"); + } + rec->destroy_cbs[rec->destroy_n++] = scr_dyn_retain(cb); + } + ScrError *err = bcs_error_of(errv); + ScrStream *chained = scr_stream_destroy(s, err); + if (err) scr_error_release(err); + scr_stream_release(chained); + if (scr_exc_pending()) return NULL; + return scr_dyn_retain(self); +} + +/* pipe(dest, { end }): runtime streams and constructor-function streams + * pipe through the runtime's pipe. */ +static bool bcs_overrides(ScrDyn *dest, const char *name); + +static ScrDyn *bcs_pipe(ScrStream *s, ScrDyn *const *args, size_t argc) { + ScrDyn *dest = bcs_arg(args, argc, 0); + ScrDyn *options = bcs_arg(args, argc, 1); + ScrStream *d = scr_dyn_builtin_stream_of(dest); + if (!d) { + if (!scr_exc_pending()) bcs_unsupported("pipe destinations other than streams are not supported yet"); + return NULL; + } + /* The runtime's pipe writes and ends the destination's stream itself; + * Node calls the destination's write() and end(). */ + if (bcs_of(dest) && (bcs_overrides(dest, "write") || bcs_overrides(dest, "end"))) { + scr_stream_release(d); + if (!scr_exc_pending()) + bcs_unsupported("pipe into a constructor-function stream with its own write() or end() is not supported yet"); + return NULL; + } + bool end = true; + if (options->kind == SCR_DYN_OBJ) { + ScrDyn *option = bcs_get(options, "end"); + if (!option) { + scr_stream_release(d); + return NULL; + } + end = !(option->kind == SCR_DYN_BOOL && !option->v.b); + scr_dyn_release(option); + } + ScrStream *chained = scr_stream_pipe(s, d, end); + if (chained) scr_stream_release(chained); + scr_stream_release(d); + if (scr_exc_pending()) return NULL; + return scr_dyn_retain(dest); +} + +static ScrDyn *bcs_not_implemented(const char *method) { + char message[64]; + snprintf(message, sizeof message, "The %s method is not implemented", method); + scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "ERR_METHOD_NOT_IMPLEMENTED"); + return NULL; +} + +static ScrDyn *bcs_method_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + int method = (int)scr_box_get_f64(closure->caps[0]); + switch (method) { + case BCS_M_READ_DEFAULT: + return bcs_not_implemented("_read()"); + case BCS_M_WRITE_DEFAULT: + return bcs_not_implemented("_write()"); + case BCS_M_TRANSFORM_DEFAULT: + return bcs_not_implemented("_transform()"); + case BCS_M_DESTROY_DEFAULT: + case BCS_M_PASSTHROUGH_TRANSFORM: { + /* _destroy(err, cb) => cb(err); _transform(chunk, enc, cb) => cb(null, chunk) */ + ScrDyn *cb = bcs_arg(args, argc, method == BCS_M_DESTROY_DEFAULT ? 1 : 2); + ScrDyn *pass[2]; + size_t passc = 1; + if (method == BCS_M_DESTROY_DEFAULT) { + pass[0] = bcs_arg(args, argc, 0); + } else { + pass[0] = scr_dyn_new_null(); + pass[1] = bcs_arg(args, argc, 0); + passc = 2; + } + ScrDyn *answer = scr_dyn_call(cb, pass, passc, "callback"); + if (method != BCS_M_DESTROY_DEFAULT) scr_dyn_release(pass[0]); + return answer; + } + default: + break; + } + ScrDyn *self = scr_dyn_this_get(); + static const char *const reading[] = { + "push", "unshift", "read", "setEncoding", "pause", "resume", "isPaused", "pipe", "unpipe", + "destroy", "write", "end", "cork", "uncork", "setDefaultEncoding", "pipe", "wrap", + "", "", "", "", "", "on", "removeListener", "removeAllListeners", + }; + ScrDyn *result = NULL; + if (method == BCS_M_WRITABLE_PIPE) { + ScrStream *s = bcs_need(self, "pipe"); + if (s) { + ScrStr *message = scr_str_new("Cannot pipe, not readable", 25); + ScrError *err = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_error_set_code(err, "ERR_STREAM_CANNOT_PIPE"); + scr_stream_fail(s, err); + scr_error_release(err); + if (!scr_exc_pending()) result = scr_dyn_retain(bcs_arg(args, argc, 0)); + } + scr_dyn_release(self); + return result; + } + if (method == BCS_M_ON || method == BCS_M_OFF || method == BCS_M_REMOVE_ALL) { + int ee = method == BCS_M_ON ? SCR_EE_M_ON : method == BCS_M_OFF ? SCR_EE_M_OFF : SCR_EE_M_REMOVE_ALL; + if (!bcs_event(ee, self, args, argc, &result)) { + /* Before the constructor ran (bl adds listeners first), Node's + * Readable.prototype.on is Stream.prototype.on over _events. */ + ScrDyn *emitter = scr_dyn_builtin_class(0); + ScrDyn *prototype = bcs_get(emitter, "prototype"); + scr_dyn_release(emitter); + ScrDyn *fn = prototype ? bcs_get(prototype, reading[method]) : NULL; + result = fn ? bcs_apply(fn, self, args, argc, reading[method]) : NULL; + if (fn) scr_dyn_release(fn); + if (prototype) scr_dyn_release(prototype); + } + scr_dyn_release(self); + return result; + } + ScrStream *s = bcs_need(self, reading[method]); + if (!s) { + scr_dyn_release(self); + return NULL; + } + ScrDyn *first = bcs_arg(args, argc, 0); + switch (method) { + case BCS_M_PUSH: + case BCS_M_UNSHIFT: { + ScrDyn *encoding = bcs_arg(args, argc, 1); + if (method == BCS_M_PUSH && first->kind == SCR_DYN_STR && encoding->kind == SCR_DYN_STR) { + ScrStr *canonical = bcs_encoding(encoding); + if (canonical) { + result = scr_dyn_new_bool(scr_stream_push_str_enc(s, first->v.str, canonical)); + scr_str_release(canonical); + } + } else if (method == BCS_M_PUSH) { + if (first->kind == SCR_DYN_UNDEF) { + ScrStr *empty = scr_str_new("", 0); + result = scr_dyn_new_bool(scr_stream_push_str(s, empty)); + scr_str_release(empty); + } else { + result = scr_dyn_new_bool(scr_stream_push_dyn(s, first)); + } + } else if (first->kind == SCR_DYN_STR) { + scr_stream_unshift_str(s, first->v.str); + result = bcs_undefined(); + } else if (first->kind == SCR_DYN_BYTES) { + ScrBytes *bytes = scr_bytes_raw_view(first->v.bytes); + scr_stream_unshift(s, bytes); + scr_bytes_release(bytes); + result = bcs_undefined(); + } else if (first->kind == SCR_DYN_NULL) { + scr_stream_push_null(s); + result = bcs_undefined(); + } else { + scr_dyn_arg_type_fail("chunk", "of type string or an instance of Buffer or Uint8Array", first); + } + break; + } + case BCS_M_READ: + result = scr_stream_read_dyn(s, first); + break; + case BCS_M_SET_ENCODING: { + ScrStr *canonical = bcs_encoding(first->kind == SCR_DYN_UNDEF ? scr_dyn_undefined() : first); + if (canonical) { + scr_stream_release(scr_stream_set_encoding(s, canonical)); + scr_str_release(canonical); + result = scr_dyn_retain(self); + } + break; + } + case BCS_M_PAUSE: + scr_stream_release(scr_stream_pause(s)); + result = scr_dyn_retain(self); + break; + case BCS_M_RESUME: + scr_stream_release(scr_stream_resume(s)); + result = scr_dyn_retain(self); + break; + case BCS_M_IS_PAUSED: + result = scr_dyn_new_bool(scr_stream_is_paused(s)); + break; + case BCS_M_PIPE: + result = bcs_pipe(s, args, argc); + break; + case BCS_M_UNPIPE: { + ScrStream *d = first->kind == SCR_DYN_UNDEF ? NULL : scr_dyn_builtin_stream_of(first); + if (first->kind == SCR_DYN_UNDEF || d) { + scr_stream_release(scr_stream_unpipe(s, d)); + if (d) scr_stream_release(d); + } + if (!scr_exc_pending()) result = scr_dyn_retain(self); + break; + } + case BCS_M_DESTROY: + result = bcs_destroy(s, self, args, argc); + break; + case BCS_M_WRITE: + result = bcs_write(s, self, args, argc); + break; + case BCS_M_END: + result = bcs_end(s, self, args, argc); + break; + case BCS_M_CORK: + scr_stream_cork(s); + result = bcs_undefined(); + break; + case BCS_M_UNCORK: + scr_stream_uncork(s); + result = bcs_undefined(); + break; + case BCS_M_SET_DEFAULT_ENCODING: + if (bcs_utf8_only(first)) result = scr_dyn_retain(self); + break; + default: + bcs_unsupported("Readable.prototype.wrap on a constructor-function stream is not supported yet"); + break; + } + scr_dyn_release(self); + if (scr_exc_pending()) { + if (result) scr_dyn_release(result); + return NULL; + } + return result; +} + +static ScrDyn *bcs_function(int method, const char *name, uint32_t arity) { + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[0], method); + return scr_dyn_new_func(closure, bcs_method_call, arity, "native:stream", name); +} + +static void bcs_method(ScrDyn *prototype, const char *name, int method, uint32_t arity) { + bcs_set(prototype, name, bcs_function(method, name, arity)); +} + +/* ── accessors ──────────────────────────────────────────────────────── */ + +enum { + BCS_P_READABLE, BCS_P_READABLE_ENDED, BCS_P_READABLE_LENGTH, BCS_P_READABLE_HWM, + BCS_P_READABLE_OBJECT_MODE, BCS_P_READABLE_FLOWING, BCS_P_WRITABLE, BCS_P_WRITABLE_ENDED, + BCS_P_WRITABLE_FINISHED, BCS_P_WRITABLE_LENGTH, BCS_P_WRITABLE_HWM, BCS_P_WRITABLE_NEED_DRAIN, + BCS_P_WRITABLE_CORKED, BCS_P_WRITABLE_OBJECT_MODE, BCS_P_DESTROYED, BCS_P_CLOSED, BCS_P_ERRORED, + BCS_P_COUNT +}; + +static const char *const bcs_property_names[BCS_P_COUNT] = { + "readable", "readableEnded", "readableLength", "readableHighWaterMark", "readableObjectMode", + "readableFlowing", "writable", "writableEnded", "writableFinished", "writableLength", + "writableHighWaterMark", "writableNeedDrain", "writableCorked", "writableObjectMode", + "destroyed", "closed", "errored", +}; + +static ScrDyn *bcs_flowing(ScrStream *s) { + double flowing = scr_stream_flowing(s); + return flowing < 0 ? scr_dyn_new_null() : scr_dyn_new_bool(flowing != 0); +} + +static ScrDyn *bcs_errored(ScrStream *s) { + ScrError *err = scr_stream_errored(s); + if (!err) return scr_dyn_new_null(); + ScrDyn *value = scr_dyn_from_error(err); + scr_error_release(err); + return value; +} + +static ScrDyn *bcs_getter_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + int property = (int)scr_box_get_f64(closure->caps[0]); + ScrDyn *self = scr_dyn_this_get(); + ScrStream *s = bcs_of(self); + scr_dyn_release(self); + /* Node's getters answer undefined (or false) before the constructor ran. */ + if (!s) return property == BCS_P_DESTROYED ? scr_dyn_new_bool(false) : bcs_undefined(); + const char *name = bcs_property_names[property]; + switch (property) { + case BCS_P_READABLE_FLOWING: + return bcs_flowing(s); + case BCS_P_ERRORED: + return bcs_errored(s); + case BCS_P_WRITABLE_OBJECT_MODE: + return scr_dyn_new_bool(false); + case BCS_P_READABLE_LENGTH: + case BCS_P_READABLE_HWM: + case BCS_P_WRITABLE_LENGTH: + case BCS_P_WRITABLE_HWM: + case BCS_P_WRITABLE_CORKED: + return scr_dyn_new_num(scr_stream_prop(s, name)); + default: + return scr_dyn_new_bool(scr_stream_prop(s, name) != 0); + } +} + +/* The destroyed setter: Node sets the states' flag without destroying. + * The readable and writable setters store a flag the runtime state does + * not keep (legacy code assigns `this.readable = true`), so they are + * accepted and have no effect. */ +static ScrDyn *bcs_setter_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + int property = (int)scr_box_get_f64(closure->caps[0]); + ScrDyn *self = scr_dyn_this_get(); + ScrStream *s = bcs_of(self); + scr_dyn_release(self); + if (s && property == BCS_P_DESTROYED) scr_stream_set_destroyed(s, scr_dyn_truthy(bcs_arg(args, argc, 0))); + return bcs_undefined(); +} + +static void bcs_define(ScrDyn *target, const char *name, ScrDyn *getter, ScrDyn *setter) { + ScrDyn *descriptor = scr_dyn_new_obj(); + bcs_set(descriptor, "get", getter); + if (setter) bcs_set(descriptor, "set", setter); + bcs_set(descriptor, "enumerable", scr_dyn_new_bool(false)); + bcs_set(descriptor, "configurable", scr_dyn_new_bool(true)); + ScrDyn *key = bcs_key(name); + ScrDyn *defined = scr_dyn_define_property(target, key, descriptor); + if (defined) scr_dyn_release(defined); + scr_dyn_release(key); + scr_dyn_release(descriptor); +} + +static void bcs_accessor(ScrDyn *prototype, int property) { + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[0], property); + const char *name = bcs_property_names[property]; + ScrDyn *getter = scr_dyn_new_func(closure, bcs_getter_call, 0, "native:stream", name); + ScrDyn *setter = NULL; + if (property == BCS_P_DESTROYED || property == BCS_P_READABLE || property == BCS_P_WRITABLE) { + ScrClosure *set = scr_closure_new(NULL, 1); + set->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(set->caps[0], property); + setter = scr_dyn_new_func(set, bcs_setter_call, 1, "native:stream", name); + } + bcs_define(prototype, name, getter, setter); +} + +/* ── _readableState / _writableState views ──────────────────────────── */ + +static const char *const bcs_rs_names[] = { + "highWaterMark", "length", "ended", "endEmitted", "reading", "destroyed", "objectMode", + "needReadable", "emittedReadable", "readableListening", "resumeScheduled", "errorEmitted", + "closed", "constructed", "flowing", "errored", +}; +static const char *const bcs_ws_names[] = { + "highWaterMark", "length", "ended", "ending", "finished", "prefinished", "destroyed", + "objectMode", "needDrain", "corked", "closed", "constructed", "errorEmitted", + "bufferedRequestCount", "errored", +}; +#define BCS_RS_COUNT (sizeof bcs_rs_names / sizeof *bcs_rs_names) +#define BCS_WS_COUNT (sizeof bcs_ws_names / sizeof *bcs_ws_names) + +static SCR_TL ScrDyn *bcs_state_protos[2]; +static SCR_TL bool bcs_teardown_registered; + +static ScrDyn *bcs_state_get(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + bool readable = scr_box_get_f64(closure->caps[0]) != 0; + size_t index = (size_t)scr_box_get_f64(closure->caps[1]); + const char *name = readable ? bcs_rs_names[index] : bcs_ws_names[index]; + ScrDyn *self = scr_dyn_this_get(); + ScrStream *s = bcs_of(self); + scr_dyn_release(self); + if (!s) return bcs_undefined(); + if (!strcmp(name, "flowing")) return bcs_flowing(s); + if (!strcmp(name, "errored")) return bcs_errored(s); + char key[48]; + snprintf(key, sizeof key, "%s:%s", readable ? "rs" : "ws", name); + double value = scr_stream_prop(s, key); + bool number = !strcmp(name, "highWaterMark") || !strcmp(name, "length") || !strcmp(name, "corked") || + !strcmp(name, "bufferedRequestCount"); + return number ? scr_dyn_new_num(value) : scr_dyn_new_bool(value != 0); +} + +static void bcs_teardown(void) { + for (int i = 0; i < 2; i++) { + if (bcs_state_protos[i]) scr_dyn_release(bcs_state_protos[i]); + bcs_state_protos[i] = NULL; + } + if (scr_exc_pending()) scr_exc_clear(); +} + +static ScrDyn *bcs_state_proto(bool readable) { + ScrDyn **slot = &bcs_state_protos[readable ? 0 : 1]; + if (!*slot) { + if (!bcs_teardown_registered) { + bcs_teardown_registered = true; + scr_atexit(bcs_teardown); + } + ScrDyn *proto = scr_dyn_new_obj(); + size_t count = readable ? BCS_RS_COUNT : BCS_WS_COUNT; + for (size_t i = 0; i < count; i++) { + ScrClosure *closure = scr_closure_new(NULL, 2); + closure->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[0], readable ? 1 : 0); + closure->caps[1] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[1], (double)i); + const char *name = readable ? bcs_rs_names[i] : bcs_ws_names[i]; + bcs_define(proto, name, scr_dyn_new_func(closure, bcs_state_get, 0, "native:stream", name), NULL); + } + *slot = proto; + } + return *slot; +} + +/* ── the prototypes ─────────────────────────────────────────────────── */ + +static ScrDyn *bcs_default_method(int kind, const char *name) { + ScrDyn *ctor = scr_dyn_builtin_class(kind); + ScrDyn *prototype = bcs_get(ctor, "prototype"); + scr_dyn_release(ctor); + if (!prototype) return NULL; + ScrDyn *method = bcs_get(prototype, name); + scr_dyn_release(prototype); + return method; +} + +static void bcs_writable_members(ScrDyn *prototype) { + bcs_method(prototype, "write", BCS_M_WRITE, 3); + bcs_method(prototype, "cork", BCS_M_CORK, 0); + bcs_method(prototype, "uncork", BCS_M_UNCORK, 0); + bcs_method(prototype, "setDefaultEncoding", BCS_M_SET_DEFAULT_ENCODING, 1); + bcs_method(prototype, "_write", BCS_M_WRITE_DEFAULT, 3); + bcs_set(prototype, "_writev", scr_dyn_new_null()); + bcs_method(prototype, "end", BCS_M_END, 3); + for (int p = BCS_P_WRITABLE; p <= BCS_P_WRITABLE_OBJECT_MODE; p++) bcs_accessor(prototype, p); +} + +/* Node's stream prototypes: Readable and Writable carry their methods, + * Duplex adds Writable's, Transform and PassThrough their _transform. */ +static void bcs_prototype(int kind, ScrDyn *prototype) { + switch (kind) { + case BCS_READABLE: + bcs_method(prototype, "destroy", BCS_M_DESTROY, 2); + bcs_method(prototype, "_destroy", BCS_M_DESTROY_DEFAULT, 2); + bcs_method(prototype, "push", BCS_M_PUSH, 2); + bcs_method(prototype, "unshift", BCS_M_UNSHIFT, 2); + bcs_method(prototype, "isPaused", BCS_M_IS_PAUSED, 0); + bcs_method(prototype, "setEncoding", BCS_M_SET_ENCODING, 1); + bcs_method(prototype, "read", BCS_M_READ, 1); + bcs_method(prototype, "_read", BCS_M_READ_DEFAULT, 1); + bcs_method(prototype, "pipe", BCS_M_PIPE, 2); + bcs_method(prototype, "on", BCS_M_ON, 2); + { + ScrDyn *on = bcs_get(prototype, "on"); + if (on) bcs_set(prototype, "addListener", on); + } + bcs_method(prototype, "removeListener", BCS_M_OFF, 2); + { + ScrDyn *off = bcs_get(prototype, "removeListener"); + if (off) bcs_set(prototype, "off", off); + } + bcs_method(prototype, "removeAllListeners", BCS_M_REMOVE_ALL, 1); + bcs_method(prototype, "resume", BCS_M_RESUME, 0); + bcs_method(prototype, "pause", BCS_M_PAUSE, 0); + bcs_method(prototype, "unpipe", BCS_M_UNPIPE, 1); + bcs_method(prototype, "wrap", BCS_M_WRAP, 1); + for (int p = BCS_P_READABLE; p <= BCS_P_READABLE_FLOWING; p++) bcs_accessor(prototype, p); + bcs_accessor(prototype, BCS_P_DESTROYED); + bcs_accessor(prototype, BCS_P_CLOSED); + bcs_accessor(prototype, BCS_P_ERRORED); + break; + case BCS_WRITABLE: + bcs_method(prototype, "pipe", BCS_M_WRITABLE_PIPE, 0); + bcs_writable_members(prototype); + bcs_method(prototype, "destroy", BCS_M_DESTROY, 2); + bcs_method(prototype, "_destroy", BCS_M_DESTROY_DEFAULT, 2); + bcs_accessor(prototype, BCS_P_DESTROYED); + bcs_accessor(prototype, BCS_P_CLOSED); + bcs_accessor(prototype, BCS_P_ERRORED); + break; + case BCS_DUPLEX: + bcs_writable_members(prototype); + bcs_accessor(prototype, BCS_P_DESTROYED); + break; + case BCS_TRANSFORM: + bcs_method(prototype, "_transform", BCS_M_TRANSFORM_DEFAULT, 3); + break; + case BCS_PASSTHROUGH: + bcs_method(prototype, "_transform", BCS_M_PASSTHROUGH_TRANSFORM, 3); + break; + default: + break; + } + if (scr_exc_pending()) scr_exc_clear(); +} + +static const ScrDynStreamOps bcs_ops = { bcs_construct, bcs_prototype, bcs_event }; + +/* A stream class value (the BC_* kinds of scr_dyn_builtin_class.c) with + * its stream half attached, +1. */ +ScrDyn *scr_dyn_builtin_stream_class(double kind) { + scr_dyn_builtin_class_streams(&bcs_ops); + return scr_dyn_builtin_class(kind); +} + +/* Whether dest's method is its own rather than the stream prototypes'. */ +static bool bcs_overrides(ScrDyn *dest, const char *name) { + ScrDyn *own = bcs_get(dest, name); + if (!own) return true; + ScrDyn *fallback = bcs_default_method(BCS_WRITABLE, name); + ScrDyn *duplex = bcs_default_method(BCS_DUPLEX, name); + bool overridden = !(fallback && scr_dyn_strict_eq(own, fallback)) && !(duplex && scr_dyn_strict_eq(own, duplex)); + if (fallback) scr_dyn_release(fallback); + if (duplex) scr_dyn_release(duplex); + scr_dyn_release(own); + return overridden; +} + +/* pipe and unpipe on a runtime stream's untyped view, which also take + * constructor-function streams. */ +static ScrDyn *bcs_view_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + ScrStream *s = scr_box_get_ref(closure->caps[0]); + bool pipe = scr_box_get_f64(closure->caps[1]) != 0; + ScrDyn *result = NULL; + if (pipe) { + result = bcs_pipe(s, args, argc); + } else { + ScrDyn *first = bcs_arg(args, argc, 0); + ScrStream *d = first->kind == SCR_DYN_UNDEF ? NULL : scr_dyn_builtin_stream_of(first); + if (first->kind == SCR_DYN_UNDEF || d) { + scr_stream_release(scr_stream_unpipe(s, d)); + if (d) scr_stream_release(d); + } else { + scr_dyn_arg_type_fail("destination", "an instance of Stream", first); + } + if (!scr_exc_pending()) result = scr_dyn_this_get(); + } + scr_stream_release(s); + return result; +} + +/* The untyped view of a runtime stream crossing into JavaScript, used in + * place of scr_stream_dyn_view by programs that link this unit: a + * constructor-function stream's own object, or the runtime view with pipe + * and unpipe that take those objects. +1. */ +ScrDyn *scr_dyn_builtin_stream_view(ScrStream *s) { + ScrDyn *object = scr_dyn_builtin_stream_object(s); + if (object) return object; + ScrDyn *view = scr_stream_dyn_view(s); + ScrDyn *prototype = scr_dyn_get_prototype(view); + if (prototype && prototype->kind == SCR_DYN_OBJ) { + for (int pipe = 0; pipe < 2; pipe++) { + ScrClosure *closure = scr_closure_new((void *)bcs_view_call, 2); + closure->caps[0] = scr_box_new_obj(scr_stream_retain_v, scr_stream_release_v, scr_stream_trace); + scr_box_set_ref(closure->caps[0], scr_stream_retain(s)); + closure->caps[1] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(closure->caps[1], pipe); + const char *name = pipe ? "pipe" : "unpipe"; + bcs_set(prototype, name, scr_dyn_new_func(closure, bcs_view_call, 0, "native stream method", name)); + } + } + if (prototype) scr_dyn_release(prototype); + if (scr_exc_pending()) scr_exc_clear(); + return view; +} + +/* A checked cast of an untyped value to a runtime stream class (kind: + * the BC_* numbering): the stream of a constructor-function stream of a + * matching class, +1, or NULL. */ +ScrStream *scr_dyn_builtin_stream_check(const ScrDyn *value, double kind) { + if (value->kind != SCR_DYN_OBJ) return NULL; + ScrStream *s = bcs_of(value); + if (!s) return NULL; + const ScrVt *vt = s->vt; + bool readable = vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post; + bool writable = vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post; + bool duplex = vt->pre >= scr_duplex_vt.pre && vt->pre <= scr_duplex_vt.post; + bool ok = false; + switch ((int)kind) { + case BCS_READABLE: ok = readable; break; + case BCS_WRITABLE: ok = writable || duplex; break; + case BCS_DUPLEX: ok = duplex; break; + case BCS_TRANSFORM: ok = vt->pre >= scr_transform_vt.pre && vt->pre <= scr_transform_vt.post; break; + case BCS_PASSTHROUGH: ok = vt->pre >= scr_passthrough_vt.pre && vt->pre <= scr_passthrough_vt.post; break; + default: break; + } + return ok ? scr_stream_retain(s) : NULL; +} + +/* stream.dynNative (checked-dynamic stream slots, scr_stream_dyn.c) in + * programs that link this unit: a constructor-function stream with the + * requested side (1 readable, 2 writable) answers its runtime stream (+1), + * so a pipe into it, and an unpipe of it, are the runtime's own, unless it + * overrides write() or end(), which the runtime's pipe would bypass: that + * one is NULL and pipes through its JavaScript members. Other values take + * scr_stream_dyn_native. Never throws. */ +ScrStream *scr_dyn_builtin_stream_dyn_native(const ScrDyn *value, double side) { + ScrStream *s = value->kind == SCR_DYN_OBJ ? bcs_of(value) : NULL; + if (s) { + const ScrVt *vt = s->vt; + bool readable = vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post; + bool writable = vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post; + bool duplex = vt->pre >= scr_duplex_vt.pre && vt->pre <= scr_duplex_vt.post; + if (!(duplex || (side == 1 ? readable : writable))) return NULL; + ScrDyn *object = (ScrDyn *)value; + if (side == 2 && (bcs_overrides(object, "write") || bcs_overrides(object, "end"))) { + if (scr_exc_pending()) scr_exc_clear(); + return NULL; + } + return scr_stream_retain(s); + } + return scr_stream_dyn_native(value, side); +} + +/* stream.unboxDyn (an untyped pipe destination or pipeline stage) in + * programs that link this unit: a constructor-function stream answers its + * runtime stream (+1), unless it overrides write() or end(), which the + * runtime's pipe would bypass; other values take scr_stream_unbox_dyn. */ +ScrStream *scr_dyn_builtin_stream_unbox(const ScrDyn *value, const ScrStr *message) { + ScrStream *s = bcs_of(value); + if (s && value->kind == SCR_DYN_OBJ) { + ScrDyn *dest = (ScrDyn *)value; + if (bcs_overrides(dest, "write") || bcs_overrides(dest, "end")) { + if (!scr_exc_pending()) + bcs_unsupported("pipes into a constructor-function stream with its own write() or end() are not supported yet"); + return NULL; + } + return scr_stream_retain(s); + } + return scr_stream_unbox_dyn(value, message); +} diff --git a/packages/runtime/src/scr_events_emitter.c b/packages/runtime/src/scr_events_emitter.c index e0e305bde..4ac7abdf1 100644 --- a/packages/runtime/src/scr_events_emitter.c +++ b/packages/runtime/src/scr_events_emitter.c @@ -518,8 +518,25 @@ static ScrClosure *scr_ee_once_closure(void *adapter, ScrClosure *boxer, ScrEeOn return c; } +static ScrBox *scr_ee_once_promise_box(ScrPromise *p) { + ScrBox *box = scr_box_new_obj(&scr_promise_retain_v, &scr_promise_release_v, &scr_promise_trace_v); + scr_box_set_ref(box, scr_promise_retain(p)); + return box; +} + +static void scr_ee_once_adopt_job(ScrClosure *job) { + ScrPromise *outer = scr_box_get_ref(job->caps[0]); + ScrPromise *inner = scr_box_get_ref(job->caps[1]); + scr_promise_race_join(outer, inner, &scr_promise_adapt_copy); + scr_promise_release(inner); + scr_promise_release(outer); +} + /* boxer and err_boxer are borrowed; the result is +1. err_boxer is NULL - * when the awaited event is 'error' itself. */ + * when the awaited event is 'error' itself. Node's once is an async + * function returning the executor's promise, so the result adopts that + * promise through a thenable job queued here, whose reaction settles the + * result one turn after the executor's promise settles. */ ScrPromise *scr_events_once(ScrEmitter *em, ScrStr *name, ScrClosure *boxer, void *adapter, ScrEeInvoke inv, ScrClosure *err_boxer, void *err_adapter, ScrEeInvoke err_inv) { @@ -558,9 +575,13 @@ ScrPromise *scr_events_once(ScrEmitter *em, ScrStr *name, ScrClosure *boxer, voi scr_caught_release(thrown); scr_promise_reject_pending(st->promise); } - ScrPromise *p = scr_promise_retain(st->promise); + ScrPromise *outer = scr_promise_new(); + ScrClosure *job = scr_closure_new((void *)&scr_ee_once_adopt_job, 2); + job->caps[0] = scr_ee_once_promise_box(outer); + job->caps[1] = scr_ee_once_promise_box(st->promise); + scr_queue_microtask(job); scr_ee_once_release(st); - return p; + return outer; } /* The adapter's tail: `cb` is the firing listener closure, `args` the @@ -1130,3 +1151,9 @@ double scr_emitter_get_default_max(void) { return scr_emitter_default_max; } static SCR_TL double scr_emitter_esm_default_max = 10; double scr_emitter_esm_default_max_get(void) { return scr_emitter_esm_default_max; } void scr_emitter_esm_sync(void) { scr_emitter_esm_default_max = scr_emitter_default_max; } + +/* events.once with a signal (scr_events_once_signal.c) undoes an add whose + * 'newListener' announcement threw. */ +void scr_emitter_unadd_quiet(ScrEmitter *em, ScrStr *name, ScrClosure *cb) { + scr_ee_unadd(em, name, cb); +} diff --git a/packages/runtime/src/scr_events_emitter.h b/packages/runtime/src/scr_events_emitter.h index a92ae1864..39a7f4b5b 100644 --- a/packages/runtime/src/scr_events_emitter.h +++ b/packages/runtime/src/scr_events_emitter.h @@ -64,6 +64,20 @@ ScrPromise *scr_events_once(ScrEmitter *em, ScrStr *name, ScrClosure *boxer, voi ScrEeInvoke inv, ScrClosure *err_boxer, void *err_adapter, ScrEeInvoke err_inv); void scr_events_once_fire(ScrClosure *cb, ScrArr *args); +/* events.once(emitter, name, { signal }) (scr_events_once_signal.c): the + * scr_events_once arguments plus the checked-dynamic signal (borrowed; + * undefined for the plain form). Never throws: a non-AbortSignal value, + * an aborted signal or a throwing 'newListener' listener rejects the +1 + * result. Its adapters end in scr_events_once_signal_fire. */ +typedef struct ScrDyn ScrDyn; /* full definition below (C11 repeat) */ +ScrPromise *scr_events_once_signal(ScrEmitter *em, ScrStr *name, ScrClosure *boxer, void *adapter, + ScrEeInvoke inv, ScrClosure *err_boxer, void *err_adapter, + ScrEeInvoke err_inv, ScrDyn *signal); +void scr_events_once_signal_fire(ScrClosure *cb, ScrArr *args); +/* Removes the listener entry whose closure is `cb` (borrowed) without a + * 'removeListener' event: the undo of an add whose 'newListener' + * announcement threw. */ +void scr_emitter_unadd_quiet(ScrEmitter *em, ScrStr *name, ScrClosure *cb); ScrEmitter *scr_emitter_on(ScrEmitter *em, ScrStr *name, ScrClosure *cb /*moves*/, ScrEeInvoke inv, bool once, bool prepend); /* The dyn-adapted registration family (JS-lane checked-dynamic diff --git a/packages/runtime/src/scr_events_once_signal.c b/packages/runtime/src/scr_events_once_signal.c new file mode 100644 index 000000000..c3c2db37d --- /dev/null +++ b/packages/runtime/src/scr_events_once_signal.c @@ -0,0 +1,303 @@ +/* events.once(emitter, name, { signal }) over typed listeners — Node 24's + * lib/events.js `once` with an options object. Linked only by programs + * that pass the options (the emitter and event-target units together). + * + * The signal is validated first: undefined is the plain form, a value that + * is not an AbortSignal rejects with ERR_INVALID_ARG_TYPE, and an aborted + * signal rejects with an AbortError whose cause is signal.reason, all + * without registering a listener. Otherwise the event's one-shot listener, + * the 'error' one (unless the event is 'error') and a once, + * resist-stop-propagation 'abort' listener on the signal share the state + * below. The event or the error removes the other emitter listener and the + * abort listener, then settles; the abort removes both emitter listeners + * and rejects with the AbortError. + * + * Ownership: the state owns the promise, the name and the signal. The + * emitter is weak in the state; the abort listener holds it strongly (the + * abort is the one settlement that does not come from the emitter itself, + * like the closure Node's abortListener keeps). The three listener closures + * reach the state through link objects; a link clears the state's pointer + * to its closure when that closure is freed, so removals never compare a + * stale identity. The cycle through the signal's listener ends when the + * promise settles or the signal aborts (or at the exit teardown of the + * signal's listeners). */ +#include "scr_runtime.h" + +#include + +typedef struct ScrEosState { + size_t rc; + ScrPromise *promise; /* owned */ + ScrEmitter *em; /* weak: alive whenever a listener fires */ + ScrStr *name; /* owned */ + ScrDyn *signal; /* owned; undefined without an abort listener */ + ScrClosure *event_cb; /* weak, NULL once freed */ + ScrClosure *error_cb; /* weak, NULL once freed or for 'error' */ + ScrDyn *listener; /* weak abort listener, NULL once freed */ + bool settled; +} ScrEosState; + +enum { SCR_EOS_EVENT, SCR_EOS_ERROR, SCR_EOS_ABORT }; + +typedef struct ScrEosLink { + size_t rc; + ScrEosState *st; /* owned */ + int slot; +} ScrEosLink; + +static void scr_eos_oom(void) { scr_trap("scriptc: out of memory in events.once\n"); } + +static void scr_eos_state_release(ScrEosState *st) { + if (--st->rc > 0) return; + scr_promise_release(st->promise); + scr_str_release(st->name); + scr_dyn_release(st->signal); + free(st); +} + +static void *scr_eos_link_retain(void *p) { + ((ScrEosLink *)p)->rc++; + return p; +} + +static void scr_eos_link_release(void *p) { + ScrEosLink *link = p; + if (--link->rc > 0) return; + ScrEosState *st = link->st; + if (link->slot == SCR_EOS_EVENT) st->event_cb = NULL; + else if (link->slot == SCR_EOS_ERROR) st->error_cb = NULL; + else st->listener = NULL; + scr_eos_state_release(st); + free(link); +} + +static ScrBox *scr_eos_link_box(ScrEosState *st, int slot) { + ScrEosLink *link = calloc(1, sizeof *link); + if (!link) scr_eos_oom(); + link->rc = 1; + link->st = st; + link->slot = slot; + st->rc++; + ScrBox *box = scr_box_new_obj(&scr_eos_link_retain, &scr_eos_link_release, NULL); + scr_box_set_ref(box, link); + return box; +} + +/* An emitter listener: caps[0] its boxer (the compiled adapter calls it), + * caps[1] its link. */ +static ScrClosure *scr_eos_closure(void *adapter, ScrClosure *boxer, ScrEosState *st, int slot) { + ScrClosure *c = scr_closure_new(adapter, 2); + ScrBox *bx = scr_box_new(SCR_BOX_FUNC); + scr_box_set_ref(bx, scr_closure_retain(boxer)); + c->caps[0] = bx; + c->caps[1] = scr_eos_link_box(st, slot); + return c; +} + +/* new AbortError(undefined, { cause: signal.reason }), thrown. */ +static void scr_eos_throw_abort(ScrDyn *signal) { + ScrStr *message = scr_str_new("The operation was aborted", 25); + ScrError *e = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + scr_str_release(e->name); + e->name = scr_str_new("AbortError", 10); + /* AbortError assigns code, then name: own enumerable properties */ + scr_error_set_code(e, "ABORT_ERR"); + e->name_present = true; + e->name_enumerable = true; + ScrStr *k = scr_str_new("reason", 6); + ScrDyn *reason = scr_dyn_handle_key_get(signal, k); + scr_str_release(k); + scr_error_define_cause(e, reason ? reason : scr_dyn_undefined()); + if (reason) scr_dyn_release(reason); + scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg()); +} + +/* removeEventListener('abort', listener) on the signal, when the listener + * is still registered. */ +static void scr_eos_remove_abort(ScrEosState *st) { + if (st->listener == NULL) return; + bool aborted; + ScrEventTargetCore *core = scr_et_signal_core_fn ? scr_et_signal_core_fn(st->signal, &aborted) : NULL; + if (!core) return; + ScrStr *abort = scr_str_new("abort", 5); + ScrDyn *type = scr_dyn_new_str(abort); + scr_str_release(abort); + ScrDyn *listener = scr_dyn_retain(st->listener); + ScrDyn *remove_args[2] = {type, listener}; + ScrDyn *r = scr_et_remove_listener(core, remove_args, 2); + if (r) scr_dyn_release(r); + scr_dyn_release(type); + scr_dyn_release(listener); +} + +/* emitter.removeListener(name, cb) for a listener closure still alive. */ +static bool scr_eos_off(ScrEmitter *em, ScrStr *name, ScrClosure *cb) { + if (cb == NULL) return true; + scr_emitter_release(scr_emitter_off(em, name, cb)); + return !scr_exc_pending(); +} + +/* The compiled adapter's tail: `cb` is the firing emitter listener, `args` + * the boxed event arguments (moves in). */ +void scr_events_once_signal_fire(ScrClosure *cb, ScrArr *args) { + ScrEosLink *link = scr_box_get_ref(cb->caps[1]); + ScrEosState *st = link->st; + if (!st->settled) { + st->settled = true; + bool error = link->slot == SCR_EOS_ERROR; + /* Node's errorListener removes the event's listener and resolver the + * error's, each through removeListener; then the abort listener. */ + bool ok; + if (error) { + ok = scr_eos_off(st->em, st->name, st->event_cb); + } else { + ScrStr *error_name = scr_str_new("error", 5); + ok = scr_eos_off(st->em, error_name, st->error_cb); + scr_str_release(error_name); + } + if (ok) { + scr_eos_remove_abort(st); + if (error) { + ScrDyn *reason = (ScrDyn *)scr_arr_get_ref(args, 0); + scr_dyn_throw(reason); /* moves reason */ + scr_promise_reject_pending(st->promise); + } else { + scr_promise_fulfill_ref(st->promise, scr_arr_retain(args), scr_arr_retain_v, + scr_arr_release_v, args->cyc ? scr_arr_trace_v : NULL); + } + } + } + scr_arr_release(args); + scr_eos_link_release(link); +} + +/* The signal's 'abort' listener: caps[0] its link, caps[1] the emitter. */ +static ScrDyn *scr_eos_abort_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + ScrEosLink *link = scr_box_get_ref(clo->caps[0]); + ScrEmitter *em = scr_box_get_ref(clo->caps[1]); + ScrEosState *st = link->st; + bool ok = true; + if (!st->settled) { + st->settled = true; + ok = scr_eos_off(em, st->name, st->event_cb); + if (ok) { + ScrStr *error_name = scr_str_new("error", 5); + ok = scr_eos_off(em, error_name, st->error_cb); + scr_str_release(error_name); + } + if (ok) { + scr_eos_throw_abort(st->signal); + scr_promise_reject_pending(st->promise); + } + } + scr_emitter_release(em); + scr_eos_link_release(link); + return ok ? scr_dyn_retain(scr_dyn_undefined()) : NULL; +} + +static ScrBox *scr_eos_promise_box(ScrPromise *p) { + ScrBox *box = scr_box_new_obj(&scr_promise_retain_v, &scr_promise_release_v, &scr_promise_trace_v); + scr_box_set_ref(box, scr_promise_retain(p)); + return box; +} + +static void scr_eos_adopt_job(ScrClosure *job) { + ScrPromise *outer = scr_box_get_ref(job->caps[0]); + ScrPromise *inner = scr_box_get_ref(job->caps[1]); + scr_promise_race_join(outer, inner, &scr_promise_adapt_copy); + scr_promise_release(inner); + scr_promise_release(outer); +} + +/* Node's once is an async function returning the executor's promise: its + * result adopts that promise through a thenable job queued now, whose + * reaction settles the result one turn after the inner promise settles. + * Moves `inner` in; the result is +1. */ +static ScrPromise *scr_eos_adopt(ScrPromise *inner) { + ScrPromise *outer = scr_promise_new(); + ScrClosure *job = scr_closure_new((void *)&scr_eos_adopt_job, 2); + job->caps[0] = scr_eos_promise_box(outer); + job->caps[1] = scr_eos_promise_box(inner); + scr_promise_release(inner); + scr_queue_microtask(job); + return outer; +} + +/* boxer and err_boxer are borrowed (err_boxer is NULL when the awaited + * event is 'error'); signal is borrowed. The result is +1 and never throws: + * every failure rejects it, as Node's async function does. */ +ScrPromise *scr_events_once_signal(ScrEmitter *em, ScrStr *name, ScrClosure *boxer, void *adapter, + ScrEeInvoke inv, ScrClosure *err_boxer, void *err_adapter, + ScrEeInvoke err_inv, ScrDyn *signal) { + ScrPromise *result = scr_promise_new(); + bool with_signal = signal->kind != SCR_DYN_UNDEF; + if (with_signal) { + bool aborted = false; + ScrEventTargetCore *core = + scr_et_signal_core_fn ? scr_et_signal_core_fn(signal, &aborted) : NULL; + if (!core) { + scr_dyn_prop_type_fail("options.signal", "an instance of AbortSignal", signal); + scr_promise_reject_pending(result); + return result; + } + if (aborted) { + scr_eos_throw_abort(signal); + scr_promise_reject_pending(result); + return result; + } + } + ScrEosState *st = calloc(1, sizeof *st); + if (!st) scr_eos_oom(); + st->rc = 1; + st->promise = result; + st->em = em; + st->name = scr_str_retain(name); + st->signal = scr_dyn_retain(signal); + ScrClosure *event_cb = scr_eos_closure(adapter, boxer, st, SCR_EOS_EVENT); + ScrClosure *error_cb = + err_boxer != NULL ? scr_eos_closure(err_adapter, err_boxer, st, SCR_EOS_ERROR) : NULL; + st->event_cb = event_cb; + st->error_cb = error_cb; + /* Node registers inside the promise's executor: a throwing 'newListener' + * listener rejects the promise, and the listener it announced is never + * added (the event's listener stays when only the 'error' announcement + * throws). No abort listener follows a throw. */ + ScrCaught *thrown = NULL; + scr_emitter_release(scr_emitter_on(em, name, event_cb, inv, true, false)); + if (scr_exc_pending()) { + thrown = scr_exc_take(); + scr_emitter_unadd_quiet(em, name, event_cb); + if (error_cb != NULL) scr_closure_release(error_cb); /* never registered */ + } else if (error_cb != NULL) { + ScrStr *error = scr_str_new("error", 5); + scr_emitter_release(scr_emitter_on(em, error, error_cb, err_inv, true, false)); + if (scr_exc_pending()) { + thrown = scr_exc_take(); + scr_emitter_unadd_quiet(em, error, error_cb); + } + scr_str_release(error); + } + if (thrown != NULL) { + st->settled = true; + scr_rethrow(thrown); + scr_caught_release(thrown); + scr_promise_reject_pending(st->promise); + } else if (with_signal) { + ScrClosure *clo = scr_closure_new((void *)&scr_eos_abort_thunk, 2); + clo->caps[0] = scr_eos_link_box(st, SCR_EOS_ABORT); + clo->caps[1] = scr_box_new_obj(&scr_emitter_retain_v, &scr_emitter_release_v, NULL); + scr_box_set_ref(clo->caps[1], scr_emitter_retain(em)); + ScrDyn *listener = scr_dyn_new_func(clo, &scr_eos_abort_thunk, 0, "()", "abortListener"); + st->listener = listener; + ScrDyn *dispose = scr_events_add_abort_listener(signal, listener); + if (dispose) scr_dyn_release(dispose); + /* The signal's listener entry keeps it; the state's pointer is weak. */ + scr_dyn_release(listener); + } + ScrPromise *inner = scr_promise_retain(st->promise); + scr_eos_state_release(st); + return scr_eos_adopt(inner); +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 9746e8eb1..4b0ef7d1c 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -8567,6 +8567,34 @@ static const char *scr_dyn_kind_name(const ScrDyn *d) { return "unknown"; } +static SCR_TL ScrDyn *scr_dyn_fallback_value; +static SCR_TL bool scr_dyn_fallback_registered; + +static void scr_dyn_fallback_cleanup(void) { + scr_dyn_release(scr_dyn_fallback_value); + scr_dyn_fallback_value = NULL; +} + +bool scr_dyn_check_recover(ScrDyn *d) { + if (!scr_exc_pending() || scr_exc_active->kind == SCR_EXC_TERMINATE) return false; + scr_exc_clear(); + if (!scr_dyn_fallback_registered) { + scr_dyn_fallback_registered = true; + scr_atexit(scr_dyn_fallback_cleanup); + } + scr_dyn_release(scr_dyn_fallback_value); + scr_dyn_fallback_value = scr_dyn_retain(d); + return true; +} + +bool scr_dyn_check_fallback_taken(void) { return scr_dyn_fallback_value != NULL; } + +ScrDyn *scr_dyn_check_fallback_take(void) { + ScrDyn *value = scr_dyn_fallback_value; + scr_dyn_fallback_value = NULL; + return value; +} + void scr_dyn_check_fail(const ScrDynPath *path, const char *want, const ScrDyn *got) { ScrJsonBuf b; scr_jb_init(&b); diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c index 374b5469a..cb9bd8a23 100644 --- a/packages/runtime/src/scr_message.c +++ b/packages/runtime/src/scr_message.c @@ -282,7 +282,7 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input) /* Published values (immortal) travel by pointer: a typed value as a * capsule over the same object, a dyn graph as itself. */ if (input->kind == SCR_DYN_TYPED_REF && input->v.typed_ref.ptr && - *(const size_t *)input->v.typed_ref.ptr == SIZE_MAX) { + SCR_RC_COUNT(input->v.typed_ref.ptr) == SCR_RC_IMMORTAL) { scr_sa_thread_shares(); encoder->visits[id].view = (ScrDyn *)input; ScrMessageNode *node = &encoder->message->nodes[id]; diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index 9dedc6844..b3ff2814d 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -37,9 +37,14 @@ typedef struct { uintptr_t obj; uint32_t rc; - uint32_t buf; /* a candidate taken out of its buffer: its index + 1, else 0 */ + uint32_t buf; /* a candidate taken out of its buffer: its index + 1, else 0; + * SCR_PUB_LOG_COUNT: `rc` is the count of the low half */ } ScrPubLog; +/* In ScrPubLog.buf: the object keeps its class id in the high half of + * its rc word (SCR_PUB_CLASS_ID), and `rc` is the count of the low half. */ +#define SCR_PUB_LOG_COUNT UINT32_C(0x80000000) + #define SCR_PUB_DYN ((uintptr_t)1) #define SCR_PUB_RC_LIFETIME UINT32_C(0xfffffffe) #define SCR_PUB_RC_BIG UINT32_C(0xffffffff) @@ -103,7 +108,7 @@ static void *scr_pub_grow(void *p, size_t *cap, size_t size, void *inline_buf) { void scr_pub_str_fn(void *v, ScrPub *ctx); void scr_pub_push(ScrPub *ctx, void *v, ScrPubFn fn) { - if (!v || *(size_t *)v == SIZE_MAX || ctx->refusal) return; + if (!v || SCR_RC_COUNT(v) == SCR_RC_IMMORTAL || ctx->refusal) return; if (fn == scr_pub_str_fn) { /* a leaf: mark it now */ scr_pub_str_fn(v, ctx); return; @@ -120,7 +125,9 @@ void scr_pub_push(ScrPub *ctx, void *v, ScrPubFn fn) { * touch the objects again. */ bool scr_pub_mark(ScrPub *ctx, void *obj, bool cyc, int kind) { size_t *rc = obj; - if (*rc == SIZE_MAX || ctx->refusal) return false; + if (SCR_RC_COUNT(obj) == SCR_RC_IMMORTAL || ctx->refusal) return false; + bool class_id = (kind & SCR_PUB_CLASS_ID) != 0; + kind &= ~SCR_PUB_CLASS_ID; if (ctx->logged == ctx->log_cap) ctx->log = scr_pub_grow(ctx->log, &ctx->log_cap, sizeof *ctx->log, ctx->log_inline); #ifdef SCR_RC_AUDIT @@ -135,10 +142,11 @@ bool scr_pub_mark(ScrPub *ctx, void *obj, bool cyc, int kind) { buf = scr_cyc_pub_take(obj) + 1; ctx->taken++; } - size_t count = *rc; + size_t count = class_id ? SCR_RC_COUNT(obj) : *rc; uint32_t logged = (uint32_t)count; + if (class_id) buf |= SCR_PUB_LOG_COUNT; #if SIZE_MAX > UINT32_MAX - if (count >= SCR_PUB_RC_LIFETIME) { + if (!class_id && count >= SCR_PUB_RC_LIFETIME) { if (count == (size_t)1 << 63) logged = SCR_PUB_RC_LIFETIME; else { if (ctx->nbig == ctx->big_cap) ctx->big = scr_pub_grow(ctx->big, &ctx->big_cap, sizeof *ctx->big, NULL); @@ -148,7 +156,8 @@ bool scr_pub_mark(ScrPub *ctx, void *obj, bool cyc, int kind) { } #endif *e = (ScrPubLog){(uintptr_t)obj, logged, buf}; - *rc = SIZE_MAX; + if (class_id) SCR_RC_COUNT(obj) = SCR_RC_IMMORTAL; + else *rc = SIZE_MAX; return true; } @@ -372,7 +381,7 @@ static bool scr_pub_keep_ok(size_t bytes) { } void scr_publish(void *root, ScrPubFn fn) { - if (!root || *(size_t *)root == SIZE_MAX) return; + if (!root || SCR_RC_COUNT(root) == SCR_RC_IMMORTAL) return; ScrPub ctx; if (scr_pub_keep.stack) { ctx.stack = scr_pub_keep.stack; @@ -417,13 +426,18 @@ void scr_publish(void *root, ScrPubFn fn) { for (size_t i = ctx.logged; i-- > 0;) { const ScrPubLog *e = &ctx.log[i]; void *obj = scr_pub_log_obj(e); - size_t rc = e->rc; + uint32_t buf = e->buf & ~SCR_PUB_LOG_COUNT; + if (e->buf & SCR_PUB_LOG_COUNT) { + SCR_RC_COUNT(obj) = (ScrRcCount)e->rc; + } else { + size_t rc = e->rc; #if SIZE_MAX > UINT32_MAX - if (e->rc == SCR_PUB_RC_LIFETIME) rc = (size_t)1 << 63; - else if (e->rc == SCR_PUB_RC_BIG) rc = ctx.big[--ctx.nbig]; + if (e->rc == SCR_PUB_RC_LIFETIME) rc = (size_t)1 << 63; + else if (e->rc == SCR_PUB_RC_BIG) rc = ctx.big[--ctx.nbig]; #endif - *(size_t *)obj = rc; - if (e->buf) scr_cyc_pub_restore(obj, e->buf - 1); + *(size_t *)obj = rc; + } + if (buf) scr_cyc_pub_restore(obj, buf - 1); } if (ctx.taken) scr_cyc_pub_fill(); scr_throw_error_msg(SCR_ERR_TYPE, ctx.refusal, strlen(ctx.refusal)); diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 8611741da..10eb96d18 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -6,6 +6,41 @@ #include "scr_core.h" +/* ── reference counts ───────────────────────────────────────────────── + * Every heap object's first member is its `size_t rc` word. On 64-bit + * targets the reference count is the word's low 32 bits: an emitted + * instance of a class in a program-rooted hierarchy keeps its class id (its + * class's preorder number + 1) in the high 32 bits (llvm/classes.ts + * classIdRcWord), and no other object's count reaches 2^32. A count of SCR_RC_IMMORTAL marks + * an immortal object; for every object but those class instances the whole + * word is then SIZE_MAX. Code that reads or writes the count of an object of + * any kind goes through SCR_RC_COUNT. */ +#if SIZE_MAX > UINT32_MAX +typedef uint32_t ScrRcCount; +#ifdef __BYTE_ORDER__ +_Static_assert(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__, + "the reference count is the low half of the rc word, at offset 0"); +#endif +#else +typedef size_t ScrRcCount; +#endif +#define SCR_RC_IMMORTAL ((ScrRcCount)-1) +#define SCR_RC_COUNT(obj) (*(ScrRcCount *)(obj)) + +/* The class id in the high half of `obj`'s rc word, or 0 for an object + * without one: the high half of any other object's word is 0, or 0xffffffff + * (immortal) or 0x80000000 (program-lifetime), and a class id is never one of + * these. */ +static inline uint32_t scr_rc_class_id(const void *obj) { +#if SIZE_MAX > UINT32_MAX + uint32_t high = (uint32_t)(*(const size_t *)obj >> 32); + return high == UINT32_MAX || high == UINT32_C(0x80000000) ? 0 : high; +#else + (void)obj; + return 0; +#endif +} + /* ── cycle collection (scr_cycle.c) ─────────────────────────────────── * Reference counting alone cannot free cycles, so every object that can * participate in one — capture boxes, heap closures, unions, promises, and @@ -591,6 +626,10 @@ enum { SCR_PUB_KIND_OBJECT = 4, SCR_PUB_KIND_UNION = 5, SCR_PUB_KIND_DYN = 6, + /* Or'ed into a kind: an object whose rc word keeps its class id in + * the high half (SCR_RC_COUNT). The mark, and a refusal's restore, write + * only the count. */ + SCR_PUB_CLASS_ID = 0x100, }; void scr_publish(void *root, ScrPubFn fn); void scr_pub_push(ScrPub *ctx, void *v, ScrPubFn fn); diff --git a/packages/runtime/src/scr_region.c b/packages/runtime/src/scr_region.c index cc094ef0b..a4025610c 100644 --- a/packages/runtime/src/scr_region.c +++ b/packages/runtime/src/scr_region.c @@ -666,11 +666,11 @@ static void scr_rgn_queue(ScrRgnWalk *w, size_t entry, void *p, int kind, void ( * region counted as explained. RC-audit builds still enter it, and stop if * the walk reaches it twice. */ static void scr_rgn_enter(ScrRgnWalk *w, void *p, int kind, void (*walk)(void *, ScrRgnWalk *)) { - if (!p || *(size_t *)p == SIZE_MAX) return; /* immortal: never freed, not walked */ + if (!p || SCR_RC_COUNT(p) == SCR_RC_IMMORTAL) return; /* immortal: never freed, not walked */ #ifdef SCR_RGN_DEBUG - bool single = w->src == 0 && *(size_t *)p == 1 && !w->debug; + bool single = w->src == 0 && SCR_RC_COUNT(p) == 1 && !w->debug; #else - bool single = w->src == 0 && *(size_t *)p == 1; + bool single = w->src == 0 && SCR_RC_COUNT(p) == 1; #endif #ifndef SCR_RC_AUDIT if (single) { @@ -922,7 +922,7 @@ static int64_t scr_rgn_settle(ScrRgnWalk *w, size_t *black_out) { size_t top = 0; for (size_t i = 0; i < n; i++) { ScrRgnEntry *x = &w->ents[i]; - if (*(size_t *)x->obj != (size_t)x->found) { + if (SCR_RC_COUNT(x->obj) != (ScrRcCount)x->found) { x->black = true; stack[top++] = i; } @@ -1041,7 +1041,7 @@ static void scr_rgn_report(const ScrRegion *r, const ScrRgnWalk *w, int64_t unex } for (size_t i = 0; i < w->nents; i++) { const ScrRgnEntry *x = &w->ents[i]; - if (*(size_t *)x->obj == (size_t)x->found) continue; /* not referenced from outside itself */ + if (SCR_RC_COUNT(x->obj) == (ScrRcCount)x->found) continue; /* not referenced from outside itself */ if (printed == lines) { more++; continue; @@ -1055,7 +1055,7 @@ static void scr_rgn_report(const ScrRegion *r, const ScrRgnWalk *w, int64_t unex } const ScrRgnFrom *f = &w->from[at]; fprintf(stderr, " held by %s.%s, with %zu references, %lld of them from what the region owns\n", - f->cls ? f->cls->name : "?", f->cls ? scr_rgn_field_name(f->cls, f->field) : "?", *(size_t *)x->obj, + f->cls ? f->cls->name : "?", f->cls ? scr_rgn_field_name(f->cls, f->field) : "?", (size_t)SCR_RC_COUNT(x->obj), (long long)x->found); } if (more) fprintf(stderr, " %zu more\n", more); @@ -1111,7 +1111,7 @@ static bool scr_rgn_try_free(ScrRgnThread *t, ScrRegion *r, bool tentative) { for (ScrRgnChunk *c = r->chunks; c; c = c->next) c->found = 0; size_t objects = 0; SCR_RGN_EACH(r, s, o) { - if (*(size_t *)o == SIZE_MAX) { + if (SCR_RC_COUNT(o) == SCR_RC_IMMORTAL) { /* Published: other threads may hold it, uncounted. */ r->state = SCR_RGN_PINNED; t->stats.pinned++; diff --git a/packages/runtime/src/scr_slots.c b/packages/runtime/src/scr_slots.c index 7c5bc7f88..84236c0d5 100644 --- a/packages/runtime/src/scr_slots.c +++ b/packages/runtime/src/scr_slots.c @@ -29,7 +29,7 @@ static uint32_t scr_slots_index(void *obj, uint32_t *word) { if (index != 0) return index; index = atomic_fetch_add_explicit(&scr_slots_next, 1, memory_order_relaxed); if (index == 0) scr_trap("scriptc: ThreadSlots ran out of slot indices\n"); - if (*(size_t *)obj != SIZE_MAX) { + if (SCR_RC_COUNT(obj) != SCR_RC_IMMORTAL) { __atomic_store_n(word, index, __ATOMIC_RELAXED); return index; } diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 56f9fd54b..7d7ec04ed 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -2254,6 +2254,8 @@ void scr_stream_set_flush(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamPlain void scr_stream_set_flush_on_prefinish(ScrStream *s) { s->st->flush_on_prefinish = true; } +void scr_stream_set_destroyed(ScrStream *s, bool destroyed) { s->st->destroyed = destroyed; } + void scr_stream_set_native(ScrStream *s, void *state /*moves*/, void (*release)(void *)) { ScrStreamState *st = s->st; if (st->native) st->native_release(st->native); @@ -4738,12 +4740,14 @@ ScrStream *scr_stream_rl_from_dyn(const ScrDyn *value, bool output) { if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && !memcmp(value->v.typed_ref.type_key, "object:", 7)) { ScrStream *stream = scr_dyn_typed_ref_unbox(value); - const ScrVt *vt = stream->vt; - bool duplex = vt == &scr_duplex_vt || vt == &scr_transform_vt || vt == &scr_passthrough_vt || - scr_vt_within(vt, &scr_duplex_vt); - bool ok = duplex || (output ? vt == &scr_writable_vt || scr_vt_within(vt, &scr_writable_vt) - : vt == &scr_readable_vt || scr_vt_within(vt, &scr_readable_vt)); - if (ok) return stream; + const ScrVt *vt = scr_stream_vt_of(stream); + if (vt) { + bool duplex = vt == &scr_duplex_vt || vt == &scr_transform_vt || vt == &scr_passthrough_vt || + scr_vt_within(vt, &scr_duplex_vt); + bool ok = duplex || (output ? vt == &scr_writable_vt || scr_vt_within(vt, &scr_writable_vt) + : vt == &scr_readable_vt || scr_vt_within(vt, &scr_readable_vt)); + if (ok) return stream; + } value->v.typed_ref.release(stream); } static const char in_m[] = "createInterface with an input that is not a readable stream has no lowering"; @@ -6409,9 +6413,9 @@ static ScrStream *scr_stream_view_receiver(const ScrDyn *value) { if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && !memcmp(value->v.typed_ref.type_key, "object:", 7)) { ScrStream *stream = scr_dyn_typed_ref_unbox(value); - const ScrVt *vt = stream->vt; - if ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || - (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post)) return stream; + const ScrVt *vt = scr_stream_vt_of(stream); + if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || + (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) return stream; value->v.typed_ref.release(stream); } scr_dyn_arg_type_fail("destination", "an instance of Stream", value); @@ -6714,9 +6718,9 @@ ScrStream *scr_stream_unbox_dyn(const ScrDyn *value, const ScrStr *message) { if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && !memcmp(value->v.typed_ref.type_key, "object:", 7)) { ScrStream *stream = scr_dyn_typed_ref_unbox(value); - const ScrVt *vt = stream->vt; - if ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || - (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post)) return stream; + const ScrVt *vt = scr_stream_vt_of(stream); + if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || + (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) return stream; value->v.typed_ref.release(stream); } scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 69da3dc0e..99f58331c 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -48,6 +48,13 @@ typedef struct ScrStream { ScrStreamState *st; } ScrStream; +/* The vtable of an object boxed as an "object:" typed ref, which may be any + * program class instance: NULL for one with a class id in its rc word + * (scr_rc.h), which has no vtable word and is no stream. */ +static inline const ScrVt *scr_stream_vt_of(const ScrStream *s) { + return scr_rc_class_id(s) == 0 ? s->vt : NULL; +} + extern SCR_TL ScrVt scr_readable_vt, scr_writable_vt, scr_duplex_vt, scr_transform_vt, scr_passthrough_vt; /* main() stamps pre/post */ @@ -136,6 +143,13 @@ ScrStream *scr_stream_pipe_general(ScrStream *src, ScrStream *dst, bool end); /* A boxed native stream out of a checked-dynamic pipeline stage (+1), or * NULL with `message` thrown as the not-supported Error. */ ScrStream *scr_stream_unbox_dyn(const struct ScrDyn *value, const ScrStr *message); +/* Checked-dynamic stream slots (scr_stream_dyn.c): the native stream a + * value boxes with the requested side (1 readable, 2 writable; +1, or + * NULL), and readable.pipe()/unpipe() for a destination that is not a + * native stream. */ +ScrStream *scr_stream_dyn_native(const struct ScrDyn *value, double side); +void scr_stream_pipe_dyn(ScrStream *src, struct ScrDyn *src_box, struct ScrDyn *dest, bool end); +void scr_stream_unpipe_dyn(ScrStream *src, struct ScrDyn *dest); ScrStream *scr_stream_new_duplex_dyn(struct ScrDyn *opts); ScrStream *scr_stream_new_transform_dyn(struct ScrDyn *opts); ScrStream *scr_stream_new_passthrough_dyn(struct ScrDyn *opts); @@ -442,6 +456,9 @@ void scr_stream_set_flush(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv); * released when the stream is freed; scr_stream_native answers it * (borrowed) when it was set with `release`, else NULL. */ void scr_stream_set_flush_on_prefinish(ScrStream *s); +/* `stream.destroyed = value` (scr_dyn_builtin_stream.c): Node's setter + * stores the states' flag without destroying the stream. */ +void scr_stream_set_destroyed(ScrStream *s, bool destroyed); void scr_stream_set_native(ScrStream *s, void *state, void (*release)(void *)); void *scr_stream_native(ScrStream *s, void (*release)(void *)); diff --git a/packages/runtime/src/scr_stream_dyn.c b/packages/runtime/src/scr_stream_dyn.c new file mode 100644 index 000000000..11dcf6ee2 --- /dev/null +++ b/packages/runtime/src/scr_stream_dyn.c @@ -0,0 +1,393 @@ +/* Checked-dynamic stream slots (the compiler's dyn-streams.ts). + * + * A program whose NodeJS.ReadableStream / NodeJS.WritableStream slots also + * hold JavaScript stream objects compiles them as checked-dynamic values: a + * native stream rides its typed capsule, any other value is the JavaScript + * object itself. Member calls test the value here and take the native + * stream path or the value's own members. Linked only by such programs + * (the stream and dyn-invoke units together). */ +#include "scr_runtime.h" + +#include +#include + +static bool scr_stream_dyn_within(const ScrVt *vt, const ScrVt *cls) { + return vt == cls || (cls->pre <= vt->pre && vt->pre <= cls->post); +} + +/* The native stream a checked-dynamic value boxes (+1), when it has the + * requested side (1 readable, 2 writable; Duplex, Transform and PassThrough + * have both), or NULL for any other value. Never throws. */ +ScrStream *scr_stream_dyn_native(const ScrDyn *value, double side) { + if (value->kind != SCR_DYN_TYPED_REF || value->v.typed_ref.type_key_len <= 7 || + memcmp(value->v.typed_ref.type_key, "object:", 7) != 0) + return NULL; + ScrStream *stream = scr_dyn_typed_ref_unbox(value); + const ScrVt *vt = scr_stream_vt_of(stream); + if (vt == NULL) { + value->v.typed_ref.release(stream); + return NULL; + } + bool duplex = scr_stream_dyn_within(vt, &scr_duplex_vt) || + scr_stream_dyn_within(vt, &scr_transform_vt) || + scr_stream_dyn_within(vt, &scr_passthrough_vt); + bool ok = duplex || (side == 1 ? scr_stream_dyn_within(vt, &scr_readable_vt) + : scr_stream_dyn_within(vt, &scr_writable_vt)); + if (ok) return stream; + value->v.typed_ref.release(stream); + return NULL; +} + +/* ── readable.pipe(destination) into a JavaScript stream object ───────── + * Node's Readable.prototype.pipe (lib/internal/streams/readable.js) over a + * native source and a destination that is not a native stream: the same + * listeners in the same order, with the destination's members called + * through the dyn-invoke unit. The context holds the source, its boxed + * value (what 'pipe' and 'unpipe' pass), the destination and the boxed + * listeners until cleanup; the live list breaks the listener cycles at + * exit without calling JavaScript. */ + +enum { + SCR_DYN_PIPE_DATA, + SCR_DYN_PIPE_END, + SCR_DYN_PIPE_UNPIPE, + SCR_DYN_PIPE_DRAIN, + SCR_DYN_PIPE_ERROR, + SCR_DYN_PIPE_CLOSE, + SCR_DYN_PIPE_FINISH, + SCR_DYN_PIPE_ON_UNPIPE, + SCR_DYN_PIPE_COUNT +}; + +typedef struct ScrDynPipe { + size_t rc; + ScrStream *src; /* +1 until dropped */ + ScrDyn *src_box; /* +1 until dropped */ + ScrDyn *dest; /* +1 until dropped */ + ScrDyn *l[SCR_DYN_PIPE_COUNT]; /* +1 boxed listeners, or NULL */ + bool end, cleaned, awaiting_drain, unpiped; + struct ScrDynPipe *live_prev, *live_next; +} ScrDynPipe; + +static SCR_TL ScrDynPipe *scr_dyn_pipe_live = NULL; + +/* A string value for an event name (+1). */ +static ScrDyn *scr_dyn_pipe_str(const char *text, size_t len) { + ScrStr *s = scr_str_new(text, len); + ScrDyn *value = scr_dyn_new_str(s); + scr_str_release(s); + return value; +} +static SCR_TL bool scr_dyn_pipe_atexit_set = false; + +static void *scr_dyn_pipe_retain_v(void *p) { + ((ScrDynPipe *)p)->rc++; + return p; +} + +static void scr_dyn_pipe_drop(ScrDynPipe *p) { + if (p->live_prev) p->live_prev->live_next = p->live_next; + else if (scr_dyn_pipe_live == p) scr_dyn_pipe_live = p->live_next; + if (p->live_next) p->live_next->live_prev = p->live_prev; + p->live_prev = p->live_next = NULL; + for (size_t i = 0; i < SCR_DYN_PIPE_COUNT; i++) { + ScrDyn *l = p->l[i]; + p->l[i] = NULL; + if (l) scr_dyn_release(l); + } + if (p->src) { + ScrStream *src = p->src; + p->src = NULL; + scr_stream_release(src); + } + if (p->src_box) { + ScrDyn *box = p->src_box; + p->src_box = NULL; + scr_dyn_release(box); + } + if (p->dest) { + ScrDyn *dest = p->dest; + p->dest = NULL; + scr_dyn_release(dest); + } +} + +static void scr_dyn_pipe_release_v(void *v) { + ScrDynPipe *p = v; + if (--p->rc > 0) return; + scr_dyn_pipe_drop(p); + free(p); +} + +static void scr_dyn_pipe_atexit(void) { + while (scr_dyn_pipe_live) { + ScrDynPipe *p = scr_dyn_pipe_retain_v(scr_dyn_pipe_live); + scr_dyn_pipe_drop(p); + scr_dyn_pipe_release_v(p); + } +} + +/* destination.method(args...), the result discarded; false on a throw. */ +static bool scr_dyn_pipe_call(ScrDynPipe *p, const char *method, ScrDyn *const *args, size_t argc) { + ScrDyn *result = scr_dyn_invoke(p->dest, method, args, argc, method); + if (result) scr_dyn_release(result); + return !scr_exc_pending(); +} + +static bool scr_dyn_pipe_dest_off(ScrDynPipe *p, const char *event, size_t which) { + if (!p->l[which]) return true; + ScrDyn *args[2] = {scr_dyn_pipe_str(event, strlen(event)), p->l[which]}; + bool ok = scr_dyn_pipe_call(p, "removeListener", args, 2); + scr_dyn_release(args[0]); + return ok; +} + +static void scr_dyn_pipe_src_off(ScrDynPipe *p, const char *event, size_t which) { + if (!p->l[which] || !p->src) return; + ScrStr *name = scr_str_new(event, strlen(event)); + scr_emitter_release(scr_emitter_off_dyn((ScrEmitter *)p->src, name, p->l[which])); + scr_str_release(name); +} + +static void scr_dyn_pipe_ondrain(ScrDynPipe *p) { + p->awaiting_drain = false; + if (!p->src) return; + ScrStr *data = scr_str_new("data", 4); + double listeners = scr_emitter_listener_count((ScrEmitter *)p->src, data); + scr_str_release(data); + if (listeners > 0) scr_stream_release(scr_stream_resume(p->src)); +} + +/* pipe's cleanup(): every listener off, then a pending drain resumes. */ +static void scr_dyn_pipe_cleanup(ScrDynPipe *p) { + if (p->cleaned) return; + if (!scr_dyn_pipe_dest_off(p, "close", SCR_DYN_PIPE_CLOSE) || + !scr_dyn_pipe_dest_off(p, "finish", SCR_DYN_PIPE_FINISH) || + !scr_dyn_pipe_dest_off(p, "drain", SCR_DYN_PIPE_DRAIN) || + !scr_dyn_pipe_dest_off(p, "error", SCR_DYN_PIPE_ERROR) || + !scr_dyn_pipe_dest_off(p, "unpipe", SCR_DYN_PIPE_ON_UNPIPE)) + return; + scr_dyn_pipe_src_off(p, "end", SCR_DYN_PIPE_END); + scr_dyn_pipe_src_off(p, "end", SCR_DYN_PIPE_UNPIPE); + scr_dyn_pipe_src_off(p, "data", SCR_DYN_PIPE_DATA); + p->cleaned = true; + if (p->l[SCR_DYN_PIPE_DRAIN] && p->awaiting_drain) scr_dyn_pipe_ondrain(p); + ScrDynPipe *held = scr_dyn_pipe_retain_v(p); + scr_dyn_pipe_drop(held); + scr_dyn_pipe_release_v(held); +} + +/* src.unpipe(dest): the source pauses (its only pipe goes) and the + * destination hears 'unpipe' with { hasUnpiped: false }. */ +static void scr_dyn_pipe_unpipe(ScrDynPipe *p) { + if (p->unpiped || !p->src) return; + p->unpiped = true; + scr_stream_release(scr_stream_pause(p->src)); + ScrDyn *info = scr_dyn_new_obj(); + scr_dyn_obj_set(info, "hasUnpiped", 10, scr_dyn_new_bool(false)); + ScrDyn *args[3] = {scr_dyn_pipe_str("unpipe", 6), scr_dyn_retain(p->src_box), info}; + (void)scr_dyn_pipe_call(p, "emit", args, 3); + for (size_t i = 0; i < 3; i++) scr_dyn_release(args[i]); +} + +static ScrDyn *scr_dyn_pipe_listener(ScrDynPipe *p, int kind); + +/* pipe's pause(): the source stops until the destination drains. */ +static void scr_dyn_pipe_pause(ScrDynPipe *p) { + if (!p->cleaned) { + p->awaiting_drain = true; + scr_stream_release(scr_stream_pause(p->src)); + } + if (!p->l[SCR_DYN_PIPE_DRAIN] && p->dest) { + p->l[SCR_DYN_PIPE_DRAIN] = scr_dyn_pipe_listener(p, SCR_DYN_PIPE_DRAIN); + ScrDyn *args[2] = {scr_dyn_pipe_str("drain", 5), p->l[SCR_DYN_PIPE_DRAIN]}; + (void)scr_dyn_pipe_call(p, "on", args, 2); + scr_dyn_release(args[0]); + } +} + +static ScrDyn *scr_dyn_pipe_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + ScrDynPipe *p = scr_box_get_ref(clo->caps[0]); /* +1 */ + int kind = (int)scr_box_get_f64(clo->caps[1]); + ScrDyn *first = argc ? args[0] : scr_dyn_undefined(); + switch (kind) { + case SCR_DYN_PIPE_DATA: { + if (!p->dest) break; + ScrDyn *ret = scr_dyn_invoke(p->dest, "write", &first, 1, "write"); + if (ret) { + if (ret->kind == SCR_DYN_BOOL && !scr_dyn_truthy(ret)) scr_dyn_pipe_pause(p); + scr_dyn_release(ret); + } + break; + } + case SCR_DYN_PIPE_END: + if (p->dest) (void)scr_dyn_pipe_call(p, "end", NULL, 0); + break; + case SCR_DYN_PIPE_UNPIPE: + scr_dyn_pipe_unpipe(p); + break; + case SCR_DYN_PIPE_DRAIN: + scr_dyn_pipe_ondrain(p); + break; + case SCR_DYN_PIPE_ERROR: { + if (!p->dest) break; + ScrDyn *dest = scr_dyn_retain(p->dest); + ScrDyn *self = scr_dyn_retain(p->l[SCR_DYN_PIPE_ERROR]); + scr_dyn_pipe_unpipe(p); + if (!scr_exc_pending()) { + ScrDyn *off[2] = {scr_dyn_pipe_str("error", 5), self}; + ScrDyn *r = scr_dyn_invoke(dest, "removeListener", off, 2, "removeListener"); + if (r) scr_dyn_release(r); + ScrDyn *count = scr_exc_pending() ? NULL : scr_dyn_invoke(dest, "listenerCount", off, 1, "listenerCount"); + if (count && count->kind == SCR_DYN_NUM && !scr_dyn_truthy(count)) { + ScrDyn *emit[2] = {off[0], first}; + r = scr_dyn_invoke(dest, "emit", emit, 2, "emit"); + if (r) scr_dyn_release(r); + } + if (count) scr_dyn_release(count); + scr_dyn_release(off[0]); + } + scr_dyn_release(self); + scr_dyn_release(dest); + break; + } + case SCR_DYN_PIPE_CLOSE: + if (scr_dyn_pipe_dest_off(p, "finish", SCR_DYN_PIPE_FINISH)) scr_dyn_pipe_unpipe(p); + break; + case SCR_DYN_PIPE_FINISH: + if (scr_dyn_pipe_dest_off(p, "close", SCR_DYN_PIPE_CLOSE)) scr_dyn_pipe_unpipe(p); + break; + case SCR_DYN_PIPE_ON_UNPIPE: { + ScrDyn *info = argc > 1 ? args[1] : NULL; + if (p->src_box && scr_dyn_strict_eq(first, p->src_box) && info && info->kind == SCR_DYN_OBJ) { + ScrDyn *has = scr_dyn_obj_read(info, "hasUnpiped", 10); + if (!has || scr_exc_pending()) { /* a throwing getter */ + if (has) scr_dyn_release(has); + break; + } + bool unpiped = has->kind == SCR_DYN_BOOL && !scr_dyn_truthy(has); + scr_dyn_release(has); + if (unpiped) { + scr_dyn_obj_set(info, "hasUnpiped", 10, scr_dyn_new_bool(true)); + scr_dyn_pipe_cleanup(p); + } + } + break; + } + } + scr_dyn_pipe_release_v(p); + return scr_exc_pending() ? NULL : scr_dyn_retain(scr_dyn_undefined()); +} + +static ScrDyn *scr_dyn_pipe_listener(ScrDynPipe *p, int kind) { + ScrClosure *clo = scr_closure_new((void *)scr_dyn_pipe_thunk, 2); + clo->caps[0] = scr_box_new_obj(scr_dyn_pipe_retain_v, scr_dyn_pipe_release_v, NULL); + scr_box_set_ref(clo->caps[0], scr_dyn_pipe_retain_v(p)); + clo->caps[1] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(clo->caps[1], (double)kind); + return scr_dyn_new_func(clo, scr_dyn_pipe_thunk, 1, "native pipe listener", NULL); +} + +/* destination.on / once / prependListener(event, listener) */ +static bool scr_dyn_pipe_listen(ScrDynPipe *p, const char *method, const char *event, int kind) { + p->l[kind] = scr_dyn_pipe_listener(p, kind); + ScrDyn *args[2] = {scr_dyn_pipe_str(event, strlen(event)), p->l[kind]}; + bool ok = scr_dyn_pipe_call(p, method, args, 2); + scr_dyn_release(args[0]); + return ok; +} + +static void scr_dyn_pipe_src_on(ScrDynPipe *p, const char *event, int kind, bool once) { + p->l[kind] = scr_dyn_pipe_listener(p, kind); + ScrStr *name = scr_str_new(event, strlen(event)); + scr_stream_release(scr_stream_on_dyn(p->src, name, p->l[kind], once, false)); + scr_str_release(name); +} + +/* readable.pipe(destination, { end }) for a destination that is not a + * native stream. src and src_box (the source's boxed value) and dest are + * borrowed. Throws what the destination's members throw. */ +void scr_stream_pipe_dyn(ScrStream *src, ScrDyn *src_box, ScrDyn *dest, bool end) { + ScrDynPipe *p = calloc(1, sizeof *p); + if (!p) abort(); + p->rc = 1; + p->src = scr_stream_retain(src); + p->src_box = scr_dyn_retain(src_box); + p->dest = scr_dyn_retain(dest); + p->end = end; + p->live_next = scr_dyn_pipe_live; + if (scr_dyn_pipe_live) scr_dyn_pipe_live->live_prev = p; + scr_dyn_pipe_live = p; + if (!scr_dyn_pipe_atexit_set) { + scr_dyn_pipe_atexit_set = true; + scr_atexit(scr_dyn_pipe_atexit); + } + /* The source's end ends the destination (end: true) or unpipes it. */ + scr_dyn_pipe_src_on(p, "end", end ? SCR_DYN_PIPE_END : SCR_DYN_PIPE_UNPIPE, true); + if (scr_exc_pending() || !scr_dyn_pipe_listen(p, "on", "unpipe", SCR_DYN_PIPE_ON_UNPIPE)) goto out; + scr_dyn_pipe_src_on(p, "data", SCR_DYN_PIPE_DATA, false); + if (scr_exc_pending()) goto out; + { + /* A throwing getter's exception propagates out of pipe(), as Node's + * prependListener helper reads the member the same way. */ + ScrDyn *prepend = scr_dyn_obj_read(dest, "prependListener", 15); + if (!prepend || scr_exc_pending()) { + if (prepend) scr_dyn_release(prepend); + goto out; + } + bool has_prepend = prepend->kind == SCR_DYN_FUNC; + scr_dyn_release(prepend); + if (!scr_dyn_pipe_listen(p, has_prepend ? "prependListener" : "on", "error", SCR_DYN_PIPE_ERROR)) + goto out; + } + if (!scr_dyn_pipe_listen(p, "once", "close", SCR_DYN_PIPE_CLOSE) || + !scr_dyn_pipe_listen(p, "once", "finish", SCR_DYN_PIPE_FINISH)) + goto out; + { + ScrDyn *args[2] = {scr_dyn_pipe_str("pipe", 4), scr_dyn_retain(src_box)}; + bool ok = scr_dyn_pipe_call(p, "emit", args, 2); + scr_dyn_release(args[0]); + scr_dyn_release(args[1]); + if (!ok) goto out; + } + { + ScrDyn *need = scr_dyn_obj_read(dest, "writableNeedDrain", 17); + if (!need || scr_exc_pending()) { + if (need) scr_dyn_release(need); + goto out; + } + bool pause = need->kind == SCR_DYN_BOOL && scr_dyn_truthy(need); + scr_dyn_release(need); + if (pause) scr_dyn_pipe_pause(p); + else if (scr_stream_is_paused(src)) + scr_stream_release(scr_stream_resume(src)); + } +out: + scr_dyn_pipe_release_v(p); +} + +/* readable.unpipe(destination) for a destination that is not a native + * stream: the pipe scr_stream_pipe_dyn registered from src into it + * unpipes, as Node's unpipe does for a piped destination; nothing happens + * when there is none. An undefined destination (unpipe() without one) + * unpipes every JavaScript destination of src, oldest pipe first. + * Borrowed. */ +void scr_stream_unpipe_dyn(ScrStream *src, ScrDyn *dest) { + bool all = dest == NULL || dest->kind == SCR_DYN_UNDEF; + for (;;) { + /* The live list is newest first; the oldest matching pipe goes next. + * Unpiping runs JavaScript ('unpipe' listeners), so each round walks + * the list again. */ + ScrDynPipe *match = NULL; + for (ScrDynPipe *p = scr_dyn_pipe_live; p; p = p->live_next) { + if (p->src != src || p->unpiped || p->cleaned || !p->dest) continue; + if (!all && !scr_dyn_strict_eq(p->dest, dest)) continue; + match = p; + } + if (!match) return; + ScrDynPipe *held = scr_dyn_pipe_retain_v(match); + scr_dyn_pipe_unpipe(held); + scr_dyn_pipe_release_v(held); + if (!all || scr_exc_pending()) return; + } +} diff --git a/packages/runtime/src/scr_string.c b/packages/runtime/src/scr_string.c index 77c020ee0..2b4752b34 100644 --- a/packages/runtime/src/scr_string.c +++ b/packages/runtime/src/scr_string.c @@ -1846,9 +1846,10 @@ static uint16_t *scr_str_units(const ScrStr *s, size_t *n) { * UTF-16 units to bytes through the cursor and checkpoints. Once its * reads would have paid for converting it (one read per 16 bytes), a * receiver up to SCR_SIDX_UNITS_MAX units keeps its code units in its - * entry and reads become loads. The array is freed with the entry, and an - * append drops it (scr_sidx_concat_append). */ -#define SCR_SIDX_UNITS_MAX ((size_t)1 << 20) + * entry and reads become loads. The bound admits source texts of several + * megabytes, which scanners read in full. The array is freed with the + * entry, and an append drops it (scr_sidx_concat_append). */ +#define SCR_SIDX_UNITS_MAX ((size_t)1 << 24) static bool scr_sidx_units(const ScrStr *s, ScrSidx *e) { if (!e->units) { if (++e->reads < 32 || e->reads * 16 < s->len || s->len > SCR_SIDX_UNITS_MAX) return false; diff --git a/packages/runtime/src/scr_string_methods.h b/packages/runtime/src/scr_string_methods.h index 6ed2e09df..1f25b8766 100644 --- a/packages/runtime/src/scr_string_methods.h +++ b/packages/runtime/src/scr_string_methods.h @@ -27,7 +27,8 @@ double scr_str_utf16_len(ScrStr *s); * holds (scr_string.c), published so emitted length and charCodeAt reads of * that string are a compare and a load. Cleared whenever its entry drops * the units (release, append, eviction); all fields zero otherwise. Emitted - * code reads it only where SCR_TL is empty. */ + * code reads it where SCR_TL is empty, and in worker executables on ELF + * targets, whose thread-locals live in the executable's static TLS block. */ typedef struct ScrStrScan { const ScrStr *s; const uint16_t *units; diff --git a/packages/runtime/src/scr_wasm.c b/packages/runtime/src/scr_wasm.c index 8f9244c28..121acacbb 100644 --- a/packages/runtime/src/scr_wasm.c +++ b/packages/runtime/src/scr_wasm.c @@ -90,25 +90,6 @@ #define SCR_WASM_ASAN 1 #endif -ScrPromise *scr_wasm_compile(ScrDyn *bytes); -bool scr_wasm_validate(ScrDyn *bytes); -ScrPromise *scr_wasm_instantiate(ScrDyn *source, ScrDyn *imports); -ScrDyn *scr_wasm_module_new(ScrDyn *bytes); -ScrDyn *scr_wasm_instance_new(ScrDyn *module, ScrDyn *imports); -bool scr_wasm_is(const ScrDyn *value, double kind); -ScrDyn *scr_wasm_error_new(double kind, ScrDyn *message, ScrDyn *options); -ScrDyn *scr_wasm_function(double kind); -ScrDyn *scr_wasm_namespace(void); -ScrDyn *scr_wasm_global_view(void); -ScrDyn *scr_wasm_memory_construct(ScrDyn *descriptor); -ScrDyn *scr_wasm_table_construct(ScrDyn *descriptor, ScrDyn *value); -ScrDyn *scr_wasm_global_construct(ScrDyn *descriptor, ScrDyn *value); -ScrDyn *scr_wasm_construct_without_new(double kind, ScrDyn *arg0, ScrDyn *arg1); -ScrDyn *scr_wasm_module_imports(ScrDyn *module); -ScrDyn *scr_wasm_module_exports(ScrDyn *module); -ScrDyn *scr_wasm_module_custom_sections(ScrDyn *module, ScrDyn *name); -ScrDyn *scr_wasm_value_of(ScrDyn *value); - /* Entry points of translated code. */ /* Sanitized builds do not declare it noreturn: AddressSanitizer would * call __asan_handle_no_return before every call, which warns on the @@ -325,9 +306,50 @@ static void scr_wasm_throw_trap(int code) { /* AddressSanitizer intercepts siglongjmp and warns when it cannot tell the * extent of the running stack, as on the runtime's async fibers. Sanitized - * builds therefore jump with the compiler's builtin pair, which no runtime - * intercepts, and unpoison the abandoned frames themselves. */ -#ifdef SCR_WASM_ASAN + * builds therefore jump with a pair no runtime intercepts and unpoison the + * abandoned frames themselves: the compiler's builtin pair, or on AArch64, + * where the compiler has no builtin setjmp lowering, the pair below. */ +#if defined(SCR_WASM_ASAN) && defined(__aarch64__) +void __asan_unpoison_memory_region(void const volatile *addr, size_t size); +/* x19-x28, the frame and link registers, sp and d8-d15: the registers the + * AArch64 procedure call standard has a callee preserve. */ +typedef uint64_t ScrWasmJmp[21]; +__attribute__((naked, noinline, returns_twice)) static int scr_wasm_setjmp(ScrWasmJmp buf) { + __asm__ volatile("stp x19, x20, [x0, #0]\n" + "stp x21, x22, [x0, #16]\n" + "stp x23, x24, [x0, #32]\n" + "stp x25, x26, [x0, #48]\n" + "stp x27, x28, [x0, #64]\n" + "stp x29, x30, [x0, #80]\n" + "mov x16, sp\n" + "str x16, [x0, #96]\n" + "stp d8, d9, [x0, #104]\n" + "stp d10, d11, [x0, #120]\n" + "stp d12, d13, [x0, #136]\n" + "stp d14, d15, [x0, #152]\n" + "mov w0, #0\n" + "ret\n"); +} +/* Returns 1 from the scr_wasm_setjmp call that filled buf. */ +__attribute__((naked, noinline, noreturn)) static void scr_wasm_longjmp(ScrWasmJmp buf) { + __asm__ volatile("ldp x19, x20, [x0, #0]\n" + "ldp x21, x22, [x0, #16]\n" + "ldp x23, x24, [x0, #32]\n" + "ldp x25, x26, [x0, #48]\n" + "ldp x27, x28, [x0, #64]\n" + "ldp x29, x30, [x0, #80]\n" + "ldr x16, [x0, #96]\n" + "mov sp, x16\n" + "ldp d8, d9, [x0, #104]\n" + "ldp d10, d11, [x0, #120]\n" + "ldp d12, d13, [x0, #136]\n" + "ldp d14, d15, [x0, #152]\n" + "mov w0, #1\n" + "ret\n"); +} +#define SCR_WASM_SETJMP(buf) scr_wasm_setjmp(buf) +#define SCR_WASM_LONGJMP(buf) scr_wasm_longjmp(buf) +#elif defined(SCR_WASM_ASAN) void __asan_unpoison_memory_region(void const volatile *addr, size_t size); typedef void *ScrWasmJmp[5]; #define SCR_WASM_SETJMP(buf) __builtin_setjmp(buf) diff --git a/packages/runtime/src/scr_wasm.h b/packages/runtime/src/scr_wasm.h index 7b0a29689..f7a791a26 100644 --- a/packages/runtime/src/scr_wasm.h +++ b/packages/runtime/src/scr_wasm.h @@ -19,5 +19,13 @@ ScrDyn *scr_wasm_error_new(double kind, ScrDyn *message, ScrDyn *options); ScrDyn *scr_wasm_function(double kind); ScrDyn *scr_wasm_namespace(void); ScrDyn *scr_wasm_global_view(void); +ScrDyn *scr_wasm_memory_construct(ScrDyn *descriptor); +ScrDyn *scr_wasm_table_construct(ScrDyn *descriptor, ScrDyn *value); +ScrDyn *scr_wasm_global_construct(ScrDyn *descriptor, ScrDyn *value); +ScrDyn *scr_wasm_construct_without_new(double kind, ScrDyn *arg0, ScrDyn *arg1); +ScrDyn *scr_wasm_module_imports(ScrDyn *module); +ScrDyn *scr_wasm_module_exports(ScrDyn *module); +ScrDyn *scr_wasm_module_custom_sections(ScrDyn *module, ScrDyn *name); +ScrDyn *scr_wasm_value_of(ScrDyn *value); #endif /* SCR_WASM_H */ diff --git a/packages/threads/package.json b/packages/threads/package.json index e3b5c91ff..358b06dc4 100644 --- a/packages/threads/package.json +++ b/packages/threads/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/threads", - "version": "0.3.6", + "version": "0.4.0", "description": "Zero-copy publication of immutable object graphs to worker threads (scriptc), with a deep-freezing Node.js implementation", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/packages/types/package.json b/packages/types/package.json index b694abb1a..74d19fc24 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@scriptc/types", - "version": "0.3.6", + "version": "0.4.0", "description": "Contract types for scriptc: type-only annotations (Int32, NoThrow, Immutable) that scriptc checks and optimizes, with no runtime semantics", "license": "Apache-2.0", "homepage": "https://scriptc.dev", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cbe529577..3974f346e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,8 +107,8 @@ importers: specifier: workspace:* version: link:../runtime tsc-ts: - specifier: 0.0.3 - version: 0.0.3 + specifier: 0.0.5 + version: 0.0.5 typescript: specifier: 7.0.2 version: 7.0.2 @@ -1357,6 +1357,10 @@ packages: cpu: [x64] os: [win32] + '@scriptc/threads@0.3.6': + resolution: {integrity: sha512-XZb4JhXM/ivhx4MMLxqLLF2mCI28nUK+VVKlCnELO83tacUuGlq7YCe/8LwvzO7fZwy/rCH4+genAYYEc6BF6Q==} + engines: {node: '>=24.0.0'} + '@sinclair/typebox@0.25.24': resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==} @@ -1370,42 +1374,42 @@ packages: '@ts-morph/common@0.11.1': resolution: {integrity: sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==} - '@tsc-ts/darwin-arm64@0.0.3': - resolution: {integrity: sha512-NSP4IhLx658tY21ub3ruraZwUwAC3GCbIf1eM0eOKUHfOBBXqq7LU/Loj3zvlvn/tJUyv2VdIDmzrWI/mxp5sA==} + '@tsc-ts/darwin-arm64@0.0.5': + resolution: {integrity: sha512-yP64b2VpAKWG92KepBqxs1P89wpwiheYwNMk4EmmhSl6aw/ImIBrdHYNjvao3sg6FHZCXTc+PB6Vukh6hyQiJQ==} cpu: [arm64] os: [darwin] - '@tsc-ts/darwin-x64@0.0.3': - resolution: {integrity: sha512-+BNvqyygkMx0/oHkjK8AmsPrE2+OcE/hGzT0wa2dndiH8lCSDBs+77C7Kmy/FSTWi9ItJShp1+AuWKgQPEOztg==} + '@tsc-ts/darwin-x64@0.0.5': + resolution: {integrity: sha512-CEeHOFfpx5H9V3WSq+V7XGU8tfc06nRN818auiRVwuwPHvpti9gHq0HYkvf/43PH1pIGcgf68sQYdIxJhrQDKQ==} cpu: [x64] os: [darwin] - '@tsc-ts/linux-arm64-gnu@0.0.3': - resolution: {integrity: sha512-I3v0J0L387TEkau0BTGiVx2GdstYfOrwVYwb0pJ/LbFURhrcGcsUODP5bdSOIYCtsp3aAX6e2afdqSIlpsxtbw==} + '@tsc-ts/linux-arm64-gnu@0.0.5': + resolution: {integrity: sha512-qwybOqGDEa35eV9x/3WO81ExAUbUMY8288ATC83kUfYGmOAVStaO+JM2IdrsiI8hLWVC7/f0RFgZ+7dyWh9/nA==} cpu: [arm64] os: [linux] libc: [glibc] - '@tsc-ts/linux-arm64-musl@0.0.3': - resolution: {integrity: sha512-f7wD9D6fC1xL7+ifvMAbJ0TqxOuZP04R2/7c8lb7Gor5SFKe85udJoxf0gocK7mbzWzmEiOJ53J1PujJLBVIxA==} + '@tsc-ts/linux-arm64-musl@0.0.5': + resolution: {integrity: sha512-6oSw3NbQIrnxxCRuEe190MbMFoOrM+P+u1usascQMgSjsMcVRnJ/ztvTnRrCDtfPEHJ5EC8drgWIpcdrhGsB0w==} cpu: [arm64] os: [linux] libc: [musl] - '@tsc-ts/linux-x64-gnu@0.0.3': - resolution: {integrity: sha512-BL9zownW4tHv/qqQkF8OlurHNDXi7xtGtJ6PV4qMeRYPMKsivTIGFRoZLW023KcLOr1Zptaof6X3zpv1nJEIpA==} + '@tsc-ts/linux-x64-gnu@0.0.5': + resolution: {integrity: sha512-KrlBLUcG7M6p7lN+ke+kHVLqy7IiY/H0FxFZzq5slXanhrps/BfjdkUahsyREg7KVprr08xFuXqJTBtLEI4eow==} cpu: [x64] os: [linux] libc: [glibc] - '@tsc-ts/linux-x64-musl@0.0.3': - resolution: {integrity: sha512-g4/lE5sULNGUVLM0Px/jrsjLsQQAYSiL+Rl+JZ+l5JE2k1dXroCejT2+IRB/NucVcz6N8EOUiCRZ13zl+Wtpcg==} + '@tsc-ts/linux-x64-musl@0.0.5': + resolution: {integrity: sha512-fr8yGzkocCcTYoE+NwC+/WGPNteQ2GmOPJTy/z9NcsLchnpSIZ1fzFll15UYPKQlwQdNRJXxKK3npVBtLRdHTg==} cpu: [x64] os: [linux] libc: [musl] - '@tsc-ts/win32-x64-msvc@0.0.3': - resolution: {integrity: sha512-oGtboSHgpuQP1dHAHqWyGjAWCpvHDpciYCi/2ywcp6MT+oOHxC99X48H/PZr29fzYVxCVeaEqWZ/VFU9qLZaKg==} + '@tsc-ts/win32-x64-msvc@0.0.5': + resolution: {integrity: sha512-SQwufJafzsiOBYncHOxYkyELWjLlBijcKB+xOs6hr6Lxxp5MMLoPtvDFNG88c7G5xv2TDb1XbvyxnjQltr6SHw==} cpu: [x64] os: [win32] @@ -2566,8 +2570,8 @@ packages: ts-toolbelt@6.15.5: resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==} - tsc-ts@0.0.3: - resolution: {integrity: sha512-Qr0i/BxP3ULIPDfr38vRb02x7sj6Q0r70Gubgu4ty2J4CEDCUKzT8RT2KMNqLXI3ZAhl7cncJq7a4LjKYZ1+0g==} + tsc-ts@0.0.5: + resolution: {integrity: sha512-Eea+zNv7y696pqNZtXBd0AX7/6PXIhR7aaEb0EhgpHadbk7VEiURD94/NngeLpYWts2tPjetkpwFLJY0+6yOnQ==} engines: {node: '>=20'} hasBin: true @@ -3428,6 +3432,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true + '@scriptc/threads@0.3.6': {} + '@sinclair/typebox@0.25.24': {} '@standard-schema/spec@1.1.0': {} @@ -3441,25 +3447,25 @@ snapshots: mkdirp: 1.0.4 path-browserify: 1.0.1 - '@tsc-ts/darwin-arm64@0.0.3': + '@tsc-ts/darwin-arm64@0.0.5': optional: true - '@tsc-ts/darwin-x64@0.0.3': + '@tsc-ts/darwin-x64@0.0.5': optional: true - '@tsc-ts/linux-arm64-gnu@0.0.3': + '@tsc-ts/linux-arm64-gnu@0.0.5': optional: true - '@tsc-ts/linux-arm64-musl@0.0.3': + '@tsc-ts/linux-arm64-musl@0.0.5': optional: true - '@tsc-ts/linux-x64-gnu@0.0.3': + '@tsc-ts/linux-x64-gnu@0.0.5': optional: true - '@tsc-ts/linux-x64-musl@0.0.3': + '@tsc-ts/linux-x64-musl@0.0.5': optional: true - '@tsc-ts/win32-x64-msvc@0.0.3': + '@tsc-ts/win32-x64-msvc@0.0.5': optional: true '@tybys/wasm-util@0.10.3': @@ -4798,15 +4804,17 @@ snapshots: ts-toolbelt@6.15.5: {} - tsc-ts@0.0.3: + tsc-ts@0.0.5: + dependencies: + '@scriptc/threads': 0.3.6 optionalDependencies: - '@tsc-ts/darwin-arm64': 0.0.3 - '@tsc-ts/darwin-x64': 0.0.3 - '@tsc-ts/linux-arm64-gnu': 0.0.3 - '@tsc-ts/linux-arm64-musl': 0.0.3 - '@tsc-ts/linux-x64-gnu': 0.0.3 - '@tsc-ts/linux-x64-musl': 0.0.3 - '@tsc-ts/win32-x64-msvc': 0.0.3 + '@tsc-ts/darwin-arm64': 0.0.5 + '@tsc-ts/darwin-x64': 0.0.5 + '@tsc-ts/linux-arm64-gnu': 0.0.5 + '@tsc-ts/linux-arm64-musl': 0.0.5 + '@tsc-ts/linux-x64-gnu': 0.0.5 + '@tsc-ts/linux-x64-musl': 0.0.5 + '@tsc-ts/win32-x64-msvc': 0.0.5 tslib@2.8.1: optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d4d370721..a3e992b0a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,14 +7,14 @@ minimumReleaseAgeExclude: - "effect@4.0.0" - "@effect/platform-node@4.0.0" - "@effect/platform-node-shared@4.0.0" - - "@tsc-ts/darwin-arm64@0.0.3" - - "@tsc-ts/darwin-x64@0.0.3" - - "@tsc-ts/linux-arm64-gnu@0.0.3" - - "@tsc-ts/linux-arm64-musl@0.0.3" - - "@tsc-ts/linux-x64-gnu@0.0.3" - - "@tsc-ts/linux-x64-musl@0.0.3" - - "@tsc-ts/win32-x64-msvc@0.0.3" - - "tsc-ts@0.0.3" + - "@tsc-ts/darwin-arm64@0.0.5" + - "@tsc-ts/darwin-x64@0.0.5" + - "@tsc-ts/linux-arm64-gnu@0.0.5" + - "@tsc-ts/linux-arm64-musl@0.0.5" + - "@tsc-ts/linux-x64-gnu@0.0.5" + - "@tsc-ts/linux-x64-musl@0.0.5" + - "@tsc-ts/win32-x64-msvc@0.0.5" + - "tsc-ts@0.0.5" allowBuilds: esbuild: true overrides: diff --git a/scripts/ci-lanes.mjs b/scripts/ci-lanes.mjs index 9edc48a1c..b020f9de0 100644 --- a/scripts/ci-lanes.mjs +++ b/scripts/ci-lanes.mjs @@ -7,18 +7,20 @@ // own exemptions; a change to this script runs every lane until it is on the base branch. It uses only Node and git. // // Lanes (each an output, true or false): -// - linux: the Linux build and test shards, the TypeScript 7 parity sweep and the smaller Linux, Windows and mobile +// - linux: the Linux build and test shards, the TypeScript 7 parity sweep and the smaller Linux, Windows and Android // jobs. Off only when every change is documentation. -// - macos: the macOS native build, the darwin test shards and the macOS LLVM artifact jobs. On when the change touches -// a darwin package, a path naming darwin, macOS, iOS or Mach-O, the darwin test files of scripts/ci-test-plan.mjs, -// the LLVM helper (native/, packages/llvm-*) or the shared runtime pack code, or changed lines in packages/{runtime,compiler,cli} that mention darwin-specific APIs (__APPLE__, kqueue, Mach-O -// and similar). +// - macos: the macOS native build, the darwin test shards, the macOS LLVM artifact jobs and the iOS runtime packs. On +// when the change touches a darwin package, a path naming darwin, macOS, iOS, Mach-O or kqueue, the runtime's +// platform header, the darwin suite's test files (scripts/ci-test-plan.mjs), the LLVM helper (native/, +// packages/llvm-*) or the shared runtime pack code, or changed lines in packages/{runtime,compiler,cli} that mention +// darwin-specific APIs (__APPLE__, kqueue, Mach-O and similar). // - effects: the Effect suites. On when the change touches their harness or fixtures. // - bootstrap: the native bootstrap seed and self-rebuild contracts. On when the change touches the bootstrap scripts, // the native CLI build, the cli-* packages or the self-hosting tests. // Every lane is on when the change touches CI configuration, the workspace manifests or lockfile, vitest -// configuration, the shared test harness modules or this script, or when the diff cannot be computed. Pushes to main -// and the nightly sweep do not use this script: they run every lane. +// configuration, the shared test harness modules or this script, or when the diff cannot be computed. Pushes to main, +// the nightly sweep and manual runs do not use this script: pushes to main run every lane except macos, and the others +// run every lane. import { execFileSync } from "node:child_process"; import { appendFileSync } from "node:fs"; @@ -38,9 +40,11 @@ const everything = [ /^packages\/[^/]+\/package\.json$/, ]; const documentation = [/^docs\//, /\.mdx?$/, /^LICENSE$/, /^CHANGELOG\.md$/]; -const darwinPath = /darwin|macos|ios|apple|mach-?o/i; +const darwinPath = /darwin|macos|ios|apple|mach-?o|kqueue/i; // Native code generation and runtime packs every platform shares. const sharedNative = /^native\/|^packages\/(llvm-[^/]+|runtime-pack-common)\//; +// The darwin suite's files (darwinCorpusFiles and darwinFiles in scripts/ci-test-plan.mjs), the LLVM differential +// the macOS artifact jobs run, and the runtime's platform header. const darwinFiles = [ "packages/cli/test/bootstrap.test.ts", "packages/cli/test/native-link-info.test.ts", @@ -49,7 +53,10 @@ const darwinFiles = [ "packages/compiler/test/debug-variables.test.ts", "packages/compiler/test/native-codegen-integration.test.ts", "packages/compiler/test/source-debugging.test.ts", + "packages/runtime-darwin-arm64/test/archive.test.ts", + "packages/runtime/src/scr_platform.h", "packages/runtime/test/string.test.ts", + "tests/harness/differential.test.ts", "tests/harness/ffi-callback-pool.test.ts", "tests/harness/ffi-catalog.test.ts", "tests/harness/ffi-frameworks.test.ts", diff --git a/scripts/ci-test-plan.mjs b/scripts/ci-test-plan.mjs index 55eb65e72..2b76b52d6 100644 --- a/scripts/ci-test-plan.mjs +++ b/scripts/ci-test-plan.mjs @@ -69,13 +69,14 @@ export function sideOwners(flavor, count) { return owners; } -// Pull requests and pushes to main run the full suite on Linux. Their macOS -// lane runs the darwin suite, which keeps the darwin clang/libc/kqueue -// oracles: the Node differential corpus, plus every file whose assertions run -// only on darwin or take darwin-specific branches (including the Mach-O -// library localization arm and the arm64 runtime string oracle). -// The nightly schedule and manual runs with macos_full run the full suite on -// macOS. +// Pull requests and pushes to main run the full suite on Linux. Pull requests +// that change darwin code (scripts/ci-lanes.mjs) also run the darwin suite on +// macOS, which keeps the darwin clang/libc/kqueue oracles: the Node +// differential corpus, plus every file whose assertions run only on darwin or +// take darwin-specific branches (including the Mach-O library localization arm +// and the arm64 runtime string oracle). Pushes to main run no macOS jobs. The +// nightly schedule and manual runs run the full suite on macOS, or the darwin +// suite with macos_suite set to darwin. export const darwinCorpusFiles = ["tests/harness/differential.test.ts"]; export const darwinFiles = [ "packages/cli/test/bootstrap.test.ts", diff --git a/scripts/generate-ts7-ast-schema.mjs b/scripts/generate-ts7-ast-schema.mjs index 45a01c213..30c506869 100644 --- a/scripts/generate-ts7-ast-schema.mjs +++ b/scripts/generate-ts7-ast-schema.mjs @@ -268,9 +268,10 @@ if (process.argv.includes("--check")) { // declarations, and require every wire property to have a declared getter. const declarations = readFileSync(join(packageRoot, "dist/api/node/node.generated.d.ts"), "utf8"); const names = [...new Set(Object.values(protocol.childProperties).flat())].sort(); -// These views adapt TS7's shared postfix slot and reparsed JSDoc bodies. -// Their implementations live outside the generated region in AstNode. -const adaptedGetters = new Set(["body", "questionToken"]); +// These views adapt TS7's shared postfix slot and reparsed JSDoc bodies, and +// return modifiers as a modifier list. Their implementations live outside the +// generated region in AstNode. +const adaptedGetters = new Set(["body", "modifiers", "questionToken"]); const getters = names .filter((name) => !adaptedGetters.has(name)) .map((name) => { diff --git a/scripts/generate-ts7-ast-views.mjs b/scripts/generate-ts7-ast-views.mjs index 1525f6246..91e7ac2f7 100644 --- a/scripts/generate-ts7-ast-views.mjs +++ b/scripts/generate-ts7-ast-views.mjs @@ -65,6 +65,8 @@ export function generateAstViews(root, packageRoot, version, check) { if (key === "jsdocPropertyTags") type = "JSDocPropertyTag"; // A list's children are a node list, as every other list field. if (name === "SyntaxList" && key === "children") type = "NodeArray"; + // A declaration's modifiers are a modifier list, with their flags. + if (key === "modifiers") type = type.replace("NodeArray<", "ModifierArray<"); if (member.questionToken) type += " | undefined"; fields.set(key, ` readonly ${key}: ${type};`); } @@ -74,7 +76,7 @@ export function generateAstViews(root, packageRoot, version, check) { ...header, "", 'import type { AstNode } from "./ast-node.js";', - 'import type { JsxTagNamePropertyAccess, NodeArray } from "./ast-types.js";', + 'import type { JsxTagNamePropertyAccess, ModifierArray, NodeArray } from "./ast-types.js";', 'import { SyntaxKind, NodeFlags, ModifierFlags, TokenFlags } from "./enums.js";', "type Node = AstNode;", ]; @@ -99,6 +101,9 @@ export function generateAstViews(root, packageRoot, version, check) { ? `<${node.typeParameters.map((param) => param.getText(generated)).join(", ")}>` : ""; const fields = members(node.name.text); + // A default clause has no expression; the declaration shares the case clause's. + if (node.name.text === "DefaultClause" && fields.has("expression")) + fields.set("expression", " readonly expression: undefined;"); if (!fields.has("kind")) { const kinds = new Set(); for (const [name, child] of interfaces) { @@ -131,6 +136,7 @@ export function generateAstViews(root, packageRoot, version, check) { ...header, "", 'import { SyntaxKind, NodeFlags, ScriptKind, OuterExpressionKinds } from "./enums.js";', + 'import { getSourceFileOfNode } from "./node-functions.js";', ]; for (const stem of ["is.generated", "is"]) { const declarations = parse(`${stem}.d.ts`); @@ -173,10 +179,9 @@ export function generateAstViews(root, packageRoot, version, check) { } let body = fn.body.getText(source); if (name === "isJSDocTypeAssertion") - body = body.replace( - "expression.kind !==", - "expression === undefined || expression.kind !==", - ); + body = body + .replace("expression.kind !==", "expression === undefined || expression.kind !==") + .replace("node.getSourceFile()", "getSourceFileOfNode(node)"); if (head.includes("node: Node") && name.startsWith("is")) { head = head.replace("node: Node", "node: Node | undefined"); body = body.replace("{", "{\n if (node === undefined) return false;"); diff --git a/tests/corpus/5819-node-stream-dyn-direct-pipe/main.ts b/tests/corpus/5819-node-stream-dyn-direct-pipe/main.ts new file mode 100644 index 000000000..a72328d3f --- /dev/null +++ b/tests/corpus/5819-node-stream-dyn-direct-pipe/main.ts @@ -0,0 +1,59 @@ +// A native Readable piped straight into a JavaScript sink, the only +// untyped stream flow in the program; unpipe of a NodeJS.WritableStream +// that holds a native Writable or a JavaScript sink; unpipe() of every +// JavaScript sink; and JavaScript destinations whose prependListener or +// writableNeedDrain getter throws. +import { Readable, Writable } from "node:stream"; +import { plainSink, throwingSink } from "./sources.js"; + +function detach(src: Readable, dst: NodeJS.WritableStream): void { + src.unpipe(dst); +} + +async function main(): Promise { + Readable.from(["a", "b"]).pipe(plainSink("direct")); + await new Promise((resolve) => setTimeout(resolve, 10)); + + const parts: string[] = []; + const native = new Writable({ + write(chunk: Buffer, _encoding, done) { + parts.push(chunk.toString()); + done(); + }, + }); + native.on("unpipe", () => console.log("native heard unpipe")); + const source = new Readable({ read() {} }); + source.pipe(native); + detach(source, native); + source.push("dropped"); + await new Promise((resolve) => setTimeout(resolve, 10)); + console.log("native got", JSON.stringify(parts.join("")), source.isPaused()); + + const sink = plainSink("detached"); + const second = new Readable({ read() {} }); + second.pipe(sink); + detach(second, sink); + detach(second, plainSink("never piped")); + second.push("late"); + await new Promise((resolve) => setTimeout(resolve, 10)); + console.log("second paused", second.isPaused()); + + const third = new Readable({ read() {} }); + third.pipe(plainSink("first of two")); + third.pipe(plainSink("second of two")); + third.unpipe(); + third.push("after unpipe()"); + await new Promise((resolve) => setTimeout(resolve, 10)); + console.log("third paused", third.isPaused(), third.listenerCount("data")); + + for (const member of ["prependListener", "writableNeedDrain"]) { + try { + new Readable({ read() {} }).pipe(throwingSink(member)); + console.log(member, "piped"); + } catch (error) { + console.log("pipe threw", (error as Error).message); + } + } +} + +void main(); diff --git a/tests/corpus/5819-node-stream-dyn-direct-pipe/sources.js b/tests/corpus/5819-node-stream-dyn-direct-pipe/sources.js new file mode 100644 index 000000000..a06d04899 --- /dev/null +++ b/tests/corpus/5819-node-stream-dyn-direct-pipe/sources.js @@ -0,0 +1,45 @@ +// A plain JavaScript sink, and sinks whose prependListener or +// writableNeedDrain getter throws. + +/** @returns {any} */ +export function plainSink(label) { + const parts = []; + const listeners = {}; + const sink = { + write(chunk) { + parts.push(String(chunk)); + return true; + }, + end() { + console.log(label, "sink end", parts.join("")); + }, + on(event, listener) { + (listeners[event] ??= []).push(listener); + return sink; + }, + once(event, listener) { + return sink.on(event, listener); + }, + removeListener(event, listener) { + listeners[event] = (listeners[event] ?? []).filter((each) => each !== listener); + return sink; + }, + emit(event, ...args) { + if (event === "unpipe") console.log(label, "heard unpipe"); + for (const listener of listeners[event] ?? []) listener(...args); + return true; + }, + }; + return sink; +} + +/** @returns {any} */ +export function throwingSink(member) { + const sink = plainSink(member); + Object.defineProperty(sink, member, { + get() { + throw new Error(`${member} getter failed`); + }, + }); + return sink; +} diff --git a/tests/corpus/5820-node-stream-dyn-constructor-overrides/main.ts b/tests/corpus/5820-node-stream-dyn-constructor-overrides/main.ts new file mode 100644 index 000000000..223e10ee8 --- /dev/null +++ b/tests/corpus/5820-node-stream-dyn-constructor-overrides/main.ts @@ -0,0 +1,35 @@ +// Constructor-function streams in NodeJS.ReadableStream / WritableStream +// slots: member calls run the stream's own pause, resume and on, and a pipe +// into one, and an unpipe of it, are the runtime's. +import { Readable } from "node:stream"; +import { collector, recordingSource } from "./sources.js"; + +function control(stream: NodeJS.ReadableStream): void { + stream.pause(); + stream.resume(); + stream.on("end", () => {}); +} + +function attach(src: Readable, dst: NodeJS.WritableStream): void { + src.pipe(dst); +} + +function detach(src: Readable, dst: NodeJS.WritableStream): void { + src.unpipe(dst); +} + +const recording = recordingSource(); +control(recording); +console.log("recorded", recording.calls.join(",")); + +const sink = collector(); +sink.on("pipe", () => console.log("pipe event")); +sink.on("unpipe", () => console.log("unpipe event")); +const source = new Readable({ read() {} }); +attach(source, sink); +source.push("a"); +setImmediate(() => { + detach(source, sink); + source.push("b"); + setImmediate(() => console.log("collected", sink.chunks.join(","), source.isPaused())); +}); diff --git a/tests/corpus/5820-node-stream-dyn-constructor-overrides/sources.js b/tests/corpus/5820-node-stream-dyn-constructor-overrides/sources.js new file mode 100644 index 000000000..462cd0538 --- /dev/null +++ b/tests/corpus/5820-node-stream-dyn-constructor-overrides/sources.js @@ -0,0 +1,39 @@ +// Constructor-function streams that override stream methods, and a plain +// JavaScript sink. +import { Readable, Writable } from "node:stream"; +import util from "node:util"; + +/** A Readable whose pause, resume and on record their calls before + * running Readable's own. */ +function Recording() { + Readable.call(this, { read() {} }); + this.calls = []; +} +util.inherits(Recording, Readable); +for (const name of ["pause", "resume", "on"]) { + Recording.prototype[name] = function (...args) { + this.calls.push(name); + return Readable.prototype[name].apply(this, args); + }; +} + +/** @returns {any} */ +export function recordingSource() { + return new Recording(); +} + +/** A Writable without overrides, piped into natively. */ +function Collector() { + Writable.call(this); + this.chunks = []; +} +util.inherits(Collector, Writable); +Collector.prototype._write = function (chunk, _encoding, done) { + this.chunks.push(String(chunk)); + done(); +}; + +/** @returns {any} */ +export function collector() { + return new Collector(); +} diff --git a/tests/corpus/6419-untyped-async-package/main.ts b/tests/corpus/6419-untyped-async-package/main.ts new file mode 100644 index 000000000..9ec10f7c5 --- /dev/null +++ b/tests/corpus/6419-untyped-async-package/main.ts @@ -0,0 +1,38 @@ +// Untyped async methods of a package, reached through the package's own +// untyped callers and prototype reflection, and overridden by a typed +// subclass. An async generator method called from the package answers its +// generator object. +import { Pool, Schema, limiter, listIssues, prototypeNames, validateAll } from "token-pool"; + +class CountingPool extends Pool { + acquired = 0; + async acquire(): Promise { + await super.acquire(); + this.acquired++; + } +} + +async function main(): Promise { + const pool = new CountingPool(1); + await pool.acquire(); + const waiting = pool.acquire(); + console.log("waiting", pool.nrWaiting()); + pool.release("t"); + await waiting; + console.log("acquired", pool.acquired, pool.nrWaiting()); + const base = new Pool(1); + console.log("token", await base.acquire()); + const run = limiter(1); + console.log(await Promise.all([run(async () => 1), run(async () => 2)])); + const schema = new Schema((value) => typeof value === "number"); + console.log(await validateAll(schema, [1, "x", 3])); + console.log(await listIssues(schema, ["p", 5, "q"])); + console.log(await schema.parseAsync(4)); + try { + await schema.parseAsync("y"); + } catch (e) { + console.log("rejected", e instanceof Error ? e.message : String(e)); + } + console.log(prototypeNames()); +} +main(); diff --git a/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.d.ts b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.d.ts new file mode 100644 index 000000000..ed0c82579 --- /dev/null +++ b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.d.ts @@ -0,0 +1,17 @@ +export declare class Pool { + constructor(size: number); + tryAcquire(): any; + acquire(): Promise; + release(token?: any): void; + nrWaiting(): number; +} +export declare function limiter(size: number): (work: () => Promise) => Promise; +export declare class Schema { + constructor(check: (value: unknown) => boolean); + safeParseAsync(data: unknown): Promise; + parseAsync(data: unknown): Promise; + issues(values: unknown[]): AsyncGenerator; +} +export declare function validateAll(schema: Schema, values: unknown[]): Promise; +export declare function listIssues(schema: Schema, values: unknown[]): Promise; +export declare function prototypeNames(): string; diff --git a/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.js b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.js new file mode 100644 index 000000000..04935ff29 --- /dev/null +++ b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.js @@ -0,0 +1,123 @@ +"use strict"; +// An untyped token pool and schema in the shape of a semaphore and a +// validation library: async methods whose results come from storage that +// passes through untyped helpers. +Object.defineProperty(exports, "__esModule", { value: true }); +function moveItems(src, srcIndex, dst, dstIndex, len) { + for (let j = 0; j < len; ++j) { + dst[j + dstIndex] = src[j + srcIndex]; + src[j + srcIndex] = void 0; + } +} +class Queue { + constructor() { + this._length = 0; + this.arr = []; + } + push(item) { + if (this._length > 64) moveItems(this.arr, 1, this.arr, 0, this._length - 1); + this.arr[this._length] = item; + this._length = this._length + 1; + } + pop() { + const length = this._length; + if (length === 0) return void 0; + const ret = this.arr[length - 1]; + this.arr[length - 1] = void 0; + this._length = length - 1; + return ret; + } + shift() { + const length = this._length; + if (length === 0) return void 0; + const ret = this.arr[0]; + for (let i = 1; i < length; i++) this.arr[i - 1] = this.arr[i]; + this.arr[length - 1] = void 0; + this._length = length - 1; + return ret; + } + get length() { + return this._length; + } +} +class Pool { + constructor(size) { + this.free = new Queue(); + this.waiting = new Queue(); + for (let i = 0; i < size; i++) this.free.push("token" + i); + } + tryAcquire() { + return this.free.pop(); + } + async acquire() { + const token = this.tryAcquire(); + if (token !== void 0) return token; + return new Promise((resolve) => { + this.waiting.push(resolve); + }); + } + release(token) { + const next = this.waiting.shift(); + if (next) next(token); + else this.free.push(token); + } + nrWaiting() { + return this.waiting.length; + } +} +exports.Pool = Pool; +function limiter(size) { + const pool = new Pool(size); + return async function limited(work) { + const token = await pool.acquire(); + try { + return await work(); + } finally { + pool.release(token); + } + }; +} +exports.limiter = limiter; +class Schema { + constructor(check) { + this.check = check; + this.results = new Queue(); + } + async safeParseAsync(data) { + await null; + this.results.push(this.check(data) ? { success: true, data } : { success: false, issue: "invalid " + data }); + return this.results.pop(); + } + async *issues(values) { + for (const value of values) { + const result = await this.safeParseAsync(value); + if (!result.success) yield result.issue; + } + } + async parseAsync(data) { + const result = await this.safeParseAsync(data); + if (result.success) return result.data; + throw new Error(result.issue); + } +} +exports.Schema = Schema; +async function validateAll(schema, values) { + const out = []; + for (const value of values) { + const result = await schema.safeParseAsync(value); + out.push(result.success ? "ok " + result.data : result.issue); + } + return out.join(", "); +} +exports.validateAll = validateAll; +async function listIssues(schema, values) { + const out = []; + const it = schema.issues(values); + for (let step = await it.next(); !step.done; step = await it.next()) out.push(step.value); + return out.join(", "); +} +exports.listIssues = listIssues; +function prototypeNames() { + return typeof Schema.prototype.parseAsync + " " + typeof Schema.prototype.safeParseAsync; +} +exports.prototypeNames = prototypeNames; diff --git a/tests/corpus/6419-untyped-async-package/node_modules/token-pool/package.json b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/package.json new file mode 100644 index 000000000..994de07b2 --- /dev/null +++ b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/package.json @@ -0,0 +1 @@ +{ "name": "token-pool", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6470-any-destructuring.ts b/tests/corpus/6470-any-destructuring.ts new file mode 100644 index 000000000..09d7f45c4 --- /dev/null +++ b/tests/corpus/6470-any-destructuring.ts @@ -0,0 +1,124 @@ +// Destructuring `any` values in TypeScript: a function typed to return +// `any` (an untyped command runner), an awaited `any` promise, JSON.parse, +// and an `any` table read by key all destructure through the +// checked-dynamic value, with renames, defaults, nesting, rest, holes and +// Node's TypeError for a null source, including empty patterns, awaited +// sources, element reads, `this` receivers and string, numeric and +// computed first keys. +function run(command: string): any { + return { stdout: ` ${command} output \n`, stderr: "", exitCode: 0, meta: { pid: 42 } }; +} + +const { stdout, stderr } = run("echo"); +console.log(JSON.stringify(stdout.toString().trim()), stderr.length); + +const { exitCode: code, missing, meta: { pid }, ...rest } = run("ls"); +console.log(code, missing, pid, Object.keys(rest).join(",")); + +const { signal = "none", exitCode = 9 } = run("true"); +console.log(signal, exitCode); + +const [first, , third = "default", ...others] = JSON.parse('["a", "b", null, "d", "e"]'); +console.log(first, third, others.length); + +const { list: [head, ...tail] } = JSON.parse('{"list":[1,2,3]}'); +console.log(head, tail); + +async function fetchAny(): Promise { + return { body: "payload", status: 200 }; +} + +async function main(): Promise { + const { body, status } = await fetchAny(); + console.log(body, status); + const pending = fetchAny(); + const { status: again } = await pending; + console.log(again + 1); +} + +const table: any = { a: { mode: 0o755, path: "/bin" }, b: { mode: 0o644, path: "/etc" } }; +for (const key in table) { + const { mode, path } = table[key]; + console.log(key, mode.toString(8), path); +} + +function lookup(found: boolean): any { + return found ? { anything: 1 } : null; +} +for (const found of [true, false]) { + try { + const { anything } = lookup(found); + console.log(anything); + } catch (e) { + console.log(e instanceof TypeError, (e as Error).message); + } +} + +async function nothing(): Promise { + return null; +} + +function nothingSync(): any { + return undefined; +} + +class Holder { + value(): any { + return null; + } + read(): void { + const { a } = this.value(); + console.log(a); + } +} + +const sparse: any = { k: null, list: [null] }; +const key = "k"; + +async function nullSources(): Promise { + const attempts: Array<() => Promise> = [ + async () => { + const {} = await nothing(); + }, + async () => { + const { a } = await nothing(); + console.log(a); + }, + async () => { + const {} = nothingSync(); + }, + async () => { + const {} = (await nothing()) as any; + }, + async () => { + const { "a-b": x } = nothingSync()!; + console.log(x); + }, + async () => { + const { 1.5: x } = sparse[key]; + console.log(x); + }, + async () => { + const { [key]: x } = sparse.list[0]; + console.log(x); + }, + async () => { + new Holder().read(); + }, + async () => { + let a: any; + ({ a } = await nothing()); + console.log(a); + }, + ]; + for (const attempt of attempts) { + try { + await attempt(); + console.log("no error"); + } catch (e) { + console.log(e instanceof TypeError, (e as Error).message); + } + } +} + +main().then(nullSources); diff --git a/tests/corpus/6471-any-method-chains.ts b/tests/corpus/6471-any-method-chains.ts new file mode 100644 index 000000000..3bfce5fba --- /dev/null +++ b/tests/corpus/6471-any-method-chains.ts @@ -0,0 +1,24 @@ +// Method chains over `any`-derived strings: each step that answers a fresh +// native value (split's string[]) keeps chaining through the +// checked-dynamic value — map, filter(Boolean), pop, join — and the result +// checks back into typed slots. +function exec(): any { + return { stdout: " 3f2a\n 9c1b \n\n77de\n", stderr: "warn: shallow\n" }; +} + +const { stdout, stderr } = exec(); +let ids: string[] = []; +ids = stdout + .split("\n") + .map((s: string) => s.trim()) + .filter(Boolean); +console.log(ids.length, ids.join(", ")); + +const last: string = stdout.trim().split("\n").pop() ?? ""; +console.log(JSON.stringify(last)); + +const firstWord: string = stderr.trim().split(":")[0]; +console.log(firstWord, stderr.trim().split(" ").length); + +const words: string[] = stdout.trim().split(/\s+/); +console.log(words.length, stdout.split("\n").length, stdout.trim().split("\n").join("|")); diff --git a/tests/corpus/6471-stream-data-buffer-or-string.ts b/tests/corpus/6471-stream-data-buffer-or-string.ts index 69289ced5..54c63945b 100644 --- a/tests/corpus/6471-stream-data-buffer-or-string.ts +++ b/tests/corpus/6471-stream-data-buffer-or-string.ts @@ -32,9 +32,12 @@ fs.writeFileSync(file, "file contents\n"); const stream = fs.createReadStream(file); const release = (): void => console.log("released"); stream.on("data", (chunk: Buffer | string) => describe("file", chunk)); -stream.on("close", release); stream.on("error", release); -stream.on("end", () => { +// The second read starts once the first stream has closed its file, so the +// order of its 'close' and the second stream's data does not depend on +// which file operation finishes first. +stream.on("close", () => { + release(); const encoded = fs.createReadStream(file, { encoding: "utf8" }); encoded.on("data", (chunk: Buffer | string) => describe("file utf8", chunk)); encoded.on("close", () => { diff --git a/tests/corpus/6472-namespace-destructuring/lib.ts b/tests/corpus/6472-namespace-destructuring/lib.ts new file mode 100644 index 000000000..d25d53d74 --- /dev/null +++ b/tests/corpus/6472-namespace-destructuring/lib.ts @@ -0,0 +1,21 @@ +export function add(a: number, b: number): number { + return a + b; +} + +export const label = "lib"; + +export let counter = 1; + +export function bump(): void { + counter++; +} + +export class Shape { + side: number; + constructor(side: number) { + this.side = side; + } + area(): number { + return this.side * this.side; + } +} diff --git a/tests/corpus/6472-namespace-destructuring/main.ts b/tests/corpus/6472-namespace-destructuring/main.ts new file mode 100644 index 000000000..7aa00a2d1 --- /dev/null +++ b/tests/corpus/6472-namespace-destructuring/main.ts @@ -0,0 +1,29 @@ +// Destructuring a program module's namespace, statically +// (`import * as ns`) and from a dynamic import: each element reads the +// export at the pattern's position — renames and defaults included — and +// binds a copy of its value (a later reassignment of an exported `let` is +// visible through the namespace, not through the binding). An annotation +// naming another module or an object type does not change which module's +// exports are read. +import * as ns from "./lib.ts"; +import type * as shape from "./shape.ts"; + +const { add, label, counter, bump, Shape: Ctor } = ns; +console.log(add(2, 3), label, counter); +bump(); +console.log(counter, ns.counter); +console.log(new Ctor(4).area()); + +async function main(): Promise { + const { add: plus, label: name = "unused" } = await import("./lib.ts"); + console.log(plus(10, 20), name); + const { add: annotatedAdd, label: annotated }: typeof shape = await import("./lib.ts"); + console.log(annotatedAdd(10, 20), annotated); +} + +const { add: shapeAdd, label: shapeLabel }: typeof shape = ns; +console.log(shapeAdd(2, 3), shapeLabel); +const { label: viaObject }: { label: string } = ns; +console.log(viaObject); + +main(); diff --git a/tests/corpus/6472-namespace-destructuring/shape.ts b/tests/corpus/6472-namespace-destructuring/shape.ts new file mode 100644 index 000000000..96adaffd3 --- /dev/null +++ b/tests/corpus/6472-namespace-destructuring/shape.ts @@ -0,0 +1,7 @@ +// A module whose exports have the same names and types as some of +// lib.ts's, so `typeof import("./shape.ts")` accepts lib.ts's namespace. +export const label: string = "shape"; + +export function add(a: number, b: number): number { + return a * b; +} diff --git a/tests/corpus/6473-any-parameter-values.ts b/tests/corpus/6473-any-parameter-values.ts new file mode 100644 index 000000000..399332551 --- /dev/null +++ b/tests/corpus/6473-any-parameter-values.ts @@ -0,0 +1,23 @@ +// Function values whose defaulted parameter the checker types `any`: under +// an `(...args: any[]) => any` wrapper slot the contextual `any` wins over +// the default's type. The callable's slot is the checked-dynamic value the +// body already reads. +function traced any>(name: string, fn: F): F { + console.log("wrap", name); + return fn; +} + +function configured(raw = process.env["SCRIPTC_TEST_UNSET_WIDTH"]): number | undefined { + const width = Number.parseInt(raw ?? "", 10); + return Number.isSafeInteger(width) && width > 0 ? width : undefined; +} + +const layout = traced("layout", (width = configured() ?? 80) => { + return { width, gutter: 2 }; +}); +console.log(JSON.stringify(layout()), JSON.stringify(layout(120))); + +const scaled = traced("scaled", function (factor = 2, base = 10) { + return base * factor; +}); +console.log(scaled(), scaled(3), scaled(undefined, 1)); diff --git a/tests/corpus/6474-async-any-callbacks/main.ts b/tests/corpus/6474-async-any-callbacks/main.ts new file mode 100644 index 000000000..c84ce37ba --- /dev/null +++ b/tests/corpus/6474-async-any-callbacks/main.ts @@ -0,0 +1,41 @@ +// Async callbacks whose result the checker types `Promise` handed to +// a JavaScript library typed `(...) => A | Promise` (A = any collapses +// the slot to `any`): the callback value still returns a promise, and its +// payload is the checked-dynamic value the body produced. +import retry from "retry-lite"; + +function loose(attempt: number): any { + if (attempt < 3) throw new Error(`attempt ${attempt} failed`); + return { text: `ok after ${attempt}`, size: attempt * 10 }; +} + +async function main(): Promise { + const got = await retry(async (_bail, attempt) => loose(attempt), { + retries: 5, + onRetry: (e: Error) => console.log("retry:", e.message), + }); + console.log(got.text, got.size); + + const parsed = await retry( + async () => { + const value = JSON.parse('{"items":[1,2,3]}'); + return value; + }, + { retries: 1 }, + ); + console.log(parsed.items.length); + + try { + await retry( + async (bail) => { + bail(new Error("gave up")); + throw new Error("ignored"); + }, + { retries: 2 }, + ); + } catch (e) { + console.log("caught:", (e as Error).message); + } +} + +main(); diff --git a/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/index.d.ts b/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/index.d.ts new file mode 100644 index 000000000..896f81f9d --- /dev/null +++ b/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/index.d.ts @@ -0,0 +1,15 @@ +declare function retryLite( + fn: retryLite.RetryFunction, + opts: retryLite.Options +): Promise; + +declare namespace retryLite { + interface Options { + retries?: number; + onRetry?: (e: Error) => any; + } + + type RetryFunction = (bail: (e: Error) => void, attempt: number) => A | Promise; +} + +export = retryLite; diff --git a/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/index.js b/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/index.js new file mode 100644 index 000000000..a1ae718ab --- /dev/null +++ b/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/index.js @@ -0,0 +1,36 @@ +"use strict"; + +function retryLite(fn, opts) { + const retries = opts.retries === undefined ? 3 : opts.retries; + return new Promise((resolve, reject) => { + let attempt = 0; + let bailed = false; + const bail = (err) => { + bailed = true; + reject(err); + }; + const onError = (err) => { + if (bailed) return; + if (attempt > retries) { + reject(err); + return; + } + if (opts.onRetry) opts.onRetry(err); + runAttempt(); + }; + const runAttempt = () => { + attempt++; + let val; + try { + val = fn(bail, attempt); + } catch (err) { + onError(err); + return; + } + Promise.resolve(val).then(resolve).catch(onError); + }; + runAttempt(); + }); +} + +module.exports = retryLite; diff --git a/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/package.json b/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/package.json new file mode 100644 index 000000000..251988634 --- /dev/null +++ b/tests/corpus/6474-async-any-callbacks/node_modules/retry-lite/package.json @@ -0,0 +1,6 @@ +{ + "name": "retry-lite", + "version": "1.0.0", + "main": "./index.js", + "types": "./index.d.ts" +} diff --git a/tests/corpus/6474-async-any-callbacks/tsconfig.json b/tests/corpus/6474-async-any-callbacks/tsconfig.json new file mode 100644 index 000000000..9297bdb5b --- /dev/null +++ b/tests/corpus/6474-async-any-callbacks/tsconfig.json @@ -0,0 +1,3 @@ +{ + "compilerOptions": { "strict": true, "types": ["node"], "esModuleInterop": true } +} diff --git a/tests/corpus/6475-js-promise-resolve-adoption/main.ts b/tests/corpus/6475-js-promise-resolve-adoption/main.ts new file mode 100644 index 000000000..61a58dbe2 --- /dev/null +++ b/tests/corpus/6475-js-promise-resolve-adoption/main.ts @@ -0,0 +1,23 @@ +// JavaScript's Promise.resolve over a checked-dynamic value that holds a +// promise (a TypeScript async callback's result handed back into a +// JavaScript library) answers that same promise — adopted, not wrapped — +// and wraps plain values. +import { sameAfterResolve, viaExecutor, viaResolve, viaThen } from "calls"; + +async function main(): Promise { + console.log(await viaResolve(async (): Promise => "resolved")); + console.log(await viaThen(async (): Promise => 42)); + console.log(await viaExecutor(async (): Promise => "executor")); + console.log(await viaExecutor((): string => "plain")); + try { + await viaExecutor(async (): Promise => { + throw new Error("callback failed"); + }); + } catch (e) { + console.log("caught:", (e as Error).message); + } + const pending = Promise.resolve("p"); + console.log(sameAfterResolve(pending), sameAfterResolve("p")); +} + +main(); diff --git a/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/index.d.ts b/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/index.d.ts new file mode 100644 index 000000000..d1040a227 --- /dev/null +++ b/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/index.d.ts @@ -0,0 +1,4 @@ +export declare function viaResolve(fn: () => A | Promise): Promise; +export declare function viaThen(fn: () => A | Promise): Promise; +export declare function viaExecutor(fn: () => A | Promise): Promise; +export declare function sameAfterResolve(value: unknown): boolean; diff --git a/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/index.js b/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/index.js new file mode 100644 index 000000000..fb55d867c --- /dev/null +++ b/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/index.js @@ -0,0 +1,21 @@ +"use strict"; + +function viaResolve(fn) { + return Promise.resolve(fn()); +} + +function viaThen(fn) { + return Promise.resolve(fn()).then((value) => value); +} + +function viaExecutor(fn) { + return new Promise((resolve, reject) => { + Promise.resolve(fn()).then(resolve).catch(reject); + }); +} + +function sameAfterResolve(value) { + return Promise.resolve(value) === value; +} + +module.exports = { viaResolve, viaThen, viaExecutor, sameAfterResolve }; diff --git a/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/package.json b/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/package.json new file mode 100644 index 000000000..c85cd1f4b --- /dev/null +++ b/tests/corpus/6475-js-promise-resolve-adoption/node_modules/calls/package.json @@ -0,0 +1,6 @@ +{ + "name": "calls", + "version": "1.0.0", + "main": "./index.js", + "types": "./index.d.ts" +} diff --git a/tests/corpus/6480-node-stream-dyn-readable/main.ts b/tests/corpus/6480-node-stream-dyn-readable/main.ts new file mode 100644 index 000000000..21d7bf2b1 --- /dev/null +++ b/tests/corpus/6480-node-stream-dyn-readable/main.ts @@ -0,0 +1,72 @@ +// NodeJS.ReadableStream slots hold native streams and JavaScript stream +// objects alike: a plain object implementing on/pipe, a constructor-function +// stream, and a native Readable pass through a TypeScript function typed +// NodeJS.ReadableStream. TypeScript consumes them with on("data") and +// pipe() into a native Writable, JavaScript consumes the returned values +// (the same objects), and a native Readable typed NodeJS.ReadableStream +// pipes into JavaScript sinks through Node's pipe protocol. +import { Readable, Writable } from "node:stream"; +import { jsCollect, plainSink, plainSource, same, sink, source } from "./sources.js"; + +function pass(stream: NodeJS.ReadableStream): NodeJS.ReadableStream { + return stream; +} + +async function collect(label: string, stream: NodeJS.ReadableStream): Promise { + const parts: string[] = []; + await new Promise((resolve) => { + stream.on("data", (chunk: Buffer | string) => parts.push(chunk.toString())); + stream.on("end", () => resolve()); + }); + console.log(label, "on data", parts.join("")); +} + +async function pipeInto(label: string, stream: NodeJS.ReadableStream): Promise { + const parts: string[] = []; + await new Promise((resolve) => { + const native = new Writable({ + write(chunk: Buffer, _encoding, done) { + parts.push(chunk.toString()); + done(); + }, + }); + native.on("finish", () => resolve()); + stream.pipe(native); + }); + console.log(label, "pipe", parts.join("")); +} + +async function main(): Promise { + await collect("plain", pass(plainSource(["p1", "p2"]))); + await collect("constructor", pass(source(["s1", "s2"]))); + await collect("native", pass(Readable.from(["n1", "n2"]))); + + await pipeInto("plain", pass(plainSource(["p3", "p4"]))); + await pipeInto("constructor", pass(source(["s3", "s4"]))); + await pipeInto("native", pass(Readable.from(["n3", "n4"]))); + + console.log("plain js", await jsCollect(pass(plainSource(["p5", "p6"])))); + console.log("constructor js", await jsCollect(pass(source(["s5", "s6"])))); + console.log("native js", await jsCollect(pass(Readable.from(["n5", "n6"])))); + + const plain = plainSource(["p7"]); + const made = source(["s7"]); + const native = Readable.from(["n7"]); + console.log("identity", same(plain, pass(plain)), same(made, pass(made)), same(native, pass(native))); + + await new Promise((resolve) => { + pass(Readable.from(["n8", "n9"])).pipe(plainSink("native into plain")); + pass(plainSource(["p8"])).pipe(plainSink("plain into plain")); + setTimeout(resolve, 20); + }); + + await new Promise((resolve) => { + const target = sink("native into constructor"); + target.on("pipe", () => console.log("sink heard pipe")); + target.on("unpipe", () => console.log("sink heard unpipe")); + target.on("finish", () => setImmediate(resolve)); + pass(Readable.from(["n10", "n11"])).pipe(target); + }); +} + +void main(); diff --git a/tests/corpus/6480-node-stream-dyn-readable/sources.js b/tests/corpus/6480-node-stream-dyn-readable/sources.js new file mode 100644 index 000000000..3b520c29f --- /dev/null +++ b/tests/corpus/6480-node-stream-dyn-readable/sources.js @@ -0,0 +1,138 @@ +// JavaScript stream objects that are not native streams: a plain object +// implementing on/pipe, and a constructor function with its own listener +// table. Sinks: a plain object with write/end, and a constructor function +// that keeps listeners, so a native pipe() into it runs Node's protocol. + +/** @returns {any} */ +export function plainSource(chunks) { + const listeners = {}; + const source = { + on(event, listener) { + (listeners[event] ??= []).push(listener); + return source; + }, + emit(event, value) { + for (const listener of listeners[event] ?? []) listener(value); + }, + pipe(dest) { + source.on("data", (chunk) => dest.write(chunk)); + source.on("end", () => dest.end()); + return dest; + }, + }; + setImmediate(() => { + for (const chunk of chunks) source.emit("data", chunk); + source.emit("end"); + }); + return source; +} + +function Source(chunks) { + this.chunks = chunks; + this.listeners = {}; + this.readable = true; + setImmediate(() => { + for (const chunk of this.chunks) this.emit("data", chunk); + this.emit("end"); + }); +} +Source.prototype.on = function (event, listener) { + (this.listeners[event] = this.listeners[event] || []).push(listener); + return this; +}; +Source.prototype.emit = function (event, value) { + for (const listener of this.listeners[event] || []) listener(value); + return true; +}; +Source.prototype.pipe = function (dest) { + this.on("data", (chunk) => dest.write(chunk)); + this.on("end", () => dest.end()); + return dest; +}; + +/** @returns {any} */ +export function source(chunks) { + return new Source(chunks); +} + +/** @returns {any} */ +export function plainSink(label) { + const parts = []; + return { + write(chunk) { + parts.push(String(chunk)); + return true; + }, + end() { + console.log(label, "sink end", parts.join("")); + }, + on() { + return this; + }, + once() { + return this; + }, + emit() { + return false; + }, + }; +} + +function Sink(label) { + this.label = label; + this.parts = []; + this.listeners = []; + this.writable = true; +} +Sink.prototype.on = function (event, listener) { + this.listeners.push({ event, listener, once: false }); + return this; +}; +Sink.prototype.once = function (event, listener) { + this.listeners.push({ event, listener, once: true }); + return this; +}; +Sink.prototype.prependListener = function (event, listener) { + this.listeners.unshift({ event, listener, once: false }); + return this; +}; +Sink.prototype.removeListener = function (event, listener) { + this.listeners = this.listeners.filter((l) => l.event !== event || l.listener !== listener); + return this; +}; +Sink.prototype.listenerCount = function (event) { + return this.listeners.filter((l) => l.event === event).length; +}; +Sink.prototype.emit = function (event, ...args) { + const matching = this.listeners.filter((l) => l.event === event); + this.listeners = this.listeners.filter((l) => l.event !== event || !l.once); + for (const l of matching) l.listener(...args); + return matching.length > 0; +}; +Sink.prototype.write = function (chunk) { + this.parts.push(String(chunk)); + return true; +}; +Sink.prototype.end = function () { + console.log(this.label, "ended with", this.parts.join("|"), "listening to", this.listeners.map((l) => l.event).join(",")); + this.emit("finish"); + console.log(this.label, "after finish listening to", this.listeners.map((l) => l.event).join(",")); +}; + +/** @returns {any} */ +export function sink(label) { + return new Sink(label); +} + +/** @returns {Promise} */ +export function jsCollect(stream) { + return new Promise((resolve) => { + const parts = []; + stream.on("data", (chunk) => parts.push(String(chunk))); + stream.on("end", () => resolve(parts.join(""))); + }); +} + +export function same(a, b) { + return a === b; +} diff --git a/tests/corpus/6481-node-stream-dyn-writable/main.ts b/tests/corpus/6481-node-stream-dyn-writable/main.ts new file mode 100644 index 000000000..9ac429b8f --- /dev/null +++ b/tests/corpus/6481-node-stream-dyn-writable/main.ts @@ -0,0 +1,67 @@ +// NodeJS.WritableStream slots hold a native Writable, a plain JavaScript +// sink and process.stdout: TypeScript writes and ends them, and a copy +// helper typed over both interfaces pipes native and JavaScript sources +// into native and JavaScript destinations. +import { Readable, Writable } from "node:stream"; +import { plainSink, plainSource } from "./sources.js"; + +function emit(label: string, out: NodeJS.WritableStream): void { + const ok = out.write(`${label} one\n`); + out.write(`${label} two\n`); + console.log(label, "write answered", typeof ok); +} + +function finish(out: NodeJS.WritableStream): void { + out.end(); +} + +function copy(src: NodeJS.ReadableStream, dst: NodeJS.WritableStream): NodeJS.WritableStream { + return src.pipe(dst); +} + +function collector(parts: string[]): Writable { + return new Writable({ + write(chunk: Buffer, _encoding, done) { + parts.push(chunk.toString()); + done(); + }, + }); +} + +async function main(): Promise { + const parts: string[] = []; + const native = collector(parts); + const finished = new Promise((resolve) => native.on("finish", () => resolve())); + emit("native", native); + finish(native); + await finished; + console.log("native got", JSON.stringify(parts.join(""))); + + const sink = plainSink("plain"); + emit("plain", sink); + finish(sink); + + emit("stdout", process.stdout); + + const target = plainSink("copy native into plain"); + console.log("copy returns target", copy(Readable.from(["c1", "c2"]), target) === target); + await new Promise((resolve) => setTimeout(resolve, 10)); + copy(plainSource(["c3", "c4"]), plainSink("copy plain into plain")); + await new Promise((resolve) => setTimeout(resolve, 10)); + const copied: string[] = []; + const native2 = collector(copied); + await new Promise((resolve) => { + native2.on("finish", () => resolve()); + copy(plainSource(["c5", "c6"]), native2); + }); + console.log("copy plain into native", copied.join("")); + const copiedNative: string[] = []; + const native3 = collector(copiedNative); + await new Promise((resolve) => { + native3.on("finish", () => resolve()); + copy(Readable.from(["c7", "c8"]), native3); + }); + console.log("copy native into native", copiedNative.join("")); +} + +void main(); diff --git a/tests/corpus/6481-node-stream-dyn-writable/sources.js b/tests/corpus/6481-node-stream-dyn-writable/sources.js new file mode 100644 index 000000000..9c8491455 --- /dev/null +++ b/tests/corpus/6481-node-stream-dyn-writable/sources.js @@ -0,0 +1,49 @@ +// A plain JavaScript source implementing on/pipe and a plain sink with +// write/end. + +/** @returns {any} */ +export function plainSource(chunks) { + const listeners = {}; + const source = { + on(event, listener) { + (listeners[event] ??= []).push(listener); + return source; + }, + emit(event, value) { + for (const listener of listeners[event] ?? []) listener(value); + }, + pipe(dest) { + source.on("data", (chunk) => dest.write(chunk)); + source.on("end", () => dest.end()); + return dest; + }, + }; + setImmediate(() => { + for (const chunk of chunks) source.emit("data", chunk); + source.emit("end"); + }); + return source; +} + +/** @returns {any} */ +export function plainSink(label) { + const parts = []; + return { + write(chunk) { + parts.push(String(chunk)); + return true; + }, + end() { + console.log(label, "sink end", parts.join("")); + }, + on() { + return this; + }, + once() { + return this; + }, + emit() { + return false; + }, + }; +} diff --git a/tests/corpus/6482-node-stream-dyn-helpers/main.ts b/tests/corpus/6482-node-stream-dyn-helpers/main.ts new file mode 100644 index 000000000..db1c686a9 --- /dev/null +++ b/tests/corpus/6482-node-stream-dyn-helpers/main.ts @@ -0,0 +1,98 @@ +// The helper shapes real code writes around NodeJS.ReadableStream, fed by +// native and JavaScript streams: an async function returning one of +// several producers, a JavaScript consumer of its result, a part piped +// into a PassThrough with { end: false } and unpiped, pause/resume/ +// setEncoding/readable, an optional destroy through an intersection type, +// for await and pipeline over native values in the slot. +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { PassThrough, Readable, Writable } from "node:stream"; +import { pipeline } from "node:stream/promises"; +import * as zlib from "node:zlib"; +import { openPart, part, streamToText } from "./sources.js"; + +async function open(kind: string, file: string): Promise { + if (kind === "file") return fs.createReadStream(file); + return openPart(["x", "y"]); +} + +async function pipePart(item: NodeJS.ReadableStream, out: PassThrough): Promise { + item.pipe(out, { end: false }); + await new Promise((resolve) => item.on("end", () => resolve())); + item.unpipe(out); +} + +function pass(stream: NodeJS.ReadableStream): NodeJS.ReadableStream { + return stream; +} + +function stop(stream: NodeJS.ReadableStream): void { + (stream as NodeJS.ReadableStream & { destroy?: () => void }).destroy?.(); +} + +async function iterate(label: string, stream: NodeJS.ReadableStream): Promise { + let text = ""; + for await (const chunk of stream) text += chunk.toString(); + console.log(label, "iterated", text); +} + +async function main(): Promise { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-stream-dyn-")); + const file = path.join(dir, "a.txt"); + fs.writeFileSync(file, "file text"); + console.log("file", await streamToText(await open("file", file))); + console.log("part", await streamToText(await open("part", file))); + + const out = new PassThrough(); + const parts: string[] = []; + out.on("data", (chunk: Buffer) => parts.push(chunk.toString())); + await pipePart(await open("part", file), out); + await pipePart(await open("file", file), out); + const ended = new Promise((resolve) => out.on("end", () => resolve())); + out.end(); + await ended; + console.log("out", parts.join("|")); + + const js = pass(part(["j1"])); + js.pause(); + js.resume(); + js.setEncoding("utf8"); + console.log("js readable", js.readable); + await new Promise((resolve) => js.on("end", () => resolve())); + const native = pass(Readable.from(["n1"])); + native.setEncoding("utf8"); + console.log("native readable", native.readable); + const chunks: string[] = []; + await new Promise((resolve) => { + native.on("data", (chunk: Buffer | string) => chunks.push(chunk.toString())); + native.on("end", () => resolve()); + }); + console.log("native chunks", chunks.join("")); + + const closing = pass(Readable.from(["c1"])); + closing.on("close", () => console.log("native closed")); + stop(pass(part(["d1"]))); + stop(closing); + + await iterate("native", pass(Readable.from(["i1", "i2"]))); + await iterate("file", pass(fs.createReadStream(file))); + + const packed = path.join(dir, "a.gz"); + fs.writeFileSync(packed, zlib.gzipSync(Buffer.from("packed text"))); + const unpacked: string[] = []; + await pipeline( + pass(fs.createReadStream(packed)), + zlib.createGunzip(), + new Writable({ + write(chunk: Buffer, _encoding, done) { + unpacked.push(chunk.toString()); + done(); + }, + }), + ); + console.log("pipeline", unpacked.join("")); + fs.rmSync(dir, { recursive: true }); +} + +void main(); diff --git a/tests/corpus/6482-node-stream-dyn-helpers/sources.js b/tests/corpus/6482-node-stream-dyn-helpers/sources.js new file mode 100644 index 000000000..c395b4379 --- /dev/null +++ b/tests/corpus/6482-node-stream-dyn-helpers/sources.js @@ -0,0 +1,71 @@ +// A constructor-function stream part with its own listener table, whose +// pipe() honors { end: false } and whose unpipe() detaches it, plus a +// JavaScript consumer that reads a stream to text. + +function Part(chunks) { + this.chunks = chunks; + this.listeners = {}; + this.dest = null; + this.ending = true; + this.readable = true; + setImmediate(() => { + for (const chunk of this.chunks) { + if (this.dest) this.dest.write(chunk); + else this.emit("data", chunk); + } + if (this.dest && this.ending) this.dest.end(); + this.emit("end"); + }); +} +Part.prototype.on = function (event, listener) { + (this.listeners[event] = this.listeners[event] || []).push(listener); + return this; +}; +Part.prototype.emit = function (event, value) { + for (const listener of this.listeners[event] || []) listener(value); + return true; +}; +Part.prototype.pipe = function (dest, options) { + this.dest = dest; + this.ending = !options || options.end !== false; + return dest; +}; +Part.prototype.unpipe = function () { + console.log("part unpipe"); + this.dest = null; + return this; +}; +Part.prototype.pause = function () { + console.log("part pause"); + return this; +}; +Part.prototype.resume = function () { + console.log("part resume"); + return this; +}; +Part.prototype.setEncoding = function (encoding) { + console.log("part setEncoding", encoding); + return this; +}; +Part.prototype.destroy = function () { + console.log("part destroy"); +}; + +/** @returns {Promise} */ +export async function openPart(chunks) { + return new Part(chunks); +} + +/** @returns {any} */ +export function part(chunks) { + return new Part(chunks); +} + +/** @returns {any} */ +export function streamToText(stream) { + return new Promise((resolve) => { + const parts = []; + stream.on("data", (chunk) => parts.push(String(chunk))); + stream.on("end", () => resolve(parts.join(""))); + }); +} diff --git a/tests/corpus/6490-events-once-signal.ts b/tests/corpus/6490-events-once-signal.ts new file mode 100644 index 000000000..8535e1fec --- /dev/null +++ b/tests/corpus/6490-events-once-signal.ts @@ -0,0 +1,120 @@ +// events.once(emitter, name, { signal }) follows Node's lib/events.js: an +// abort removes the event's and the 'error' listener and rejects with an +// AbortError whose cause is signal.reason, an aborted signal rejects +// without registering anything, and a settled once removes its abort +// listener. The result settles on Node's microtask turn: once is an async +// function returning the executor's promise. +import { EventEmitter, once } from "node:events"; + +async function main(): Promise { + const ee = new EventEmitter(); + const ac = new AbortController(); + setTimeout(() => ee.emit("ready", 1, "two"), 1); + const args = await once(ee, "ready", { signal: ac.signal }); + console.log("fulfilled", JSON.stringify(args), ee.listenerCount("ready"), ee.listenerCount("error")); + + const ac2 = new AbortController(); + const pending = once(ee, "ready", { signal: ac2.signal }); + console.log("before abort", ee.listenerCount("ready"), ee.listenerCount("error")); + ac2.abort(); + console.log("after abort", ee.listenerCount("ready"), ee.listenerCount("error")); + try { + await pending; + } catch (err) { + const e = err as NodeJS.ErrnoException; + console.log("rejected", e.name, e.message, e.code, (e.cause as Error).name); + } + + const ac3 = new AbortController(); + ac3.abort(new Error("already")); + try { + await once(ee, "ready", { signal: ac3.signal }); + } catch (err) { + const e = err as Error; + console.log("pre-aborted", e.name, (e.cause as Error).message, ee.listenerCount("ready")); + } + + const ac4 = new AbortController(); + setTimeout(() => ee.emit("error", new Error("boom")), 1); + try { + await once(ee, "ready", { signal: ac4.signal }); + } catch (err) { + console.log("error event", (err as Error).message, ee.listenerCount("ready")); + } + ac4.abort(); + console.log("late abort changes nothing"); + + const errs = once(ee, "error", { signal: ac.signal }); + ee.emit("error", new Error("as value")); + const [first] = await errs; + console.log("error as value", (first as Error).message); + + // An undefined signal is the plain form. + const maybe: AbortSignal | undefined = undefined; + setTimeout(() => ee.emit("ready", 3, "four"), 1); + console.log("undefined signal", JSON.stringify(await once(ee, "ready", { signal: maybe }))); + + // Listeners registered before and after the once's abort listener. + const ac5 = new AbortController(); + const ee5 = new EventEmitter(); + ac5.signal.addEventListener("abort", () => { + console.log("first abort listener", ee5.listenerCount("z")); + }); + const p5 = once(ee5, "z", { signal: ac5.signal }); + ac5.signal.addEventListener("abort", () => { + console.log("second abort listener", ee5.listenerCount("z")); + }); + ac5.abort("why"); + await p5.catch((err: Error) => console.log("p5", err.name, String(err.cause))); + + // The abort's removals emit 'removeListener'. + const ee6 = new EventEmitter(); + ee6.on("removeListener", (name: string) => console.log("removed", name)); + const ac6 = new AbortController(); + const p6 = once(ee6, "w", { signal: ac6.signal }); + ac6.abort(); + await p6.catch(() => console.log("p6 rejected")); + + // A 'newListener' listener that throws rejects the promise. + const ee7 = new EventEmitter(); + ee7.on("newListener", (name: string) => { + if (name === "go") throw new Error("no " + name); + }); + await once(ee7, "go", { signal: new AbortController().signal }).then( + () => console.log("fulfilled"), + (err: Error) => console.log("newListener rejected", err.message, ee7.listenerCount("go")), + ); + + // Settlement turns against plain promise chains. + const ee8 = new EventEmitter(); + const ac8 = new AbortController(); + void once(ee8, "x").then(() => console.log("plain")); + void once(ee8, "x", { signal: ac8.signal }).then(() => console.log("signal")); + void Promise.resolve() + .then(() => console.log("t1")) + .then(() => console.log("t2")) + .then(() => console.log("t3")) + .then(() => console.log("t4")); + ee8.emit("x"); + const ac9 = new AbortController(); + void once(ee8, "y", { signal: ac9.signal }).then( + () => console.log("fulfilled"), + () => console.log("aborted"), + ); + ac9.abort(); + const ac10 = new AbortController(); + ac10.abort(); + void once(ee8, "y", { signal: ac10.signal }).then( + () => console.log("fulfilled"), + () => console.log("pre-aborted turn"), + ); + await new Promise((resolve) => setTimeout(resolve, 5)); + + // Pending onces whose signal never aborts keep their listeners. + const idle = new AbortController(); + void once(ee, "never", { signal: idle.signal }); + void once(new EventEmitter(), "never", { signal: idle.signal }); + console.log("pending", ee.listenerCount("never"), ee.listenerCount("error")); +} + +void main(); diff --git a/tests/corpus/6490-js-named-promise-executor/main.ts b/tests/corpus/6490-js-named-promise-executor/main.ts new file mode 100644 index 000000000..79e2a17d7 --- /dev/null +++ b/tests/corpus/6490-js-named-promise-executor/main.ts @@ -0,0 +1,22 @@ +// A JavaScript library that builds its promise from a named executor +// function (`function run(resolve, reject) {...}; return new Promise(run)`, +// the classic retry helper shape): the executor's parameters are untyped, +// so it receives the native resolve and reject as checked-dynamic function +// values and calls them like any JavaScript function. +import { attempt } from "later"; +async function main(): Promise { + const v = await attempt(async (_bail: (e: Error) => void, n: number): Promise => { + if (n < 3) throw new Error("try " + n); + return "done after " + n; + }, 5); + console.log(v); + try { + await attempt((bail: (e: Error) => void): string => { + bail(new Error("stop")); + return "ignored"; + }, 5); + } catch (e) { + console.log("caught", (e as Error).message); + } +} +main(); diff --git a/tests/corpus/6490-js-named-promise-executor/node_modules/later/index.d.ts b/tests/corpus/6490-js-named-promise-executor/node_modules/later/index.d.ts new file mode 100644 index 000000000..55956f01d --- /dev/null +++ b/tests/corpus/6490-js-named-promise-executor/node_modules/later/index.d.ts @@ -0,0 +1 @@ +export declare function attempt(fn: (bail: (e: Error) => void, n: number) => A | Promise, retries: number): Promise; diff --git a/tests/corpus/6490-js-named-promise-executor/node_modules/later/index.js b/tests/corpus/6490-js-named-promise-executor/node_modules/later/index.js new file mode 100644 index 000000000..deefa6aee --- /dev/null +++ b/tests/corpus/6490-js-named-promise-executor/node_modules/later/index.js @@ -0,0 +1,31 @@ +"use strict"; +function attempt(fn, retries) { + function run(resolve, reject) { + let n = 0; + function bail(err) { + reject(err || new Error("Aborted")); + } + function onError(err) { + if (err.bail) { + bail(err); + return; + } + if (n > retries) reject(err); + else runAttempt(); + } + function runAttempt() { + n++; + let val; + try { + val = fn(bail, n); + } catch (err) { + onError(err); + return; + } + Promise.resolve(val).then(resolve).catch(onError); + } + runAttempt(); + } + return new Promise(run); +} +module.exports = { attempt }; diff --git a/tests/corpus/6490-js-named-promise-executor/node_modules/later/package.json b/tests/corpus/6490-js-named-promise-executor/node_modules/later/package.json new file mode 100644 index 000000000..ce1f29bac --- /dev/null +++ b/tests/corpus/6490-js-named-promise-executor/node_modules/later/package.json @@ -0,0 +1,6 @@ +{ + "name": "later", + "version": "1.0.0", + "main": "./index.js", + "types": "./index.d.ts" +} diff --git a/tests/corpus/6490-js-named-promise-executor/tsconfig.json b/tests/corpus/6490-js-named-promise-executor/tsconfig.json new file mode 100644 index 000000000..27d492dd2 --- /dev/null +++ b/tests/corpus/6490-js-named-promise-executor/tsconfig.json @@ -0,0 +1,3 @@ +{ + "compilerOptions": { "strict": true, "types": ["node"] } +} diff --git a/tests/corpus/6491-catch-any-bindings.ts b/tests/corpus/6491-catch-any-bindings.ts new file mode 100644 index 000000000..9979637b4 --- /dev/null +++ b/tests/corpus/6491-catch-any-bindings.ts @@ -0,0 +1,34 @@ +// A catch binding annotated `any` reads the caught value through its +// checked-dynamic face: member reads, expando writes, `delete` and `in` +// act on the thrown object itself, and a rethrow keeps its identity. +const thrown: Error[] = []; + +function fail(n: number): never { + const e = new Error(`failure ${n}`); + thrown.push(e); + throw e; +} + +for (const n of [1, 2]) { + try { + try { + fail(n); + } catch (error: any) { + error.bail = n === 1; + console.log("marked:", error.bail, "bail" in error, error.message); + if (error.bail) { + delete error.bail; + console.log("deleted:", error.bail, "bail" in error); + } + throw error; + } + } catch (outer) { + console.log("outer:", outer === thrown[n - 1], (outer as Error).message, "bail" in (outer as object)); + } +} + +try { + JSON.parse("{"); +} catch (error: any) { + console.log(error.name, typeof error.message, error instanceof SyntaxError); +} diff --git a/tests/corpus/6491-events-once-streams.ts b/tests/corpus/6491-events-once-streams.ts new file mode 100644 index 000000000..efe60c737 --- /dev/null +++ b/tests/corpus/6491-events-once-streams.ts @@ -0,0 +1,87 @@ +// events.once over streams: file streams, Readable.from, a PassThrough +// destroyed with an error, a Writable, and a NodeJS.ReadableStream slot +// raced against its destination's 'close' with an AbortSignal that the +// finally block aborts. +import { once } from "node:events"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { PassThrough, Readable, Writable } from "node:stream"; + +function copyPart(src: NodeJS.ReadableStream, out: PassThrough): Promise { + const ac = new AbortController(); + src.pipe(out, { end: false }); + return Promise.race([ + once(src, "end", { signal: ac.signal }), + once(src, "error", { signal: ac.signal }).then(([error]) => { + throw error; + }), + once(out, "close", { signal: ac.signal }).then(() => { + throw new Error("output closed before the part ended"); + }), + ]).finally(() => { + ac.abort(); + src.unpipe(out); + console.log("part done", out.listenerCount("close"), out.listenerCount("error")); + }); +} + +async function main(): Promise { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-once-streams-")); + const file = path.join(dir, "a.txt"); + fs.writeFileSync(file, "part one\n"); + + const rs = fs.createReadStream(file); + rs.resume(); + console.log("file end", JSON.stringify(await once(rs, "end"))); + const ws = fs.createWriteStream(path.join(dir, "b.txt")); + ws.end("x"); + await once(ws, "finish"); + console.log("write stream finish"); + await once(ws, "close"); + console.log("write stream close"); + + const pt = new PassThrough(); + setTimeout(() => pt.destroy(new Error("pt broke")), 1); + try { + await once(pt, "end"); + } catch (err) { + console.log("passthrough error", (err as Error).message, pt.listenerCount("end")); + } + const r = Readable.from(["a", "b"]); + r.resume(); + await once(r, "close"); + console.log("readable close"); + const w = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, + }); + w.end("z"); + await once(w, "finish"); + console.log("writable finish"); + + const out = new PassThrough(); + let text = ""; + out.on("data", (chunk: Buffer) => { + text += chunk.toString(); + }); + await copyPart(fs.createReadStream(file), out); + await copyPart(fs.createReadStream(file), out); + console.log(JSON.stringify(text)); + try { + await copyPart(fs.createReadStream(path.join(dir, "missing.txt")), out); + } catch (err) { + console.log("missing", (err as NodeJS.ErrnoException).code); + } + const closing = new PassThrough(); + setTimeout(() => closing.destroy(), 1); + try { + await copyPart(new Readable({ read() {} }), closing); + } catch (err) { + console.log("closed", (err as Error).message); + } + fs.rmSync(dir, { recursive: true }); +} + +void main(); diff --git a/tests/corpus/6492-promise-race-combined.ts b/tests/corpus/6492-promise-race-combined.ts new file mode 100644 index 000000000..70f5399af --- /dev/null +++ b/tests/corpus/6492-promise-race-combined.ts @@ -0,0 +1,72 @@ +// Promise.race over events.once results: entries that only reject (a +// handler that throws) fit any result, and entries of different value +// types combine into 'unknown'. +import { EventEmitter, once } from "node:events"; + +function waitFor(a: EventEmitter, b: EventEmitter) { + let failed = false; + return Promise.race([ + once(a, "done"), + once(a, "error").then(([error]) => { + failed = true; + throw error; + }), + once(b, "close").then(() => { + failed = true; + throw new Error("closed early"); + }), + ]).finally(() => { + console.log("settled", failed); + }); +} + +function waitVoid(a: EventEmitter): Promise { + return Promise.race([ + once(a, "done").then((): void => undefined), + once(a, "error").then(([error]) => { + throw error; + }), + ]); +} + +function waitMixed(a: EventEmitter): Promise { + return Promise.race([once(a, "done").then((): void => undefined), once(a, "other")]); +} + +async function main(): Promise { + const a = new EventEmitter(); + const b = new EventEmitter(); + setTimeout(() => a.emit("done", 7), 1); + console.log("value", JSON.stringify(await waitFor(a, b))); + setTimeout(() => b.emit("close"), 1); + try { + await waitFor(a, b); + } catch (err) { + console.log("caught", (err as Error).message); + } + setTimeout(() => a.emit("error", new Error("bad")), 1); + try { + await waitFor(a, b); + } catch (err) { + console.log("caught", (err as Error).message); + } + setTimeout(() => a.emit("done", 8), 1); + await waitVoid(a); + console.log("void done"); + setTimeout(() => a.emit("other", 1, "x"), 1); + console.log("mixed", JSON.stringify(await waitMixed(a))); + setTimeout(() => a.emit("done", 9), 1); + console.log("mixed undefined", await waitMixed(a)); + // Turn order: the race settles one turn after its first entry. + const c = new EventEmitter(); + void waitMixed(c).then(() => console.log("race settled")); + void Promise.resolve() + .then(() => console.log("u1")) + .then(() => console.log("u2")) + .then(() => console.log("u3")) + .then(() => console.log("u4")) + .then(() => console.log("u5")); + c.emit("other", 2, "y"); +} + +void main(); diff --git a/tests/corpus/6493-stream-function-callbacks.ts b/tests/corpus/6493-stream-function-callbacks.ts new file mode 100644 index 000000000..7d5ee45e1 --- /dev/null +++ b/tests/corpus/6493-stream-function-callbacks.ts @@ -0,0 +1,66 @@ +// Stream subclasses that declare their completion callbacks with the broad +// `Function` type: the runtime's callable arrives and calls through it +// complete the chunk, pass data, or fail the stream. +import { Transform, Writable } from "node:stream"; + +class Prefixer extends Transform { + _s: string; + constructor() { + super(); + this._s = ""; + } + _transform(c: Buffer, _: string, cb: Function) { + const text = c.toString(); + const lines = text.split("\n").map((line, index, all) => { + if (index === 0) return this._s === "" || this._s.endsWith("\n") ? "> " + line : line; + if (index === all.length - 1) return line !== "" ? "> " + line : line; + return "> " + line; + }); + this._s += text; + this.push(lines.join("\n")); + cb(); + } +} + +class Upper extends Transform { + _transform(chunk: Buffer, _encoding: string, cb: Function) { + if (chunk.toString() === "bad") { + cb(new Error("bad chunk")); + return; + } + cb(null, chunk.toString().toUpperCase()); + } + _flush(cb: Function) { + this.push("!"); + cb(); + } +} + +class Sink extends Writable { + seen: string[] = []; + _write(chunk: Buffer, _encoding: string, cb: Function) { + this.seen.push(chunk.toString()); + cb(); + } + _final(cb: Function) { + this.seen.push(""); + cb(); + } +} + +const prefixer = new Prefixer(); +prefixer.pipe(process.stdout); +prefixer.write("a\nb"); +prefixer.write("c\n"); +prefixer.end("d\n"); + +const sink = new Sink(); +const upper = new Upper(); +upper.pipe(sink); +sink.on("finish", () => console.log("sink", JSON.stringify(sink.seen))); +upper.write("a"); +upper.end("b"); + +const bad = new Upper(); +bad.on("error", (err: Error) => console.log("error", err.message)); +bad.write("bad"); diff --git a/tests/corpus/6494-default-export-class-members/lib/date.js b/tests/corpus/6494-default-export-class-members/lib/date.js new file mode 100644 index 000000000..0c4a426f7 --- /dev/null +++ b/tests/corpus/6494-default-export-class-members/lib/date.js @@ -0,0 +1,8 @@ +// A class whose base scriptc cannot extend yet: the default export object +// below narrows this member away, and only reads of it would fence. +export class StampDate extends Date { + constructor(text) { + super(text); + this.label = "stamp:" + text; + } +} diff --git a/tests/corpus/6494-default-export-class-members/lib/error.js b/tests/corpus/6494-default-export-class-members/lib/error.js new file mode 100644 index 000000000..7f2ac5744 --- /dev/null +++ b/tests/corpus/6494-default-export-class-members/lib/error.js @@ -0,0 +1,7 @@ +export class ParseError extends Error { + constructor(message, line) { + super(message); + this.name = "ParseError"; + this.line = line; + } +} diff --git a/tests/corpus/6494-default-export-class-members/lib/index.js b/tests/corpus/6494-default-export-class-members/lib/index.js new file mode 100644 index 000000000..66935fac6 --- /dev/null +++ b/tests/corpus/6494-default-export-class-members/lib/index.js @@ -0,0 +1,14 @@ +import { StampDate } from "./date.js"; +import { ParseError } from "./error.js"; +function parse(text) { + if (text === "") throw new ParseError("empty document", 1); + const [key, value] = text.split("=").map((part) => part.trim()); + return { key, value }; +} +function stringify(value) { + return JSON.stringify(value); +} +// A default export object of functions and class values (the shorthand +// spelling of `{ ParseError: ParseError }`). +export default { parse, stringify, StampDate, ParseError }; +export { parse, stringify, StampDate, ParseError }; diff --git a/tests/corpus/6494-default-export-class-members/main.ts b/tests/corpus/6494-default-export-class-members/main.ts new file mode 100644 index 000000000..1548e6415 --- /dev/null +++ b/tests/corpus/6494-default-export-class-members/main.ts @@ -0,0 +1,15 @@ +// A JavaScript module's default export object holding functions and class +// values builds at module initialization, and its members work through +// both the default object and the named exports. +import lib, { parse, ParseError } from "./lib/index.js"; + +console.log(JSON.stringify(parse("a = 1"))); +console.log(lib.stringify(lib.parse("b = 2"))); +try { + lib.parse(""); +} catch (e) { + if (e instanceof ParseError) console.log(e.name, e.message, e.line); +} +const Ctor = lib.ParseError; +const made = new Ctor("built through the default object", 7); +console.log(made instanceof ParseError, made.message, made.line, Ctor === ParseError); diff --git a/tests/corpus/6494-default-export-class-members/package.json b/tests/corpus/6494-default-export-class-members/package.json new file mode 100644 index 000000000..ca00e63c9 --- /dev/null +++ b/tests/corpus/6494-default-export-class-members/package.json @@ -0,0 +1,5 @@ +{ + "name": "default-export-class-members-corpus", + "private": true, + "type": "module" +} diff --git a/tests/corpus/6494-events-once-dyn-stream-slot/main.ts b/tests/corpus/6494-events-once-dyn-stream-slot/main.ts new file mode 100644 index 000000000..501944ae3 --- /dev/null +++ b/tests/corpus/6494-events-once-dyn-stream-slot/main.ts @@ -0,0 +1,59 @@ +// events.once over a checked-dynamic NodeJS.ReadableStream slot: the +// program also moves a JavaScript stream object into such a slot, so the +// slots hold native streams through their typed capsules. +import { once } from "node:events"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { PassThrough } from "node:stream"; +import { part } from "./sources.js"; + +function collect(src: NodeJS.ReadableStream): Promise { + return new Promise((resolve) => { + let text = ""; + src.on("data", (chunk: Buffer | string) => { + text += chunk.toString(); + }); + src.on("end", () => resolve(text)); + }); +} + +function copyPart(src: NodeJS.ReadableStream, out: PassThrough) { + const ac = new AbortController(); + src.pipe(out, { end: false }); + return Promise.race([ + once(src, "end", { signal: ac.signal }), + once(src, "error", { signal: ac.signal }).then(([error]) => { + throw error; + }), + once(out, "close", { signal: ac.signal }).then(() => { + throw new Error("output closed before the part ended"); + }), + ]).finally(() => { + ac.abort(); + src.unpipe(out); + }); +} + +async function main(): Promise { + console.log("js part", await collect(part(["a", "b"]))); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-once-dyn-")); + const file = path.join(dir, "a.txt"); + fs.writeFileSync(file, "native part\n"); + const out = new PassThrough(); + let text = ""; + out.on("data", (chunk: Buffer) => { + text += chunk.toString(); + }); + await copyPart(fs.createReadStream(file), out); + await copyPart(fs.createReadStream(file), out); + console.log(JSON.stringify(text)); + try { + await copyPart(fs.createReadStream(path.join(dir, "missing.txt")), out); + } catch (err) { + console.log("missing", (err as NodeJS.ErrnoException).code); + } + fs.rmSync(dir, { recursive: true }); +} + +void main(); diff --git a/tests/corpus/6494-events-once-dyn-stream-slot/sources.js b/tests/corpus/6494-events-once-dyn-stream-slot/sources.js new file mode 100644 index 000000000..e76e88785 --- /dev/null +++ b/tests/corpus/6494-events-once-dyn-stream-slot/sources.js @@ -0,0 +1,21 @@ +// A constructor-function stream with its own listener table. +function Part(chunks) { + this.chunks = chunks; + this.listeners = {}; + setImmediate(() => { + for (const chunk of this.chunks) this.emit("data", chunk); + this.emit("end"); + }); +} +Part.prototype.on = function (event, listener) { + (this.listeners[event] = this.listeners[event] || []).push(listener); + return this; +}; +Part.prototype.emit = function (event, value) { + for (const listener of this.listeners[event] || []) listener(value); + return true; +}; + +export function part(chunks) { + return new Part(chunks); +} diff --git a/tests/corpus/6550-npm-boolean-bitwise/main.ts b/tests/corpus/6550-npm-boolean-bitwise/main.ts new file mode 100644 index 000000000..a08502343 --- /dev/null +++ b/tests/corpus/6550-npm-boolean-bitwise/main.ts @@ -0,0 +1,15 @@ +// Bitwise operators over booleans in a JavaScript dependency convert them +// with ToNumber, true to 1 and false to 0, as Node does (async-sema's +// `isFn(pauseFn) ^ isFn(resumeFn)` option check). +import { Sema, bits } from 'token-sema'; + +const plain = new Sema(3); +const paired = new Sema(2, { pauseFn: () => {}, resumeFn: () => {} }); +console.log(plain.capacity, plain.pausable, paired.capacity, paired.pausable); +try { + new Sema(1, { pauseFn: () => {} }); +} catch (error) { + console.log((error as Error).message); +} +console.log(bits([1, 0, 3])); +console.log(bits([])); diff --git a/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.d.ts b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.d.ts new file mode 100644 index 000000000..de028a027 --- /dev/null +++ b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.d.ts @@ -0,0 +1,6 @@ +export declare class Sema { + constructor(capacity: number, options?: { pauseFn?: () => void; resumeFn?: () => void }); + readonly capacity: number; + readonly pausable: number; +} +export declare function bits(values: number[]): string; diff --git a/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.js b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.js new file mode 100644 index 000000000..1efaf449c --- /dev/null +++ b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/index.js @@ -0,0 +1,35 @@ +// The option check from async-sema (MIT): pauseFn and resumeFn must be given +// together, tested with `^` over two booleans. Bitwise operators convert +// booleans with ToNumber. +'use strict'; +function isFn(x) { + return typeof x == 'function'; +} +class Sema { + constructor(capacity, { pauseFn, resumeFn } = {}) { + if (isFn(pauseFn) ^ isFn(resumeFn)) throw new Error('pauseFn and resumeFn must be both set for pausing'); + this.capacity = capacity; + this.pausable = isFn(pauseFn) & isFn(resumeFn); + } +} +function bits(values) { + var yes = values.length > 0; + var no = values.length < 0; + var flags = 0; + var count = 0; + for (var v of values) { + flags = flags | (v > 0); + count = count + ((v > 1) & 1); + } + // The right operand assigns the variable the left one reads; the left + // operand reads it first. + var seen = values.length > 0; + var toggled = seen ^ (seen = !seen); + var shifted = seen << (seen = true); + return [ + yes ^ no, yes ^ yes, yes & no, yes | no, no | no, yes & 3, 2 | yes, + yes << 3, yes >> 0, no >>> 0, -1 >>> yes, 5 ^ yes, no ^ 7, + typeof (yes ^ no), flags, count, toggled, shifted, seen, + ].join(' '); +} +module.exports = { Sema: Sema, bits: bits }; diff --git a/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/package.json b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/package.json new file mode 100644 index 000000000..7d74609a7 --- /dev/null +++ b/tests/corpus/6550-npm-boolean-bitwise/node_modules/token-sema/package.json @@ -0,0 +1 @@ +{ "name": "token-sema", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6560-constructor-function-readable.cjs b/tests/corpus/6560-constructor-function-readable.cjs new file mode 100644 index 000000000..5319aada1 --- /dev/null +++ b/tests/corpus/6560-constructor-function-readable.cjs @@ -0,0 +1,121 @@ +// Readable streams built by constructor functions, the from2 pattern: +// util.inherits(Ctor, Readable), Readable.call(this, opts) and _read on +// the prototype. +const { Readable } = require("stream"); +const util = require("util"); + +function Source(list, opts) { + if (!(this instanceof Source)) return new Source(list, opts); + this._list = list.slice(); + // Before Readable.call there is no state: Node's setter ignores this. + this.destroyed = false; + Readable.call(this, opts || {}); + this._reads = 0; + console.log("hwm", this._readableState.highWaterMark, "objectMode", this._readableState.objectMode); +} +util.inherits(Source, Readable); +Source.prototype._read = function (size) { + this._reads++; + const item = this._list.shift(); + this.push(item === undefined ? null : Buffer.from(item)); +}; + +const s = Source(["alpha", "beta", "gamma"]); +console.log("instanceof", s instanceof Source, s instanceof Readable, Source.super_ === Readable); +console.log("readable", s.readable, "ended", s.readableEnded, "destroyed", s.destroyed); +console.log("flowing", s.readableFlowing, s._readableState.flowing); +s.on("data", function (chunk) { + console.log("data", this === s, Buffer.isBuffer(chunk), chunk.toString()); +}); +s.on("end", function () { + console.log("end", this === s, s.readableEnded, s._readableState.ended, s._reads); +}); +s.on("close", () => { + console.log("close", s.destroyed, s.closed); + paused(); +}); +console.log("sync after on('data')", s.readableFlowing); + +// from2's callback-driven _read, asynchronous pushes and a small +// highWaterMark. +function From(read, opts) { + if (!(this instanceof From)) return new From(read, opts); + this._reading = false; + this._callback = check; + this.destroyed = false; + Readable.call(this, opts); + const self = this; + const hwm = this._readableState.highWaterMark; + function check(err, data) { + if (self.destroyed) return; + if (err) return self.destroy(err); + if (data === null) return self.push(null); + self._reading = false; + if (self.push(data)) self._read(hwm); + } + this._from = read; +} +util.inherits(From, Readable); +From.prototype._read = function (size) { + if (this._reading || this.destroyed) return; + this._reading = true; + this._from(size, this._callback); +}; +From.prototype.destroy = function (err) { + if (this.destroyed) return; + this.destroyed = true; + const self = this; + process.nextTick(function () { + if (err) self.emit("error", err); + self.emit("close"); + }); +}; + +function paused() { + const parts = ["one ", "two ", "three"]; + const r = new From(function (size, next) { + setImmediate(() => next(null, parts.length ? parts.shift() : null)); + }, { highWaterMark: 4 }); + console.log("from hwm", r._readableState.highWaterMark, r.readableHighWaterMark); + r.setEncoding("utf8"); + let text = ""; + r.on("readable", () => { + let chunk; + while ((chunk = r.read()) !== null) text += chunk; + }); + r.on("end", () => { + console.log("paused text", JSON.stringify(text), typeof text); + // From() without new runs outside the listener, where `this` is unset. + setImmediate(failing); + }); +} + +function failing() { + let calls = 0; + const r = From(function (size, next) { + calls++; + if (calls === 2) return next(new Error("source failed")); + next(null, "chunk" + calls); + }); + r.on("data", (d) => console.log("failing data", String(d))); + r.on("error", (e) => console.log("failing error", e.message, r.destroyed)); + r.on("close", () => { + console.log("failing close", calls); + unshifted(); + }); +} + +function unshifted() { + const r = new Source(["xyz"]); + r.once("readable", () => { + const first = r.read(1); + console.log("read(1)", String(first), r.readableLength); + r.unshift(first); + console.log("after unshift", r.readableLength); + r.pause(); + console.log("isPaused", r.isPaused()); + r.on("data", (d) => console.log("rest", String(d))); + r.resume(); + }); + r.on("end", () => console.log("unshifted end")); +} diff --git a/tests/corpus/6560-npm-js-capability-probes/main.ts b/tests/corpus/6560-npm-js-capability-probes/main.ts new file mode 100644 index 000000000..e9eacee8e --- /dev/null +++ b/tests/corpus/6560-npm-js-capability-probes/main.ts @@ -0,0 +1,14 @@ +// JavaScript dependency forms from bundled zip packages, which run as Node +// runs them: +// - a var table replaced by an Int32Array view of itself (buffer-crc32); +// - function declarations that redefine themselves on the first call, and +// Buffer.from over an array with an undefined encoding (yazl); +// - the deprecated `new Buffer(...)` fallback both keep, and the Buffer +// methods called on the untyped buffers they return. +import { crc32, localHeader, signature, legacy } from 'zip-helpers'; + +console.log(crc32('hello'), crc32(Buffer.from([1, 2, 3])), crc32('world', 17), crc32(4)); +console.log(Array.from(localHeader('a.txt', 9)).join(',')); +console.log(Array.from(localHeader('b', 1)).join(',')); +console.log(Array.from(signature()).join(',')); +console.log(legacy()); diff --git a/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.d.ts b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.d.ts new file mode 100644 index 000000000..0b51fe366 --- /dev/null +++ b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.d.ts @@ -0,0 +1,4 @@ +export declare function crc32(input: Uint8Array | string | number, previous?: number): number; +export declare function localHeader(name: string, size: number): Uint8Array; +export declare function signature(): Uint8Array; +export declare function legacy(): string; diff --git a/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.js b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.js new file mode 100644 index 000000000..15fc0a0c4 --- /dev/null +++ b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/index.js @@ -0,0 +1,125 @@ +// Small parts of buffer-crc32 (MIT) and yazl (MIT), in their published +// shapes: a var table replaced by an Int32Array view of itself, and +// function declarations that replace themselves with the implementation +// that works on the first call. +'use strict'; +var Buffer2 = require('buffer').Buffer; +var CRC_TABLE = [ + 0, 1996959894, 3993919788, 2567524794, 124634137, 1886057615, 3915621685, 2657392035, + 249268274, 2044508324, 3772115230, 2547177864, 162941995, 2125561021, 3887607047, 2428444049, + 498536548, 1789927666, 4089016648, 2227061214, 450548861, 1843258603, 4107580753, 2211677639, + 325883990, 1684777152, 4251122042, 2321926636, 335633487, 1661365465, 4195302755, 2366115317, + 997073096, 1281953886, 3579855332, 2724688242, 1006888145, 1258607687, 3524101629, 2768942443, + 901097722, 1119000684, 3686517206, 2898065728, 853044451, 1172266101, 3705015759, 2882616665, + 651767980, 1373503546, 3369554304, 3218104598, 565507253, 1454621731, 3485111705, 3099436303, + 671266974, 1594198024, 3322730930, 2970347812, 795835527, 1483230225, 3244367275, 3060149565, + 1994146192, 31158534, 2563907772, 4023717930, 1907459465, 112637215, 2680153253, 3904427059, + 2013776290, 251722036, 2517215374, 3775830040, 2137656763, 141376813, 2439277719, 3865271297, + 1802195444, 476864866, 2238001368, 4066508878, 1812370925, 453092731, 2181625025, 4111451223, + 1706088902, 314042704, 2344532202, 4240017532, 1658658271, 366619977, 2362670323, 4224994405, + 1303535960, 984961486, 2747007092, 3569037538, 1256170817, 1037604311, 2765210733, 3554079995, + 1131014506, 879679996, 2909243462, 3663771856, 1141124467, 855842277, 2852801631, 3708648649, + 1342533948, 654459306, 3188396048, 3373015174, 1466479909, 544179635, 3110523913, 3462522015, + 1591671054, 702138776, 2966460450, 3352799412, 1504918807, 783551873, 3082640443, 3233442989, + 3988292384, 2596254646, 62317068, 1957810842, 3939845945, 2647816111, 81470997, 1943803523, + 3814918930, 2489596804, 225274430, 2053790376, 3826175755, 2466906013, 167816743, 2097651377, + 4027552580, 2265490386, 503444072, 1762050814, 4150417245, 2154129355, 426522225, 1852507879, + 4275313526, 2312317920, 282753626, 1742555852, 4189708143, 2394877945, 397917763, 1622183637, + 3604390888, 2714866558, 953729732, 1340076626, 3518719985, 2797360999, 1068828381, 1219638859, + 3624741850, 2936675148, 906185462, 1090812512, 3747672003, 2825379669, 829329135, 1181335161, + 3412177804, 3160834842, 628085408, 1382605366, 3423369109, 3138078467, 570562233, 1426400815, + 3317316542, 2998733608, 733239954, 1555261956, 3268935591, 3050360625, 752459403, 1541320221, + 2607071920, 3965973030, 1969922972, 40735498, 2617837225, 3943577151, 1913087877, 83908371, + 2512341634, 3803740692, 2075208622, 213261112, 2463272603, 3855990285, 2094854071, 198958881, + 2262029012, 4057260610, 1759359992, 534414190, 2176718541, 4139329115, 1873836001, 414664567, + 2282248934, 4279200368, 1711684554, 285281116, 2405801727, 4167216745, 1634467795, 376229701, + 2685067896, 3608007406, 1308918612, 956543938, 2808555105, 3495958263, 1231636301, 1047427035, + 2932959818, 3654703836, 1088359270, 936918000, 2847714899, 3736837829, 1202900863, 817233897, + 3183342108, 3401237130, 1404277552, 615818150, 3134207493, 3453421203, 1423857449, 601450431, + 3009837614, 3294710456, 1567103746, 711928724, 3020668471, 3272380065, 1510334235, 755167117 +]; +typeof Int32Array < 'u' && (CRC_TABLE = new Int32Array(CRC_TABLE)); + +function ensureBuffer(input) { + if (Buffer2.isBuffer(input)) return input; + var hasNewBufferAPI = typeof Buffer2.alloc == 'function' && typeof Buffer2.from == 'function'; + if (typeof input == 'number') return hasNewBufferAPI ? Buffer2.alloc(input) : new Buffer2(input); + if (typeof input == 'string') return hasNewBufferAPI ? Buffer2.from(input) : new Buffer2(input); + throw new Error('input must be buffer, number, or string, received ' + typeof input); +} + +function crc32(buf, previous) { + buf = ensureBuffer(buf); + for (var crc = ~~previous ^ -1, n = 0; n < buf.length; n++) crc = CRC_TABLE[(crc ^ buf[n]) & 255] ^ (crc >>> 8); + return (crc ^ -1) >>> 0; +} + +function bufferFrom(something, encoding) { + bufferFrom = modern; + try { + return bufferFrom(something, encoding); + } catch { + return (bufferFrom = legacy), bufferFrom(something, encoding); + } + function modern(something2, encoding2) { + return Buffer.from(something2, encoding2); + } + function legacy(something2, encoding2) { + return new Buffer(something2, encoding2); + } +} + +function bufferAlloc(size) { + bufferAlloc = modern; + try { + return bufferAlloc(size); + } catch { + return (bufferAlloc = legacy), bufferAlloc(size); + } + function modern(size2) { + return Buffer.alloc(size2); + } + function legacy(size2) { + return new Buffer(size2); + } +} + +function localHeader(name, size) { + var nameBuffer = bufferFrom(name, 'utf-8'); + var fixed = bufferAlloc(8); + fixed.writeUInt32LE(0x04034b50, 0); + fixed.writeUInt16LE(nameBuffer.length, 4); + fixed.writeUInt16LE(size, 6); + return Buffer.concat([fixed, nameBuffer]); +} + +var SIGNATURE = bufferFrom([0x50, 0x4b, 0x05, 0x06], undefined); + +function legacy() { + var sized = new Buffer2(3); + var text = new Buffer2('hi', 'utf8'); + var array = new Buffer2([1, 2, 255]); + var out = [sized.length, sized[0], text.toString(), array.readUInt16BE(1), array.readInt8(2)]; + try { + new Buffer2(2, 'utf8'); + } catch (error) { + out.push(error.code); + } + try { + array.writeUInt8(300, 0); + } catch (error) { + out.push(error.code); + } + try { + array.readUInt8('0'); + } catch (error) { + out.push(error.code); + } + return out.join(' '); +} + +function signature() { + return SIGNATURE; +} + +module.exports = { crc32: crc32, localHeader: localHeader, signature: signature, legacy: legacy }; diff --git a/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/package.json b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/package.json new file mode 100644 index 000000000..2192addcf --- /dev/null +++ b/tests/corpus/6560-npm-js-capability-probes/node_modules/zip-helpers/package.json @@ -0,0 +1 @@ +{ "name": "zip-helpers", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6561-constructor-function-writable.cjs b/tests/corpus/6561-constructor-function-writable.cjs new file mode 100644 index 000000000..ee25b7595 --- /dev/null +++ b/tests/corpus/6561-constructor-function-writable.cjs @@ -0,0 +1,141 @@ +// Writable and Duplex streams built by constructor functions, the bl and +// tar-stream patterns: Writable.call(this, opts), Duplex.call(this), +// _write/_final/_read on the prototype, listeners added before the base +// constructor runs, and an end() override that calls the base's end. +const stream = require("stream"); +const util = require("util"); + +function Sink(opts) { + if (!(this instanceof Sink)) return new Sink(opts); + stream.Writable.call(this, opts); + this.chunks = []; + this.finalized = false; +} +util.inherits(Sink, stream.Writable); +Sink.prototype._write = function (chunk, enc, cb) { + console.log("_write", this instanceof Sink, enc, Buffer.isBuffer(chunk), chunk.toString()); + this.chunks.push(chunk); + if (this.chunks.length === 2) setImmediate(cb); + else cb(); +}; +Sink.prototype._final = function (cb) { + console.log("_final", this.chunks.length); + this.finalized = true; + cb(); +}; + +const sink = new Sink({ highWaterMark: 8 }); +console.log("writable", sink.writable, sink.writableHighWaterMark, sink._writableState.highWaterMark); +console.log("write a", sink.write("first")); +console.log("write b", sink.write(Buffer.from("second"), () => console.log("write b callback"))); +console.log("needDrain", sink.writableNeedDrain, sink.writableLength); +sink.on("drain", () => console.log("drain", sink.writableLength)); +sink.on("finish", function () { + console.log("finish", this === sink, sink.finalized, sink.writableFinished, Buffer.concat(sink.chunks).toString()); +}); +sink.on("close", () => { + console.log("close", sink.destroyed, sink.closed); + duplex(); +}); +sink.end("third", (err) => console.log("end callback", err, sink.writableEnded, sink._writableState.ended)); +console.log("after end", sink.writableEnded, sink.writable); + +// A bl-style list: a Duplex that collects writes and reads them back. +function BufferListStream(callback) { + if (!(this instanceof BufferListStream)) return new BufferListStream(callback); + if (typeof callback === "function") { + this._callback = callback; + const piper = function piper(err) { + if (this._callback) { + this._callback(err); + this._callback = null; + } + }.bind(this); + this.on("pipe", function onPipe(src) { + console.log("pipe event", src instanceof stream.Readable); + src.on("error", piper); + }); + this.on("unpipe", function onUnpipe(src) { + console.log("unpipe event"); + src.removeListener("error", piper); + }); + callback = null; + } + this._bufs = []; + this.length = 0; + stream.Duplex.call(this); +} +util.inherits(BufferListStream, stream.Duplex); +BufferListStream.prototype._write = function (buf, encoding, callback) { + this._bufs.push(buf); + this.length += buf.length; + callback(); +}; +BufferListStream.prototype._read = function (size) { + if (!this.length) return this.push(null); + const all = Buffer.concat(this._bufs); + size = Math.min(size, this.length, 4); + this.push(all.subarray(0, size)); + this._bufs = [all.subarray(size)]; + this.length -= size; +}; +BufferListStream.prototype.end = function end(chunk) { + stream.Duplex.prototype.end.call(this, chunk); + if (this._callback) { + this._callback(null, Buffer.concat(this._bufs)); + this._callback = null; + } +}; +BufferListStream.prototype.slice = function () { + return Buffer.concat(this._bufs); +}; + +function Letters(text) { + stream.Readable.call(this, { highWaterMark: 2 }); + this._text = text; +} +util.inherits(Letters, stream.Readable); +Letters.prototype._read = function () { + const next = this._text.slice(0, 3); + this._text = this._text.slice(3); + this.push(next.length ? next : null); +}; + +function duplex() { + const list = new BufferListStream((err, data) => { + console.log("bl callback", err, data.toString(), data.length); + }); + console.log("duplex", list.readable, list.writable, list.allowHalfOpen, list instanceof stream.Duplex); + console.log("pre-constructor listeners", list.listenerCount("pipe"), list.listenerCount("unpipe")); + list.write("hello "); + list.write("constructor "); + list.end("functions"); + list.on("finish", () => { + const out = []; + list.on("data", (d) => out.push(d.toString())); + list.on("end", () => { + console.log("read back", JSON.stringify(out)); + piping(); + }); + }); +} + +function piping() { + const src = new Letters("abcdefghij"); + const dst = new Sink({ highWaterMark: 1 }); + dst.on("pipe", (from) => console.log("dst pipe", from === src)); + const returned = src.pipe(dst); + console.log("pipe returns dest", returned === dst); + dst.on("finish", () => { + console.log("piped", Buffer.concat(dst.chunks).toString(), src.readableEnded); + const keep = new Sink(); + const from = new Letters("xyz"); + from.pipe(keep, { end: false }); + from.on("end", () => { + setImmediate(() => { + console.log("end:false keeps the destination open", keep.writable, keep.writableEnded); + keep.end(); + }); + }); + }); +} diff --git a/tests/corpus/6562-constructor-function-transform.cjs b/tests/corpus/6562-constructor-function-transform.cjs new file mode 100644 index 000000000..144a746bc --- /dev/null +++ b/tests/corpus/6562-constructor-function-transform.cjs @@ -0,0 +1,143 @@ +// Transform and PassThrough streams built by constructor functions (the +// yazl and tar-stream patterns), piped together with runtime streams: +// zlib and fs. +const { Readable, Writable, Transform, PassThrough } = require("stream"); +const util = require("util"); +const zlib = require("zlib"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); + +// yazl's ByteCounter +function ByteCounter(options) { + Transform.call(this, options); + this.byteCount = 0; +} +util.inherits(ByteCounter, Transform); +ByteCounter.prototype._transform = function (chunk, encoding, cb) { + this.byteCount += chunk.length; + cb(null, chunk); +}; + +// A transform with a flush that appends a trailer. +function Upper() { + if (!(this instanceof Upper)) return new Upper(); + Transform.call(this); + this.seen = 0; +} +util.inherits(Upper, Transform); +Upper.prototype._transform = function (chunk, encoding, cb) { + this.seen++; + this.push(chunk.toString().toUpperCase()); + cb(); +}; +Upper.prototype._flush = function (cb) { + cb(null, "[" + this.seen + " chunks]"); +}; + +// tar-stream's entry stream: a PassThrough with its own destroy. +function Source(parent, offset) { + this._parent = parent; + this.offset = offset; + PassThrough.call(this, { autoDestroy: false }); +} +util.inherits(Source, PassThrough); +Source.prototype.destroy = function (err) { + console.log("Source.destroy forwarded", err && err.message); + this._parent.destroyed = true; +}; + +// A PassThrough without its own _transform passes chunks through. +function Plain() { + PassThrough.call(this); +} +util.inherits(Plain, PassThrough); + +function Words(words) { + Readable.call(this); + this._words = words.slice(); +} +util.inherits(Words, Readable); +Words.prototype._read = function () { + this.push(this._words.length ? this._words.shift() : null); +}; + +function Collect() { + Writable.call(this); + this.parts = []; +} +util.inherits(Collect, Writable); +Collect.prototype._write = function (chunk, enc, cb) { + this.parts.push(chunk); + cb(); +}; +Collect.prototype.text = function () { + return Buffer.concat(this.parts).toString(); +}; + +const upper = Upper(); +const words = []; +upper.on("data", (d) => words.push(d.toString())); +upper.on("end", () => { + console.log("upper", JSON.stringify(words), upper.seen, upper._readableState.ended, upper._writableState.finished); + counter(); +}); +upper.write("one "); +upper.write(Buffer.from("two ")); +upper.end("three"); + +function counter() { + const counterStream = new ByteCounter(); + const sink = new Collect(); + new Words(["hello ", "byte ", "counter"]).pipe(counterStream).pipe(sink); + sink.on("finish", () => { + console.log("counted", counterStream.byteCount, JSON.stringify(sink.text())); + entry(); + }); +} + +function entry() { + const parent = { destroyed: false }; + const source = new Source(parent, 512); + const plain = new Plain(); + console.log("source", source.offset, source instanceof PassThrough, source instanceof Transform); + let body = ""; + source.pipe(plain); + plain.on("data", (d) => (body += d)); + plain.on("end", () => { + console.log("entry body", JSON.stringify(body), source.destroyed); + source.destroy(new Error("stop")); + console.log("parent destroyed", parent.destroyed); + gzip(); + }); + source.write("file "); + source.end("contents"); +} + +function gzip() { + const counted = new ByteCounter(); + const out = new Collect(); + new Words(["compressed ", "through ", "zlib"]).pipe(counted).pipe(zlib.createGzip()).pipe(out); + out.on("finish", () => { + const back = zlib.gunzipSync(Buffer.concat(out.parts)).toString(); + console.log("gzip roundtrip", JSON.stringify(back), counted.byteCount); + toFile(); + }); +} + +function toFile() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ctor-stream-")); + const file = path.join(dir, "out.txt"); + const ws = fs.createWriteStream(file); + new Words(["written ", "by ", "a ", "constructor"]).pipe(Upper()).pipe(ws); + ws.on("finish", () => { + console.log("file", JSON.stringify(fs.readFileSync(file, "utf8"))); + // A runtime stream piped into a constructor-function stream. + const back = new Collect(); + fs.createReadStream(file).pipe(new ByteCounter()).pipe(back); + back.on("finish", () => { + console.log("read back", JSON.stringify(back.text())); + fs.rmSync(dir, { recursive: true }); + }); + }); +} diff --git a/tests/corpus/6563-constructor-function-stream-errors.cjs b/tests/corpus/6563-constructor-function-stream-errors.cjs new file mode 100644 index 000000000..cf2fb3e7c --- /dev/null +++ b/tests/corpus/6563-constructor-function-stream-errors.cjs @@ -0,0 +1,137 @@ +// Errors, destroy and custom events on constructor-function streams. +const { Readable, Writable, Duplex } = require("stream"); +const util = require("util"); + +function Failing() { + Writable.call(this); +} +util.inherits(Failing, Writable); +Failing.prototype._write = function (chunk, enc, cb) { + const err = new Error("cannot store " + chunk); + err.code = "E_STORE"; + this.lastError = err; + cb(err); +}; + +const failing = new Failing(); +failing.on("error", function (err) { + console.log("write error", this === failing, err === failing.lastError, err.message, err.code); + console.log("errored", failing.errored === err, failing.destroyed, failing.writable); +}); +failing.on("close", () => { + console.log("write close"); + destroying(); +}); +failing.write("x", (err) => console.log("write callback", err && err.code)); + +function Ticker() { + Readable.call(this); + this.n = 0; +} +util.inherits(Ticker, Readable); +Ticker.prototype._read = function () { + this.push(String(this.n++)); +}; +Ticker.prototype._destroy = function (err, cb) { + console.log("_destroy", err && err.message, this.n > 0); + setImmediate(() => cb(err && new Error("wrapped: " + err.message))); +}; + +function destroying() { + const t = new Ticker(); + const order = []; + t.on("data", (d) => { + if (d.toString() === "3") t.destroy(new Error("enough"), (e) => order.push("destroy callback " + (e && e.message))); + }); + t.on("error", (e) => order.push("error " + e.message)); + t.on("close", () => { + order.push("close"); + console.log(order.join(" | "), t.destroyed, t.readable); + t.destroy(null, () => console.log("destroy after close calls back")); + notImplemented(); + }); +} + +function Bare() { + Readable.call(this); +} +util.inherits(Bare, Readable); + +function Throws() { + Readable.call(this); +} +util.inherits(Throws, Readable); +Throws.prototype._read = function () { + throw new TypeError("read threw"); +}; + +function notImplemented() { + const bare = new Bare(); + bare.on("error", (e) => { + console.log("bare", e.code, e.message); + const thrower = new Throws(); + thrower.on("error", (err) => { + console.log("throws", err instanceof TypeError, err.message, thrower.destroyed); + afterEnd(); + }); + thrower.resume(); + }); + bare.resume(); +} + +function Quiet() { + Writable.call(this); +} +util.inherits(Quiet, Writable); +Quiet.prototype._write = function (chunk, enc, cb) { + cb(); +}; + +function afterEnd() { + const q = new Quiet(); + q.end("done"); + q.on("error", (e) => { + console.log("after end", e.code, e.message); + customEvents(); + }); + q.write("late"); +} + +// tar-stream's Extract emits 'entry' with three arguments. +function Extract() { + if (!(this instanceof Extract)) return new Extract(); + Writable.call(this); +} +util.inherits(Extract, Writable); +Extract.prototype._write = function (data, enc, cb) { + const header = { name: data.toString(), size: data.length }; + this.emit("entry", header, "stream-for-" + header.name, cb); +}; + +function customEvents() { + const ex = Extract(); + const seen = []; + function onEntry(header, s, next) { + seen.push(header.name + ":" + header.size + ":" + s + ":" + (this === ex)); + next(); + } + ex.on("entry", onEntry); + ex.once("entry", (header) => seen.push("once " + header.name)); + ex.prependListener("entry", () => seen.push("first")); + console.log("listenerCount", ex.listenerCount("entry"), ex.listenerCount("entry", onEntry)); + ex.write("a.txt"); + ex.removeListener("entry", onEntry); + ex.write("b.txt"); + console.log(seen.join(" | ")); + console.log("emit returns", ex.emit("nothing"), ex.emit("entry", { name: "c", size: 0 }, "", () => {})); + try { + ex.emit("error", new RangeError("nobody listens")); + } catch (e) { + console.log("unhandled error rethrown", e instanceof RangeError, e.message); + } + ex.setMaxListeners(1); + console.log("max listeners", ex.getMaxListeners()); + ex.removeAllListeners("entry"); + console.log("after removeAll", ex.listenerCount("entry")); + console.log("Duplex prototype", typeof Duplex.prototype.write, typeof Duplex.prototype.push); +} diff --git a/tests/corpus/6564-constructor-function-stream-late-hooks.cjs b/tests/corpus/6564-constructor-function-stream-late-hooks.cjs new file mode 100644 index 000000000..22a8ec2f4 --- /dev/null +++ b/tests/corpus/6564-constructor-function-stream-late-hooks.cjs @@ -0,0 +1,117 @@ +// Stream hooks a constructor function assigns after the base constructor +// runs, and unpipe from the destination's 'pipe' listener. +const { Writable, Transform, PassThrough } = require("stream"); +const util = require("util"); + +function Sink() { + Writable.call(this); + this.chunks = []; + this._final = function (cb) { + console.log("_final", this instanceof Sink, this.chunks.join(",")); + setImmediate(cb); + }; +} +util.inherits(Sink, Writable); +Sink.prototype._write = function (chunk, enc, cb) { + this.chunks.push(String(chunk)); + cb(); +}; + +const sink = new Sink(); +sink.on("prefinish", () => console.log("sink prefinish")); +sink.on("finish", () => { + console.log("sink finish"); + flushing(); +}); +sink.write("a"); +sink.end("b"); + +function Summer() { + Transform.call(this); + this.total = 0; + this._flush = function (cb) { + console.log("_flush", this.total); + cb(null, "total " + this.total); + }; +} +util.inherits(Summer, Transform); +Summer.prototype._transform = function (chunk, enc, cb) { + this.total += Number(String(chunk)); + cb(null, "seen " + chunk + "\n"); +}; + +function flushing() { + const summer = new Summer(); + const out = []; + summer.on("data", (chunk) => out.push(String(chunk))); + summer.on("end", () => { + console.log("summer end", JSON.stringify(out)); + passing(); + }); + summer.write("2"); + summer.write("3"); + summer.end(); +} + +function Upper() { + PassThrough.call(this); + this._transform = function (chunk, enc, cb) { + cb(null, String(chunk).toUpperCase()); + }; +} +util.inherits(Upper, PassThrough); + +function passing() { + const upper = new Upper(); + const out = []; + upper.on("data", (chunk) => out.push(String(chunk))); + upper.on("end", () => { + console.log("upper end", JSON.stringify(out)); + plain(); + }); + upper.write("x"); + upper.end("y"); +} + +// Without late hooks the defaults still apply. +function Plain() { + PassThrough.call(this); +} +util.inherits(Plain, PassThrough); + +function plain() { + const p = new Plain(); + const out = []; + p.on("data", (chunk) => out.push(String(chunk))); + p.on("finish", () => console.log("plain finish")); + p.on("end", () => { + console.log("plain end", JSON.stringify(out)); + unpiping(); + }); + p.end("same"); +} + +function Collector() { + Writable.call(this); + this.chunks = []; +} +util.inherits(Collector, Writable); +Collector.prototype._write = function (chunk, enc, cb) { + this.chunks.push(String(chunk)); + cb(); +}; + +function unpiping() { + const source = new PassThrough(); + const collector = new Collector(); + collector.on("pipe", function (src) { + console.log("pipe", typeof src.unpipe, this === collector); + src.unpipe(this); + }); + collector.on("unpipe", (src) => { + console.log("unpipe", typeof src.pipe); + source.write("after"); + setImmediate(() => console.log("collected", JSON.stringify(collector.chunks), source.readableFlowing)); + }); + source.pipe(collector); +} diff --git a/tests/corpus/6565-rebindable-function-import-cycle/main.ts b/tests/corpus/6565-rebindable-function-import-cycle/main.ts new file mode 100644 index 000000000..ff505d978 --- /dev/null +++ b/tests/corpus/6565-rebindable-function-import-cycle/main.ts @@ -0,0 +1,5 @@ +// A package whose import cycle calls a reassigned function declaration +// while the cycle initializes. +import { report } from "cycle-format"; + +for (const line of report()) console.log(line); diff --git a/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/dep.js b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/dep.js new file mode 100644 index 000000000..730e7b34e --- /dev/null +++ b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/dep.js @@ -0,0 +1,8 @@ +import { format } from "./index.js"; + +// Runs before index.js's body: format is the hoisted declaration. +export const early = format("a"); + +export function describe(value) { + return `${typeof format} ${format(value)}`; +} diff --git a/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.d.ts b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.d.ts new file mode 100644 index 000000000..55a683bb7 --- /dev/null +++ b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.d.ts @@ -0,0 +1,2 @@ +export declare function format(value: string): string; +export declare const report: () => string[]; diff --git a/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.js b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.js new file mode 100644 index 000000000..c96fadcad --- /dev/null +++ b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/index.js @@ -0,0 +1,18 @@ +// A reassigned function declaration of a module in an import cycle: the +// dependency calls it while the cycle initializes, before this module's +// body runs, and gets the hoisted declaration; later calls see the +// replacement. +import { early, describe } from "./dep.js"; + +export function format(value) { + format = function formatUpper(text) { + return `[${String(text).toUpperCase()}]`; + }; + return `<${value}>`; +} + +export const report = () => [ + "early: " + early, + "now: " + format("b") + " " + format("c"), + "dep: " + describe("d"), +]; diff --git a/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/package.json b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/package.json new file mode 100644 index 000000000..becff6bc6 --- /dev/null +++ b/tests/corpus/6565-rebindable-function-import-cycle/node_modules/cycle-format/package.json @@ -0,0 +1 @@ +{ "name": "cycle-format", "version": "1.0.0", "type": "module", "types": "./index.d.ts", "exports": { ".": { "types": "./index.d.ts", "default": "./index.js" } } } diff --git a/tests/corpus/6570-require-mixed-declarations.cjs b/tests/corpus/6570-require-mixed-declarations.cjs new file mode 100644 index 000000000..3d3e2c2fc --- /dev/null +++ b/tests/corpus/6570-require-mixed-declarations.cjs @@ -0,0 +1,46 @@ +// A require declarator beside other declarators in one var statement, the +// shape bundlers emit for a module's top level. The require binding has no +// storage of its own, so every read resolves to the module: construction in +// a class constructor, a class expression extending it, and calls through +// the other builtin bindings of the same statement. +var EventEmitter = require("events"), util = require("util"), path = require("path"), Base = class extends EventEmitter {}; + +class Pool { + constructor(size) { + this.size = size; + this.events = new EventEmitter; + this.events.on("grow", (by) => { + this.size += by; + console.log("grow", by, util.format("%d -> %d", this.size - by, this.size)); + }); + } +} +const pool = new Pool(2); +pool.events.emit("grow", 3); +console.log("pool", pool.size, pool.events instanceof EventEmitter, pool.events.listenerCount("grow")); + +var Worker = class { + constructor(name) { + this.name = name; + this.bus = new Base; + this.bus.on("done", (file) => console.log("done", this.name, path.basename(file))); + } +}; +const worker = new Worker("w1"); +worker.bus.emit("done", "/tmp/out/report.txt"); +console.log("worker", worker.bus instanceof Base, worker.bus instanceof EventEmitter, typeof Base); + +// A builtin require in a nested block is alias plumbing too. +if (pool.size > 0) { + var Emitter = require("node:events"), label = "nested"; +} +class Relay { + constructor() { + this.out = new Emitter(); + this.out.once("ping", (n) => console.log(label, "ping", n)); + } +} +const relay = new Relay(); +relay.out.emit("ping", 1); +relay.out.emit("ping", 2); +console.log("relay", relay.out.listenerCount("ping"), Emitter === EventEmitter); diff --git a/tests/corpus/6592-promise-armed-method-values/main.ts b/tests/corpus/6592-promise-armed-method-values/main.ts new file mode 100644 index 000000000..379c56ef9 --- /dev/null +++ b/tests/corpus/6592-promise-armed-method-values/main.ts @@ -0,0 +1,27 @@ +// Parse steps that answer either a status record or a promise of one keep +// their method values: reads through untyped code, detached calls, and the +// per-instance rebinding of the base constructor all see the function. +import { map, number, object, record, set, stepDetached, stepKinds } from "status-schema"; +async function main(): Promise { + const shaped = object({ count: number(), size: number() }); + const counts = record(number()); + const byName = map(number()); + const seen = set(number()); + console.log(stepKinds(shaped), "|", stepKinds(counts), "|", stepKinds(byName), "|", stepKinds(seen)); + console.log(shaped.safeParse({ count: 1, size: 2 }).success, shaped.safeParse({ count: "x", size: 2 }).success); + console.log(JSON.stringify(shaped.parse({ count: 3, size: 4 }))); + console.log(JSON.stringify(await shaped.parseAsync({ count: 5, size: 6 })), (await shaped.spa(7)).success); + console.log(counts.safeParse({ a: 1, b: 2 }).success, (await counts.safeParseAsync({ a: "x" })).success); + console.log(byName.safeParse(new Map([["a", 1]])).success, (await byName.safeParseAsync(new Map([["b", "x"]]))).success); + console.log(seen.safeParse(new Set([1, 2])).success, (await seen.spa(new Set([3]))).success); + console.log(await stepDetached(shaped, { count: 1, size: 1 }), "|", await stepDetached(counts, { a: "x" })); + console.log(await stepDetached(byName, new Map([["c", 2]])), "|", await stepDetached(seen, [1])); + try { + await counts.parseAsync(null); + } catch (e) { + console.log("rejected", e instanceof Error ? e.message : String(e)); + } + const nested = object({ inner: record(number()), tags: set(number()) }); + console.log(JSON.stringify(await nested.parseAsync({ inner: { q: 1 }, tags: new Set([1]) }))); +} +main(); diff --git a/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.d.ts b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.d.ts new file mode 100644 index 000000000..6607bf0c7 --- /dev/null +++ b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.d.ts @@ -0,0 +1,18 @@ +export interface SafeResult { + success: boolean; + data?: unknown; +} +export declare class Schema { + safeParse(data: unknown): SafeResult; + parse(data: unknown): unknown; + safeParseAsync(data: unknown): Promise; + spa(data: unknown): Promise; + parseAsync(data: unknown): Promise; +} +export declare function number(): Schema; +export declare function object(shape: { [key: string]: Schema }): Schema; +export declare function record(valueType: Schema): Schema; +export declare function map(valueType: Schema): Schema; +export declare function set(valueType: Schema): Schema; +export declare function stepKinds(schema: Schema): string; +export declare function stepDetached(schema: Schema, data: unknown): Promise; diff --git a/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.js b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.js new file mode 100644 index 000000000..4fb5ae460 --- /dev/null +++ b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/index.js @@ -0,0 +1,118 @@ +// A schema hierarchy in the shape of a validation library's: each parse +// step answers a status record or, in async mode, a promise of one built +// by a static async merge helper. The base constructor rebinds its +// methods per instance, async ones included. +export const INVALID = Object.freeze({ status: "aborted" }); +export class Status { + static async mergeAsync(pairs) { + const settled = []; + for (const pair of pairs) { + settled.push({ key: pair.key, value: await pair.value }); + } + return Status.mergeSync(settled); + } + static mergeSync(pairs) { + const value = {}; + for (const pair of pairs) { + if (pair.value.status === "aborted") return INVALID; + value[pair.key] = pair.value.value; + } + return { status: "valid", value }; + } +} +export class Schema { + constructor(def) { + this.spa = this.safeParseAsync; + this._def = def; + this.parse = this.parse.bind(this); + this.safeParse = this.safeParse.bind(this); + this.parseAsync = this.parseAsync.bind(this); + this.safeParseAsync = this.safeParseAsync.bind(this); + this.spa = this.spa.bind(this); + } + safeParse(data) { + const result = this._parse({ data, async: false }); + if (result instanceof Promise) throw new Error("async parse step in safeParse"); + return result.status === "valid" ? { success: true, data: result.value } : { success: false }; + } + parse(data) { + const result = this.safeParse(data); + if (result.success) return result.data; + throw new Error(this._def.name + ": invalid input"); + } + async safeParseAsync(data) { + const result = await this._parse({ data, async: true }); + return result.status === "valid" ? { success: true, data: result.value } : { success: false }; + } + async parseAsync(data) { + const result = await this.safeParseAsync(data); + if (result.success) return result.data; + throw new Error(this._def.name + ": invalid input"); + } +} +export class NumberSchema extends Schema { + _parse(input) { + return typeof input.data === "number" ? { status: "valid", value: input.data } : INVALID; + } +} +export class ObjectSchema extends Schema { + _parse(input) { + if (input.data === null || typeof input.data !== "object") return INVALID; + const pairs = []; + for (const key of Object.keys(this._def.shape)) { + pairs.push({ key, value: this._def.shape[key]._parse({ data: input.data[key], async: input.async }) }); + } + return input.async ? Status.mergeAsync(pairs) : Status.mergeSync(pairs); + } +} +export class RecordSchema extends Schema { + _parse(input) { + if (input.data === null || typeof input.data !== "object") return INVALID; + const pairs = []; + for (const key in input.data) { + pairs.push({ key, value: this._def.valueType._parse({ data: input.data[key], async: input.async }) }); + } + return input.async ? Status.mergeAsync(pairs) : Status.mergeSync(pairs); + } +} +export class MapSchema extends Schema { + _parse(input) { + if (!(input.data instanceof Map)) return INVALID; + const finish = () => { + const out = new Map(); + for (const [k, v] of input.data) { + if (this._def.valueType._parse({ data: v, async: false }).status !== "valid") return INVALID; + out.set(k, v); + } + return { status: "valid", value: out }; + }; + return input.async ? Promise.resolve().then(finish) : finish(); + } +} +export class SetSchema extends Schema { + _parse(input) { + if (!(input.data instanceof Set)) return INVALID; + const finish = () => { + const out = new Set(); + for (const v of input.data) { + if (this._def.valueType._parse({ data: v, async: false }).status !== "valid") return INVALID; + out.add(v); + } + return { status: "valid", value: out }; + }; + return input.async ? Promise.resolve().then(finish) : finish(); + } +} +export const number = () => new NumberSchema({ name: "number" }); +export const object = (shape) => new ObjectSchema({ name: "object", shape }); +export const record = (valueType) => new RecordSchema({ name: "record", valueType }); +export const map = (valueType) => new MapSchema({ name: "map", valueType }); +export const set = (valueType) => new SetSchema({ name: "set", valueType }); +// Untyped readers of the parse step's method value. +export const stepKinds = (schema) => typeof schema._parse + " " + typeof Object.getPrototypeOf(schema)._parse; +export const stepDetached = async (schema, data) => { + const step = schema._parse; + const sync = step.call(schema, { data, async: false }); + const later = await step.call(schema, { data, async: true }); + return sync.status + " " + later.status; +}; diff --git a/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/package.json b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/package.json new file mode 100644 index 000000000..1160994b1 --- /dev/null +++ b/tests/corpus/6592-promise-armed-method-values/node_modules/status-schema/package.json @@ -0,0 +1 @@ +{ "name": "status-schema", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/diagnostics/retired-engine-761-any-objects.ts b/tests/corpus/761-any-objects.ts similarity index 67% rename from tests/diagnostics/retired-engine-761-any-objects.ts rename to tests/corpus/761-any-objects.ts index 59796020b..45e6e6f82 100644 --- a/tests/diagnostics/retired-engine-761-any-objects.ts +++ b/tests/corpus/761-any-objects.ts @@ -1,6 +1,6 @@ -// Object/array literals in `any` slots build natively in the island; -// property reads/writes, computed keys, and method calls go through the -// engine (this-binding and coercions JS-exact). +// Object/array literals in `any` slots: property reads/writes, computed +// keys, and method calls go through the checked-dynamic value, including a +// member read on a fresh typed call result (`s.split(" ").length`). const o: any = { a: 1, b: "x", nested: { deep: true } }; o.a = o.a + 10; o["c"] = 2.5; diff --git a/tests/corpus/class-id-instances-as-streams.ts b/tests/corpus/class-id-instances-as-streams.ts new file mode 100644 index 000000000..813e54ed2 --- /dev/null +++ b/tests/corpus/class-id-instances-as-streams.ts @@ -0,0 +1,39 @@ +// Instances of a class hierarchy, held as untyped values, where the stream +// adapters probe for a native stream: pipe, pipeline, readline, and +// instanceof checks against the stream classes. Their first field is a +// number, which sits where a stream's vtable word would be. +import { Duplex, PassThrough, Readable, Transform, Writable, pipeline } from "node:stream"; +import { createInterface } from "node:readline"; + +class Shape { + size = 3.5; + area(): number { + return 0; + } +} +class Square extends Shape { + override area(): number { + return this.size * this.size; + } +} + +function attempt(label: string, run: () => unknown): void { + try { + run(); + console.log(label, "ok"); + } catch (error) { + console.log(label, "throws", error instanceof Error); + } +} + +const shape: any = new Square(); +try { + Readable.from(["a"]).pipe(shape); +} catch (error) { + console.log("pipe", (error as Error).name); +} +attempt("pipeline", () => pipeline(shape, new PassThrough(), () => {})); +attempt("readline-input", () => createInterface({ input: shape })); +console.log("instanceof", shape instanceof Readable, shape instanceof Writable, shape instanceof Duplex); +console.log("instanceof-transform", shape instanceof Transform, shape instanceof PassThrough); +console.log("area", (shape as Square).area()); diff --git a/tests/corpus/json-parse-fallback-extra-keys.ts b/tests/corpus/json-parse-fallback-extra-keys.ts new file mode 100644 index 000000000..7eba7b2a8 --- /dev/null +++ b/tests/corpus/json-parse-fallback-extra-keys.ts @@ -0,0 +1,37 @@ +// A failed checked cast keeps every member of the parsed object: extra +// members stay visible to Object.keys, JSON.stringify, for...in and +// util.inspect, in the source object's key order. +interface Point { + x: number; + y: number; +} + +function show(text: string): void { + const p = JSON.parse(text) as Point; + console.log(Object.keys(p).join(",")); + console.log(Object.values(p).length, Object.entries(p).length); + console.log(JSON.stringify(p)); + console.log(JSON.stringify(p, null, 2)); + const seen: string[] = []; + for (const k in p) seen.push(k); + console.log(seen.join("|")); + console.log(p); + console.log("has z:", "z" in p, Object.hasOwn(p, "z")); +} + +show('{"x":1,"y":2}'); +show('{"z":3,"x":"1","y":2,"label":{"text":"hi","size":[1,2]}}'); +show('{"x":1}'); +show('{"b":1,"a":2,"x":null,"y":[]}'); + +// Spread copies every member of the failed value, in source order. +function copyOf(text: string): void { + const p = JSON.parse(text) as Point; + const copy = { ...p, y: 10 }; + console.log(copy, Object.keys(copy).join(",")); + const pair = [p.x, p.y]; + console.log(pair, JSON.stringify({ point: p, at: [p.x] })); +} + +copyOf('{"x":1,"y":2}'); +copyOf('{"w":0,"x":"1","extra":true}'); diff --git a/tests/corpus/json-parse-fallback-flows.ts b/tests/corpus/json-parse-fallback-flows.ts new file mode 100644 index 000000000..82232959f --- /dev/null +++ b/tests/corpus/json-parse-fallback-flows.ts @@ -0,0 +1,57 @@ +// Checked-cast fallback across declaration forms and control flow: an +// annotated declaration from an untyped value, `unknown as T`, a site in a +// loop whose iterations alternate between fitting and failing values, +// closures and callbacks over the failed value, and a value that fits a +// typed parameter once it reaches it. +interface Job { + name: string; + retries: number; + steps: string[]; +} + +function retriesOf(n: number): string { + return n === 1 ? "1 retry" : `${n} retries`; +} + +function annotated(text: string): void { + const job: Job = JSON.parse(text); + console.log("annotated", job.name, job.retries, job.steps?.length); +} + +function fromUnknown(value: unknown): void { + const job = value as Job; + console.log("unknown", job.name ?? "(unnamed)", typeof job.retries); +} + +function loop(texts: string[]): void { + for (const text of texts) { + const job = JSON.parse(text) as Job; + const label = job.name + ":" + job.retries; + console.log("loop", label, typeof job.retries === "number" ? retriesOf(job.retries) : "n/a"); + } +} + +async function later(text: string): Promise { + const job = JSON.parse(text) as Job; + const steps = job.steps ?? []; + await new Promise((resolve) => setTimeout(resolve, 1)); + steps.forEach((step, i) => console.log("step", i, step)); + const upper = steps.map((s) => String(s).toUpperCase()); + console.log("upper", upper.join(" ")); + const describeJob = (): string => `${job.name} (${job.retries})`; + console.log("closure", describeJob()); +} + +annotated('{"name":"a","retries":1,"steps":["x"]}'); +annotated('{"name":"b","retries":"2"}'); +fromUnknown(JSON.parse('{"name":"c","retries":3,"steps":[]}')); +fromUnknown(JSON.parse('{"retries":"3"}')); +loop([ + '{"name":"d","retries":1,"steps":[]}', + '{"name":"e","retries":"x","steps":[]}', + '{"name":"f","retries":2,"steps":[]}', + '{"name":"g"}', +]); +later('{"name":"h","retries":0,"steps":["s1","s2"]}').then(() => + later('{"name":"i","retries":"0","steps":["s1",2]}'), +); diff --git a/tests/corpus/json-parse-fallback-missing.ts b/tests/corpus/json-parse-fallback-missing.ts new file mode 100644 index 000000000..b84f8af2f --- /dev/null +++ b/tests/corpus/json-parse-fallback-missing.ts @@ -0,0 +1,36 @@ +// A `JSON.parse(text) as T` whose value lacks required members keeps the +// parsed value: missing members read undefined, as in Node. +interface Address { + city: string; + zip: string; +} +interface User { + id: number; + name: string; + admin: boolean; + address: Address; + tags: string[]; +} + +function describe(text: string): void { + const user = JSON.parse(text) as User; + console.log("id", user.id, typeof user.id); + console.log("name", user.name, user.name === undefined); + console.log("admin", user.admin ? "yes" : "no", !user.admin); + console.log("city", user.address?.city ?? "(no city)"); + console.log("zip?", "zip" in (user.address ?? {})); + console.log("tags", user.tags?.length ?? 0); + if (!user.address) console.log("no address"); + else console.log("address", user.address.city, user.address.zip); + try { + console.log("street", user.address.city.length); + } catch (e) { + console.log("read threw", e instanceof TypeError, (e as Error).message); + } + console.log(`label: ${user.name}#${user.id}`); +} + +describe('{"id":1,"name":"ada","admin":true,"address":{"city":"London","zip":"N1"},"tags":["a"]}'); +describe('{"id":2}'); +describe('{"name":"bob","address":{"city":"Paris"}}'); +describe("{}"); diff --git a/tests/corpus/json-parse-fallback-nested.ts b/tests/corpus/json-parse-fallback-nested.ts new file mode 100644 index 000000000..96abddaf9 --- /dev/null +++ b/tests/corpus/json-parse-fallback-nested.ts @@ -0,0 +1,42 @@ +// Nested records and arrays of a value that failed its checked cast keep +// their identity: reads, writes and comparisons reach the same objects. +interface Line { + sku: string; + qty: number; +} +interface Cart { + id: string; + lines: Line[]; + owner: { name: string; email: string }; +} + +function lineTotal(lines: Line[]): number { + let n = 0; + for (const line of lines) n += line.qty; + return n; +} + +function inspectCart(text: string): void { + const cart = JSON.parse(text) as Cart; + const lines = cart.lines; + console.log("same lines:", lines === cart.lines, cart.owner === cart.owner); + lines.push({ sku: "added", qty: 1 }); + console.log("count:", cart.lines.length, lines.length); + const first = lines[0]; + first.qty = first.qty + 10; + console.log("first:", cart.lines[0].sku, cart.lines[0].qty); + console.log("skus:", lines.map((l) => l.sku).join(",")); + console.log("big:", lines.filter((l) => l.qty > 5).length); + console.log("found:", lines.find((l) => l.sku === "b")?.qty ?? "none"); + console.log("some string qty:", lines.some((l) => typeof l.qty === "string")); + console.log("owner:", cart.owner?.name ?? "(anonymous)", cart.owner?.email); + console.log(JSON.stringify(cart)); + // A typed function receives the lines when they fit its parameter. + if (lines.every((l) => typeof l.qty === "number" && typeof l.sku === "string")) + console.log("total:", lineTotal(lines)); + else console.log("total: skipped"); +} + +inspectCart('{"id":"c1","lines":[{"sku":"a","qty":1},{"sku":"b","qty":2}],"owner":{"name":"n","email":"e"}}'); +inspectCart('{"id":"c2","lines":[{"sku":"a","qty":1},{"sku":"b","qty":2}]}'); +inspectCart('{"id":3,"lines":[{"sku":"a","qty":"1"}],"owner":{"name":"n"}}'); diff --git a/tests/corpus/json-parse-fallback-partial.ts b/tests/corpus/json-parse-fallback-partial.ts new file mode 100644 index 000000000..ec3e2c769 --- /dev/null +++ b/tests/corpus/json-parse-fallback-partial.ts @@ -0,0 +1,59 @@ +// `Partial` and optional members: a request whose nested required +// members are missing is accepted the way Node accepts it (the listener +// pattern: the binding is assigned inside try, read after it). +interface Deployment { + id: string; + name: string; + billing: { plan: string }; +} +interface Order { + deployment: Deployment; + owner: { id: string }; +} +interface ListenerRequest { + buildOrder: Order; + traceCarrier?: Record; +} + +const delivered: string[] = []; + +async function readBody(body: string): Promise { + return body; +} + +async function handle(body: string): Promise { + const text = await readBody(body); + let request: Partial; + try { + request = JSON.parse(text) as Partial; + } catch { + console.log("400 invalid JSON body"); + return 400; + } + const buildOrder = request.buildOrder; + const deploymentId = buildOrder?.deployment?.id; + if (!deploymentId) { + console.log("400 missing buildOrder.deployment.id"); + return 400; + } + delivered.push(deploymentId); + console.log("200 accepted", deploymentId, Object.keys(request).join(",")); + console.log("carrier:", request.traceCarrier?.traceparent ?? "(none)"); + try { + console.log("plan:", buildOrder?.deployment.billing.plan); + } catch (e) { + console.log("plan threw:", (e as Error).message); + } + return 200; +} + +async function main(): Promise { + await handle('{"buildOrder":{"deployment":{"id":"dpl_1","name":"n","billing":{"plan":"pro"}},"owner":{"id":"o"}}}'); + await handle('{"buildOrder":{"deployment":{"id":"dpl_2"}},"traceCarrier":{"traceparent":"00-ab"}}'); + await handle('{"buildOrder":{"deployment":{}}}'); + await handle('{"traceCarrier":{}}'); + await handle("not json"); + console.log(delivered.join(",")); +} + +main(); diff --git a/tests/corpus/json-parse-fallback-wrong-types.ts b/tests/corpus/json-parse-fallback-wrong-types.ts new file mode 100644 index 000000000..06ed79255 --- /dev/null +++ b/tests/corpus/json-parse-fallback-wrong-types.ts @@ -0,0 +1,33 @@ +// Members whose JSON type differs from the declared one keep their real +// values and JavaScript semantics after a failed checked cast. +interface Reading { + sensor: string; + value: number; + ok: boolean | number; + samples: number[]; +} + +function report(text: string): string { + const r = JSON.parse(text) as Reading; + const doubled = r.value * 2; + const sum = r.value + 1; + console.log(r.sensor, typeof r.sensor, r.value, typeof r.value, doubled, sum); + console.log("ok:", r.ok, typeof r.ok, r.ok === true, r.ok == 1); + console.log("cmp:", r.value > 10, r.value < 10, r.value === 5); + console.log("samples:", Array.isArray(r.samples), r.samples.length); + let total = 0; + for (const s of r.samples) total = total + s; + console.log("total:", total, typeof total); + try { + const parts = r.samples.map((s) => typeof s); + console.log("kinds:", parts.join(",")); + } catch (e) { + console.log("map threw:", (e as Error).message); + } + return `${r.sensor}=${r.value}`; +} + +console.log(report('{"sensor":"t1","value":5,"ok":true,"samples":[1,2,3]}')); +console.log(report('{"sensor":7,"value":"5","ok":1,"samples":[1,"2",3]}')); +console.log(report('{"sensor":"t3","value":null,"ok":"yes","samples":"abc"}')); +console.log(report('{"sensor":["x"],"value":{"n":1},"ok":false,"samples":[]}')); diff --git a/tests/diagnostics/events-once-fences.ts b/tests/diagnostics/events-once-fences.ts index 2e5613f70..97bc76254 100644 --- a/tests/diagnostics/events-once-fences.ts +++ b/tests/diagnostics/events-once-fences.ts @@ -1,11 +1,14 @@ -// events.once lowers for EventEmitter instances and literal event names; -// these forms stay refused. +// events.once lowers for EventEmitter and stream instances, literal event +// names and a literal { signal } options object; these forms stay refused. import { EventEmitter, once } from "node:events"; +import { PassThrough } from "node:stream"; const ee = new EventEmitter(); const name: string = process.argv[2] ?? "ready"; +const options = {}; async function main(): Promise { - await once(ee, "ready", { signal: AbortSignal.timeout(5) }); // options + await once(ee, "ready", options); // options that are not an object literal await once(ee, name); // computed event name + await once(new PassThrough(), "data"); // a stream's 'data' event } void main(); diff --git a/tests/fixtures/node-types/events-once-signal-race.ts b/tests/fixtures/node-types/events-once-signal-race.ts new file mode 100644 index 000000000..4bc1d2d36 --- /dev/null +++ b/tests/fixtures/node-types/events-once-signal-race.ts @@ -0,0 +1,86 @@ +// events.once under @types/node, which types the result Promise: +// a NodeJS.ReadableStream slot and a PassThrough raced with an AbortSignal +// that the finally block aborts (the entries that throw are +// Promise, so the race result is the any[] of the 'end' entry), an +// EventEmitter race, and a Transform whose completion callback is declared +// with the broad `Function` type. +import { EventEmitter, once } from "node:events"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { PassThrough, Transform } from "node:stream"; + +function copyPart(src: NodeJS.ReadableStream, out: PassThrough) { + const ac = new AbortController(); + let failed = false; + src.pipe(out, { end: false }); + return Promise.race([ + once(src, "end", { signal: ac.signal }), + once(src, "error", { signal: ac.signal }).then(([error]) => { + failed = true; + throw error; + }), + once(out, "error", { signal: ac.signal }).then(([error]) => { + failed = true; + throw error; + }), + once(out, "close", { signal: ac.signal }).then(() => { + failed = true; + throw new Error("output closed before the part ended"); + }), + ]).finally(() => { + ac.abort(); + src.unpipe(out); + console.log("part done", failed); + }); +} + +function waitFor(a: EventEmitter) { + const ac = new AbortController(); + return Promise.race([ + once(a, "done", { signal: ac.signal }), + once(a, "error", { signal: ac.signal }).then(([error]) => { + throw error; + }), + ]).finally(() => ac.abort()); +} + +class Prefixer extends Transform { + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + _transform(chunk: Buffer, _encoding: string, cb: Function) { + cb(null, "> " + chunk.toString()); + } +} + +async function main(): Promise { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "scriptc-once-race-")); + const file = path.join(dir, "a.txt"); + fs.writeFileSync(file, "part\n"); + const out = new PassThrough(); + let text = ""; + out.on("data", (chunk: Buffer) => { + text += chunk.toString(); + }); + const ended = await copyPart(fs.createReadStream(file), out); + console.log("ended", ended.length); + await copyPart(fs.createReadStream(file).pipe(new Prefixer()), out); + console.log(JSON.stringify(text)); + try { + await copyPart(fs.createReadStream(path.join(dir, "missing.txt")), out); + } catch (err) { + console.log("missing", (err as NodeJS.ErrnoException).code); + } + const a = new EventEmitter(); + setTimeout(() => a.emit("done", 1, "two"), 1); + const args = (await waitFor(a)) as unknown[]; + console.log("done", JSON.stringify(args), a.listenerCount("done"), a.listenerCount("error")); + setTimeout(() => a.emit("error", new Error("bad")), 1); + try { + await waitFor(a); + } catch (err) { + console.log("caught", (err as Error).message, a.listenerCount("done")); + } + fs.rmSync(dir, { recursive: true }); +} + +void main(); diff --git a/tests/fixtures/node-types/process-versions-absent.ts b/tests/fixtures/node-types/process-versions-absent.ts new file mode 100644 index 000000000..8a59f7a58 --- /dev/null +++ b/tests/fixtures/node-types/process-versions-absent.ts @@ -0,0 +1,35 @@ +/* @types/node declares http_parser, v8, uv, zlib, ares and modules as + * strings. A compiled binary provides only node and openssl, and Node 24 + * has no http_parser either, so those entries read undefined and compare + * like it. Checks on the others print agreement between equality, typeof + * and `in`, which holds whichever entries the running process has. */ +const versions = process.versions; + +console.log(process.versions.http_parser === undefined, process.versions['http_parser'] === undefined); +console.log(versions.http_parser == null, typeof versions.http_parser, versions.http_parser ?? 'none'); +const parser = versions.http_parser; +console.log(parser === undefined ? 'no http_parser' : parser); +if (versions.http_parser) console.log('http_parser truthy'); +else console.log('http_parser falsy'); + +console.log( + (process.versions.v8 === undefined) === !('v8' in process.versions), + (process.versions['uv'] !== undefined) === 'uv' in versions, + (versions.zlib == null) === (typeof versions.zlib === 'undefined'), + (versions.ares === undefined) === (typeof versions.ares !== 'string'), +); +const names = ['v8', 'uv', 'zlib', 'ares', 'modules', 'node', 'openssl'] as const; +for (const name of names) { + const value = versions[name]; + const absent = value === undefined; + console.log( + name, + absent === !(name in versions), + absent === (typeof value === 'undefined'), + ((value ?? '') === '') === absent, + ); +} +const v8 = versions.v8; +console.log(v8 === undefined || v8.length > 0); +console.log(typeof (versions.modules || 'fallback')); +console.log(typeof versions.node, versions.openssl !== undefined, versions.node === process.version.slice(1)); diff --git a/tests/fixtures/self-hosting/lowering-context-cases.ts b/tests/fixtures/self-hosting/lowering-context-cases.ts index efa27183d..a39063796 100644 --- a/tests/fixtures/self-hosting/lowering-context-cases.ts +++ b/tests/fixtures/self-hosting/lowering-context-cases.ts @@ -35,7 +35,7 @@ function runContextCase(services: FrontendServices, entry: string): string[] { const shadowSymbol = symbols[1]!; const unusedSymbol = symbols[2]!; requireValue(outerSymbol !== shadowSymbol, "shadowed bindings share identity"); - requireValue(outerSymbol.name === shadowSymbol.name, "fixture did not shadow a name"); + requireValue(ts.symbolName(outerSymbol) === ts.symbolName(shadowSymbol), "fixture did not shadow a name"); const source = bindingSource(declarations[0]!); const root = newFnCtx(false, null, null, VOID); const outer = declareContextLocal(root, "value", F64, true, outerSymbol, source); diff --git a/tests/fixtures/self-hosting/ts7-checker-cases.ts b/tests/fixtures/self-hosting/ts7-checker-cases.ts index da0a3dc52..664dccd50 100644 --- a/tests/fixtures/self-hosting/ts7-checker-cases.ts +++ b/tests/fixtures/self-hosting/ts7-checker-cases.ts @@ -2,7 +2,7 @@ import { AstNode } from "../../../packages/compiler/src/frontend/ts7/ast-node.js import type { SourceFile } from "../../../packages/compiler/src/frontend/ts7/ast-types.js"; import { CheckerFacade, constituentTypes } from "../../../packages/compiler/src/frontend/ts7/checker.js"; import { SemanticChecker } from "../../../packages/compiler/src/frontend/ts7/semantic-checker.js"; -import { SemanticType } from "../../../packages/compiler/src/frontend/ts7/semantic-model.js"; +import { SemanticType, symbolName, symbolNameOf } from "../../../packages/compiler/src/frontend/ts7/semantic-model.js"; import { SyntaxKind, TypeFlags } from "../../../packages/compiler/src/frontend/ts7/enums.js"; export function checkerSource(): string { @@ -67,8 +67,8 @@ export function checkCheckerFacade(raw: SemanticChecker, root: SourceFile, reque const box = declaration(root, "Box"); const boxSymbol = facade.getSymbolAtLocation(box.name!)!; const boxType = facade.getDeclaredTypeOfSymbol(boxSymbol); - check(facade.getPropertyOfType(boxType, "value")!.name === "value", "property lookup"); - check(facade.getPropertiesOfType(boxType).some((property) => property.name === "method"), "property list"); + check(symbolName(facade.getPropertyOfType(boxType, "value")!) === "value", "property lookup"); + check(facade.getPropertiesOfType(boxType).some((property) => symbolName(property) === "method"), "property list"); check(facade.getBaseTypes(boxType).length === 0, "empty class bases"); const child = facade.getDeclaredTypeOfSymbol(facade.getSymbolAtLocation(declaration(root, "Child").name!)!); check(facade.getBaseTypes(child)[0] === boxType, "class base identity"); @@ -123,7 +123,7 @@ export function checkCheckerFacade(raw: SemanticChecker, root: SourceFile, reque check(facade.getTypeArguments(number).length === 0, "nonreference arguments"); const dict = facade.getDeclaredTypeOfSymbol(facade.getSymbolAtLocation(declaration(root, "Dict").name!)!); const index = facade.getIndexInfosOfType(dict)[0]!; - check(index.keyType === string && index.valueType === number && index.isReadonly, "index signature"); + check(index.keyType === string && index.type === number && index.isReadonly, "index signature"); check(facade.getAwaitedType(aliasType(facade, root, "Promised")) === number, "await promise"); check(facade.getAwaitedType(aliasType(facade, root, "MaybePromised")) === number, "await collapsed union"); check(facade.getAwaitedType(aliasType(facade, root, "DifferentPromises")) === undefined, "await distinct union refusal"); diff --git a/tests/fixtures/self-hosting/ts7-client-cases.ts b/tests/fixtures/self-hosting/ts7-client-cases.ts index d97f90892..0feaee6c9 100644 --- a/tests/fixtures/self-hosting/ts7-client-cases.ts +++ b/tests/fixtures/self-hosting/ts7-client-cases.ts @@ -12,6 +12,7 @@ import { SyntaxKind, ScriptTarget, ModuleKind, ModuleResolutionKind, ModuleDetec import { Ts7Session } from "../../../packages/compiler/src/frontend/ts7/session.js"; import { Ts7Paths, ts7DocumentFile } from "../../../packages/compiler/src/frontend/ts7/session-path.js"; import { parseSemanticJson } from "../../../packages/compiler/src/frontend/ts7/semantic-json.js"; +import { symbolName, symbolNameOf } from "../../../packages/compiler/src/frontend/ts7/semantic-model.js"; import { checkSemanticModel, semanticSource } from "./ts7-semantic-cases.js"; import { checkAstViews } from "./ts7-ast-views.js"; @@ -313,7 +314,7 @@ export function runTs7Client(client: Ts7RpcClient, directory: string, report: st const timing = client.timing(); check(timing.requests > 20 && timing.callbacks > 0 && reads > 0, "requests and filesystem callbacks executed"); writeFileSync(report, JSON.stringify({ - typeText, symbol: ownedSymbol.name, diagnostics: diagnostics.map((diagnostic) => diagnostic.code), + typeText, symbol: symbolName(ownedSymbol), diagnostics: diagnostics.map((diagnostic) => diagnostic.code), surrogateBoundary: checkSurrogateBoundary(), semanticSurrogateBoundary: checkSemanticSurrogateBoundary(), semanticModel: true, sessionLifecycle: true, echo: true, binaryAst: true, astIdentity: true, astViews: true, checkerFacade: true, checkerSnapshots: true, virtualFiles: true, retainedSnapshot: true, serverErrorRecovery: true, protocolFailures: true, diff --git a/tests/fixtures/self-hosting/ts7-semantic-cases.ts b/tests/fixtures/self-hosting/ts7-semantic-cases.ts index 41b61b824..07f2d7e14 100644 --- a/tests/fixtures/self-hosting/ts7-semantic-cases.ts +++ b/tests/fixtures/self-hosting/ts7-semantic-cases.ts @@ -1,7 +1,8 @@ import { AstFile, AstNode } from "../../../packages/compiler/src/frontend/ts7/ast-node.js"; import { AstKind } from "../../../packages/compiler/src/frontend/ts7/ast-schema.generated.js"; import { SemanticChecker } from "../../../packages/compiler/src/frontend/ts7/semantic-checker.js"; -import { SemanticSignature, SemanticSnapshot, SemanticSymbol, SemanticType } from "../../../packages/compiler/src/frontend/ts7/semantic-model.js"; +import { SemanticSignature, SemanticSnapshot, SemanticSymbol, SemanticType, symbolName, symbolNameOf } from "../../../packages/compiler/src/frontend/ts7/semantic-model.js"; +import type { __String } from "../../../packages/compiler/src/frontend/ts7/ast-types.js"; import { SemanticSignatureKind, SemanticSymbolFlags, SemanticTypeFlags } from "../../../packages/compiler/src/frontend/ts7/semantic-schema.generated.js"; export function semanticSource(): string { @@ -113,7 +114,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic const pairTarget = pair.getTarget()!; check(pairTarget.isTupleType() && pairTarget.fixedLength === 2 && pairTarget.readonly === true && pairTarget.elementFlags!.length === 2, "tuple target metadata"); check(checker.getTypeArguments(pair)[0] === intrinsicNumber && checker.getTypeArguments(pair)[1] === intrinsicString, "tuple argument identities"); - check(pair.getAliasSymbol()!.name === "Pair" && pair.getAliasTypeArguments()[0] === intrinsicNumber, "alias metadata"); + check(symbolName(pair.getAliasSymbol()!) === "Pair" && pair.getAliasTypeArguments()[0] === intrinsicNumber, "alias metadata"); const box = declaration(root, "Box"); const boxSymbol = symbolAt(checker, box.name!); @@ -121,21 +122,21 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic check(boxType.isClassOrInterface() && boxType.getSymbol() === boxSymbol, "declared class type"); // The checker instantiates a transient property symbol for a class type; // the binder's member symbol is distinct but refers to the same AST node. - const boundMember = boxSymbol.getMembers().get("value")!; + const boundMember = boxSymbol.getMembers().get("value" as __String)!; const checkedMember = checker.getPropertyOfType(boxType, "value")!; check(boundMember !== checkedMember && boundMember.declarationHandles[0]!.resolve() === checkedMember.declarationHandles[0]!.resolve(), "bound and instantiated member declarations"); check(boxSymbol.getMembers() === boxSymbol.getMembers() && boxSymbol.getExports() === boxSymbol.getExports(), "symbol table cache"); check(boxType.getBaseTypes()!.length === 0 && checker.getBaseTypes(boxType).length === 0, "empty base types"); const child = checker.getDeclaredTypeOfSymbol(symbolAt(checker, declaration(root, "Child").name!)); check(child.getBaseTypes()![0] === boxType && checker.getBaseTypes(child)[0] === boxType, "base type request identity"); - check(checker.getPropertiesOfType(boxType).some((symbol) => symbol.name === "method"), "property query"); + check(checker.getPropertiesOfType(boxType).some((symbol) => symbolName(symbol) === "method"), "property query"); check(checker.getPropertyOfType(boxType, "missing") === undefined, "absent property"); const nullable = typeAt(checker, declaration(root, "Nullable").type!); check(checker.getNonNullableType(nullable) === boxType, "non-nullable type"); const dict = checker.getDeclaredTypeOfSymbol(symbolAt(checker, declaration(root, "Dict").name!)); const index = checker.getIndexInfosOfType(dict)[0]!; - check(index.keyType === intrinsicString && index.valueType === intrinsicNumber && index.isReadonly, "index signature types"); - check(index.declaration!.resolve() === declaration(root, "Dict").members?.nodes[0], "index signature declaration"); + check(index.keyType === intrinsicString && index.type === intrinsicNumber && index.isReadonly, "index signature types"); + check(index.declaration === declaration(root, "Dict").members?.nodes[0], "index signature declaration"); const generic = checker.getDeclaredTypeOfSymbol(symbolAt(checker, declaration(root, "Generic").name!)); const genericParameter = generic.getTypeParameters()[0]!; check(genericParameter.isTypeParameter() && generic.getLocalTypeParameters()[0] === genericParameter && generic.getOuterTypeParameters().length === 0, "interface type parameters"); @@ -147,7 +148,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic const signature = signatureAt(checker, identity); const signatureTypes = signature.getTypeParameters(); check(signatureTypes.length === 1 && signatureTypes[0]!.isTypeParameter(), "signature type parameters"); - check(signature.getParameters()[0]!.name === "value" && signature.getParameters()[0]!.declarationHandles[0]!.resolve() === identity.parameters?.nodes[0], "signature parameters"); + check(symbolName(signature.getParameters()[0]!) === "value" && signature.getParameters()[0]!.declarationHandles[0]!.resolve() === identity.parameters?.nodes[0], "signature parameters"); check(signature.declarationHandle!.resolve() === identity, "signature declaration"); check(checker.getReturnTypeOfSignature(signature) === signatureTypes[0], "generic return identity"); check(checker.getParameterType(signature, 0) === signatureTypes[0], "parameter type query"); @@ -159,7 +160,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic const ctor = checker.getSignaturesOfType(checker.getTypeOfSymbol(boxSymbol)!, SemanticSignatureKind.Construct)[0]!; check(ctor.isConstruct && !ctor.isAbstract && checker.getReturnTypeOfSignature(ctor) === boxType, "construct signature"); const thisSignature = signatureAt(checker, declaration(root, "withThis")); - check(thisSignature.hasRestParameter && !thisSignature.isConstruct && thisSignature.getThisParameter()!.name === "this", "signature this/rest flags"); + check(thisSignature.hasRestParameter && !thisSignature.isConstruct && symbolName(thisSignature.getThisParameter()!) === "this", "signature this/rest flags"); check(checker.getRestTypeOfSignature(thisSignature) === intrinsicNumber && checker.isArrayLikeType(pair), "array predicates and rest element type"); const predicate = checker.getTypePredicateOfSignature(signatureAt(checker, declaration(root, "isBox")))!; check(predicate.type === boxType && predicate.parameterIndex === 0 && predicate.parameterName === "value", "type predicate"); @@ -169,7 +170,7 @@ export function checkSemanticModel(snapshot: SemanticSnapshot, checker: Semantic const moduleSymbol = symbolAt(checker, root); const exports = moduleSymbol.getExports(); - const renamed = exports.get("renamed")!; + const renamed = exports.get("renamed" as __String)!; check(checker.getAliasedSymbol(renamed) === answerSymbol && checker.getImmediateAliasedSymbol(renamed) === answerSymbol, "symbol alias targets"); check(checker.getMemberInModuleExports(moduleSymbol, "answer") === answerSymbol, "module export lookup"); check(checker.getExportsOfModule(moduleSymbol).some((symbol) => symbol === answerSymbol), "module exports identity"); diff --git a/tests/harness/__snapshots__/events-once-fences.ts.txt b/tests/harness/__snapshots__/events-once-fences.ts.txt index ffab538b0..60c499cfe 100644 --- a/tests/harness/__snapshots__/events-once-fences.ts.txt +++ b/tests/harness/__snapshots__/events-once-fences.ts.txt @@ -1,17 +1,26 @@ -events-once-fences.ts:8:27 - error SC2020: 'events.once with options' is part of the standard library types but has no scriptc lowering yet +events-once-fences.ts:10:27 - error SC2020: 'events.once options that are not an object literal' is part of the standard library types but has no scriptc lowering yet - 7 | async function main(): Promise { - 8 | await once(ee, "ready", { signal: AbortSignal.timeout(5) }); // options - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 9 | await once(ee, name); // computed event name + 9 | async function main(): Promise { + 10 | await once(ee, "ready", options); // options that are not an object literal + | ^~~~~~~ + 11 | await once(ee, name); // computed event name - hint: the lowered form is events.once(emitter, name); AbortSignal options are not supported yet + hint: pass the options as an object literal: { signal } -events-once-fences.ts:9:18 - error SC2020: 'events.once with a computed or meta event name' is part of the standard library types but has no scriptc lowering yet +events-once-fences.ts:11:18 - error SC2020: 'events.once with a computed or meta event name' is part of the standard library types but has no scriptc lowering yet - 8 | await once(ee, "ready", { signal: AbortSignal.timeout(5) }); // options - 9 | await once(ee, name); // computed event name + 10 | await once(ee, "ready", options); // options that are not an object literal + 11 | await once(ee, name); // computed event name | ^~~~ - 10 | } + 12 | await once(new PassThrough(), "data"); // a stream's 'data' event - hint: name the awaited event with a string literal \ No newline at end of file + hint: name the awaited event with a string literal + +events-once-fences.ts:12:33 - error SC2020: 'events.once of a stream's 'data' event' is part of the standard library types but has no scriptc lowering yet + + 11 | await once(ee, name); // computed event name + 12 | await once(new PassThrough(), "data"); // a stream's 'data' event + | ^~~~~~ + 13 | } + + hint: the chunk has no checked-dynamic boxing here; register stream.once('data', listener) instead \ No newline at end of file diff --git a/tests/harness/__snapshots__/retired-engine-2083-destructuring-island-decl.ts.txt b/tests/harness/__snapshots__/retired-engine-2083-destructuring-island-decl.ts.txt index 531002a4b..4bbff5a33 100644 --- a/tests/harness/__snapshots__/retired-engine-2083-destructuring-island-decl.ts.txt +++ b/tests/harness/__snapshots__/retired-engine-2083-destructuring-island-decl.ts.txt @@ -1,66 +1,3 @@ -retired-engine-2083-destructuring-island-decl.ts:9:5 - error SC2011: destructuring on 'any'-typed values cannot compile statically - - 8 | const a: any = [1, "two", true, 4, 5]; - 9 | var [x, , y, ...rest] = a; - | ^~~~~~~~~~~~~~~~~~~~~ - 10 | console.log(`${x} ${y} ${rest.length} ${rest[0]} ${rest[1]}`); - - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - -retired-engine-2083-destructuring-island-decl.ts:12:7 - error SC2011: destructuring on 'any'-typed values cannot compile statically - - 11 | const o: any = { p: 1, q: { r: "deep" } }; - 12 | const { p, q: { r } } = o; - | ^~~~~~~~~~~~~~~~~~~ - 13 | console.log(`${p} ${r}`); - - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - -retired-engine-2083-destructuring-island-decl.ts:14:7 - error SC2011: destructuring on 'any'-typed values cannot compile statically - - 13 | console.log(`${p} ${r}`); - 14 | const { missing = 42 } = o; - | ^~~~~~~~~~~~~~~~~~~~ - 15 | console.log(`${missing}`); - - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - -retired-engine-2083-destructuring-island-decl.ts:16:7 - error SC2011: destructuring on 'any'-typed values cannot compile statically - - 15 | console.log(`${missing}`); - 16 | const { p: p2 = 9, ...others } = o; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 17 | console.log(`${p2} ${others.q.r}`); - - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - -retired-engine-2083-destructuring-island-decl.ts:17:22 - error SC2004: uses of 'others' inherit the blocker on its declaration - - 16 | const { p: p2 = 9, ...others } = o; - 17 | console.log(`${p2} ${others.q.r}`); - | ^~~~~~~~~~ - 18 | var [] = [1, 2] as any; - - hint: the declaration of 'others' did not compile — fix the diagnostic reported there and these sites clear with it - -retired-engine-2083-destructuring-island-decl.ts:19:5 - error SC2011: destructuring on 'any'-typed values cannot compile statically - - 18 | var [] = [1, 2] as any; - 19 | var {} = o; - | ^~~~~~ - 20 | console.log("empties ok"); - - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - -retired-engine-2083-destructuring-island-decl.ts:22:11 - error SC2011: destructuring on 'any'-typed values cannot compile statically - - 21 | const u: any = undefined; - 22 | try { var {} = u; } catch (e) { console.log("obj-undef throws"); } - | ^~~~~~ - 23 | try { var [] = 0 as any; } catch (e) { console.log("arr-num throws"); } - - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - retired-engine-2083-destructuring-island-decl.ts:23:11 - error SC2011: destructuring on 'any'-typed values cannot compile statically 22 | try { var {} = u; } catch (e) { console.log("obj-undef throws"); } diff --git a/tests/harness/__snapshots__/retired-engine-2104-computed-key-destructuring.ts.txt b/tests/harness/__snapshots__/retired-engine-2104-computed-key-destructuring.ts.txt index b41cd4d10..415eb5895 100644 --- a/tests/harness/__snapshots__/retired-engine-2104-computed-key-destructuring.ts.txt +++ b/tests/harness/__snapshots__/retired-engine-2104-computed-key-destructuring.ts.txt @@ -1,12 +1,10 @@ -retired-engine-2104-computed-key-destructuring.ts:12:9 - error SC2011: destructuring on 'any'-typed values cannot compile statically +retired-engine-2104-computed-key-destructuring.ts:12:11 - error SC1031: destructuring with computed keys that do not fold to one property name is not supported yet 11 | function pick(key: string) { 12 | const { [key]: got } = src; - | ^~~~~~~~~~~~~~~~~~~~ + | ^~~~~~~~~~ 13 | return got; - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - retired-engine-2104-computed-key-destructuring.ts:13:10 - error SC2004: uses of 'got' inherit the blocker on its declaration 12 | const { [key]: got } = src; @@ -16,15 +14,13 @@ retired-engine-2104-computed-key-destructuring.ts:13:10 - error SC2004: uses of hint: the declaration of 'got' did not compile — fix the diagnostic reported there and these sites clear with it -retired-engine-2104-computed-key-destructuring.ts:17:9 - error SC2011: destructuring on 'any'-typed values cannot compile statically +retired-engine-2104-computed-key-destructuring.ts:17:11 - error SC1031: destructuring with computed keys that do not fold to one property name is not supported yet 16 | function pick2(key: string) { 17 | const { [key]: got = "dflt" } = src; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + | ^~~~~~~~~~~~~~~~~~~ 18 | return got; - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - retired-engine-2104-computed-key-destructuring.ts:18:10 - error SC2004: uses of 'got' inherit the blocker on its declaration 17 | const { [key]: got = "dflt" } = src; @@ -34,15 +30,6 @@ retired-engine-2104-computed-key-destructuring.ts:18:10 - error SC2004: uses of hint: the declaration of 'got' did not compile — fix the diagnostic reported there and these sites clear with it -retired-engine-2104-computed-key-destructuring.ts:23:7 - error SC2011: destructuring on 'any'-typed values cannot compile statically - - 22 | const key2 = "y"; - 23 | const { x: sx, [key2]: sy } = src; - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~ - 24 | console.log(`${sx} ${sy}`); - - hint: use a supported typed operation, or 'unknown' with a checked cast ('x as T') - retired-engine-2104-computed-key-destructuring.ts:30:38 - error SC2009: values of type '[any?]' cannot be compiled: the tuple shape is supported, but its element type 'any' does not compile 29 | let order = (n: any): any => { traceS += `${n},`; return n; }; diff --git a/tests/harness/__snapshots__/retired-engine-761-any-objects.ts.txt b/tests/harness/__snapshots__/retired-engine-761-any-objects.ts.txt deleted file mode 100644 index 008901f2d..000000000 --- a/tests/harness/__snapshots__/retired-engine-761-any-objects.ts.txt +++ /dev/null @@ -1,6 +0,0 @@ -retired-engine-761-any-objects.ts:13:38 - error SC1090: reading 'length' from a value of type 'any' is not supported yet - - 12 | const s: any = "hello world"; - 13 | console.log(`${s.toUpperCase()}`, `${s.split(" ").length}`, `${s.charAt(4)}`); - | ^~~~~~~~~~~~~~~~~~~ - 14 | const num: any = 3.14159; \ No newline at end of file diff --git a/tests/harness/ci-lanes.test.ts b/tests/harness/ci-lanes.test.ts index c0e74a934..faca8a7d0 100644 --- a/tests/harness/ci-lanes.test.ts +++ b/tests/harness/ci-lanes.test.ts @@ -1,7 +1,22 @@ -import { readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { load } from "js-yaml"; import { expect, test } from "vitest"; import { select } from "../../scripts/ci-lanes.mjs"; +import { darwinCorpusFiles, darwinFiles } from "../../scripts/ci-test-plan.mjs"; + +type Job = { + if?: string; + "runs-on"?: string; + strategy?: { matrix?: { include?: { runner?: string }[] } }; + steps: { id?: string; run?: string }[]; +}; +const workflow = load( + readFileSync(new URL("../../.github/workflows/ci.yml", import.meta.url), "utf8"), +) as { on: { workflow_dispatch: { inputs: Record } }; jobs: Record }; +const lanesScript = workflow.jobs["lanes"]!.steps.find((step) => step.id === "lanes")!.run!; type Selection = { on: Record; reasons: string[] }; const lanesOf = (files: string[], lines: { file: string; text: string }[] = []) => @@ -43,6 +58,16 @@ test.each([ ["tests/harness/ffi-frameworks.test.ts"], { linux: true, macos: true, effects: false, bootstrap: false }, ], + [ + "the kqueue event loop", + ["packages/runtime/src/scr_loop_kqueue.c"], + { linux: true, macos: true, effects: false, bootstrap: false }, + ], + [ + "the runtime's platform header", + ["packages/runtime/src/scr_platform.h"], + { linux: true, macos: true, effects: false, bootstrap: false }, + ], [ "the Effect harness", ["tests/harness/effect.test.ts"], @@ -91,15 +116,61 @@ test("a changed line that mentions a darwin-specific API runs the macOS lane", ( expect(lanesOf([file], [{ file, text: " struct kevent change;" }]).macos).toBe(true); }); +test("every file of the darwin suite runs the macOS lane", () => { + for (const file of [...darwinCorpusFiles, ...darwinFiles]) + expect(lanesOf([file]).macos, file).toBe(true); +}); + test("CI runs the base commit's lane selection against the pull request's diff", () => { - const workflow = load( - readFileSync(new URL("../../.github/workflows/ci.yml", import.meta.url), "utf8"), - ) as { jobs: Record }; - const script = workflow.jobs["lanes"]!.steps.find((step) => step.id === "lanes")!.run!; - expect(script).toContain('git show "$BASE:scripts/ci-lanes.mjs"'); - expect(script).toContain( + expect(lanesScript).toContain('git show "$BASE:scripts/ci-lanes.mjs"'); + expect(lanesScript).toContain( 'node "$RUNNER_TEMP/ci-lanes.mjs" --base "$BASE" --head "$HEAD" --github-output', ); - // Pushes and the nightly sweep, and a base without the script, run every lane. - expect(script.match(/every$/gm)).toHaveLength(2); + // The nightly sweep and manual runs, and a base without the script, run every lane. + expect(lanesScript.match(/every$/gm)).toHaveLength(2); +}); + +function runLanes(event: string) { + const directory = mkdtempSync(join(tmpdir(), "ci-lanes-")); + try { + const output = join(directory, "output"); + writeFileSync(output, ""); + const result = spawnSync("bash", ["-e", "-o", "pipefail", "-c", lanesScript], { + encoding: "utf8", + env: { ...process.env, EVENT: event, GITHUB_OUTPUT: output }, + }); + expect(result.status, result.stderr).toBe(0); + return readFileSync(output, "utf8"); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +} + +test("pushes to main run every lane except macos; the nightly sweep and manual runs run every lane", () => { + expect(runLanes("push")).toBe("linux=true\nmacos=false\neffects=true\nbootstrap=true\n"); + for (const event of ["schedule", "workflow_dispatch"]) + expect(runLanes(event), event).toBe("linux=true\nmacos=true\neffects=true\nbootstrap=true\n"); + expect(workflow.on.workflow_dispatch.inputs["macos_suite"]).toEqual({ + description: "The test suite to run on macOS", + type: "choice", + options: ["full", "darwin"], + default: "full", + }); +}); + +test("every job on a macOS runner is in the macos lane", () => { + const macosJobs = Object.entries(workflow.jobs).filter(([, job]) => + [job["runs-on"], ...(job.strategy?.matrix?.include ?? []).map((entry) => entry.runner)].some( + (runner) => runner?.startsWith("macos"), + ), + ); + expect(macosJobs.map(([name]) => name).sort()).toEqual([ + "llvm_artifacts_macos_arm64", + "llvm_artifacts_macos_x64", + "mobile_runtime_packs_ios", + "native_macos", + "tests_macos", + ]); + for (const [name, job] of macosJobs) + expect(job.if, name).toContain("needs.lanes.outputs.macos == 'true'"); }); diff --git a/tests/harness/ci-test-plan.test.ts b/tests/harness/ci-test-plan.test.ts index 2343407df..34d61346f 100644 --- a/tests/harness/ci-test-plan.test.ts +++ b/tests/harness/ci-test-plan.test.ts @@ -201,12 +201,12 @@ test.for([1, 2, 3])("the darwin suite retains every darwin file with %i shards", /** Expands the ${{ }} expressions of a workflow value for one event: string * literals, ==, !=, !, &&, || and fromJSON, with matrix.shard left as * "{shard}". A value that is a single expression returns its result. */ -function expand(value: string, context: { event: string; macosFull: boolean | null }): unknown { +function expand(value: string, context: { event: string; macosSuite: string | null }): unknown { const evaluate = (expression: string): unknown => new Function( `return (${expression .replaceAll("github.event_name", JSON.stringify(context.event)) - .replaceAll("inputs.macos_full", JSON.stringify(context.macosFull)) + .replaceAll("inputs.macos_suite", JSON.stringify(context.macosSuite)) .replaceAll("fromJSON", "JSON.parse")});`, )(); const single = /^\$\{\{(.*)\}\}$/s.exec(value); @@ -240,18 +240,17 @@ test("pull requests run the full suite on Linux and the darwin suite on macOS", ).toBe("native-linux-x64-gnu"); expect(macos["runs-on"]).toBe("macos-15"); expect(macos.needs).toContain("native_macos"); - // Pull requests, pushes to main and plain manual runs run the darwin suite in - // three slices; the nightly sweep and manual runs with macos_full run the + // Pull requests and manual runs with macos_suite set to darwin run the darwin + // suite in three slices; the nightly sweep and other manual runs run the // complete suite in ten. - for (const [event, macosFull, full] of [ + for (const [event, macosSuite, full] of [ ["pull_request", null, false], - ["push", null, false], - ["workflow_dispatch", false, false], - ["workflow_dispatch", true, true], + ["workflow_dispatch", "darwin", false], + ["workflow_dispatch", "full", true], ["schedule", null, true], ] as const) { - const context = { event, macosFull }; - const label = `${event} ${macosFull}`; + const context = { event, macosSuite }; + const label = `${event} ${macosSuite}`; expect(expand(String(macos.strategy.matrix.shard), context), label).toEqual( full ? [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] : [1, 2, 3], ); diff --git a/tests/harness/corpus-costs/5819-node-stream-dyn-direct-pipe/main.ts.json b/tests/harness/corpus-costs/5819-node-stream-dyn-direct-pipe/main.ts.json new file mode 100644 index 000000000..12637a176 --- /dev/null +++ b/tests/harness/corpus-costs/5819-node-stream-dyn-direct-pipe/main.ts.json @@ -0,0 +1 @@ +{ "plain": 4912, "san": 12058 } diff --git a/tests/harness/corpus-costs/5820-node-stream-dyn-constructor-overrides/main.ts.json b/tests/harness/corpus-costs/5820-node-stream-dyn-constructor-overrides/main.ts.json new file mode 100644 index 000000000..f8ccf0699 --- /dev/null +++ b/tests/harness/corpus-costs/5820-node-stream-dyn-constructor-overrides/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3490, "san": 8050 } diff --git a/tests/harness/corpus-costs/6470-any-destructuring.ts.json b/tests/harness/corpus-costs/6470-any-destructuring.ts.json new file mode 100644 index 000000000..101a112d4 --- /dev/null +++ b/tests/harness/corpus-costs/6470-any-destructuring.ts.json @@ -0,0 +1 @@ +{ "plain": 2678, "san": 3175 } diff --git a/tests/harness/corpus-costs/6471-any-method-chains.ts.json b/tests/harness/corpus-costs/6471-any-method-chains.ts.json new file mode 100644 index 000000000..13db76f84 --- /dev/null +++ b/tests/harness/corpus-costs/6471-any-method-chains.ts.json @@ -0,0 +1 @@ +{ "plain": 1526, "san": 1821 } diff --git a/tests/harness/corpus-costs/6472-namespace-destructuring/main.ts.json b/tests/harness/corpus-costs/6472-namespace-destructuring/main.ts.json new file mode 100644 index 000000000..a296c0da0 --- /dev/null +++ b/tests/harness/corpus-costs/6472-namespace-destructuring/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1728, "san": 1731 } diff --git a/tests/harness/corpus-costs/6473-any-parameter-values.ts.json b/tests/harness/corpus-costs/6473-any-parameter-values.ts.json new file mode 100644 index 000000000..cb6a6fb42 --- /dev/null +++ b/tests/harness/corpus-costs/6473-any-parameter-values.ts.json @@ -0,0 +1 @@ +{ "plain": 1588, "san": 1779 } diff --git a/tests/harness/corpus-costs/6474-async-any-callbacks/main.ts.json b/tests/harness/corpus-costs/6474-async-any-callbacks/main.ts.json new file mode 100644 index 000000000..7c0aa7365 --- /dev/null +++ b/tests/harness/corpus-costs/6474-async-any-callbacks/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1691, "san": 1787 } diff --git a/tests/harness/corpus-costs/6475-js-promise-resolve-adoption/main.ts.json b/tests/harness/corpus-costs/6475-js-promise-resolve-adoption/main.ts.json new file mode 100644 index 000000000..e1cfd6357 --- /dev/null +++ b/tests/harness/corpus-costs/6475-js-promise-resolve-adoption/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1592, "san": 1653 } diff --git a/tests/harness/corpus-costs/6490-js-named-promise-executor/main.ts.json b/tests/harness/corpus-costs/6490-js-named-promise-executor/main.ts.json new file mode 100644 index 000000000..f5d08389a --- /dev/null +++ b/tests/harness/corpus-costs/6490-js-named-promise-executor/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1658, "san": 1832 } diff --git a/tests/harness/corpus-costs/6491-catch-any-bindings.ts.json b/tests/harness/corpus-costs/6491-catch-any-bindings.ts.json new file mode 100644 index 000000000..644275755 --- /dev/null +++ b/tests/harness/corpus-costs/6491-catch-any-bindings.ts.json @@ -0,0 +1 @@ +{ "plain": 1509, "san": 1704 } diff --git a/tests/harness/corpus-costs/6564-constructor-function-stream-late-hooks.cjs.json b/tests/harness/corpus-costs/6564-constructor-function-stream-late-hooks.cjs.json new file mode 100644 index 000000000..e873baf78 --- /dev/null +++ b/tests/harness/corpus-costs/6564-constructor-function-stream-late-hooks.cjs.json @@ -0,0 +1 @@ +{ "plain": 2750, "san": 3200 } diff --git a/tests/harness/corpus-costs/761-any-objects.ts.json b/tests/harness/corpus-costs/761-any-objects.ts.json new file mode 100644 index 000000000..50113af31 --- /dev/null +++ b/tests/harness/corpus-costs/761-any-objects.ts.json @@ -0,0 +1 @@ +{ "plain": 1524, "san": 1822 } diff --git a/tests/harness/corpus-costs/class-id-instances-as-streams.ts.json b/tests/harness/corpus-costs/class-id-instances-as-streams.ts.json new file mode 100644 index 000000000..07189e390 --- /dev/null +++ b/tests/harness/corpus-costs/class-id-instances-as-streams.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 4500 } diff --git a/tests/harness/dyncheck.test.ts b/tests/harness/dyncheck.test.ts index 2ab323f37..2633cc815 100644 --- a/tests/harness/dyncheck.test.ts +++ b/tests/harness/dyncheck.test.ts @@ -159,7 +159,8 @@ console.log("unreachable", c.host); test("schema-directed JSON.parse declines to the checked route with identical errors", async () => { // `JSON.parse(text) as T` for plain record/array targets parses in one // pass and reruns the checked-dynamic route whenever it declines, so - // syntax, depth, and checked-cast failures keep their exact messages. + // syntax and depth failures keep their exact messages, and a value the + // check rejects continues as its parsed value (checked-cast fallback). const r = await compileAndRun( "schema-parse-errors", `interface Item { sku: string; qty: number } @@ -197,9 +198,9 @@ for (const text of inputs) { 'SyntaxError: Unexpected token \']\', ...","x":[1,],"qty":"... is not valid JSON (same)', "SyntaxError: Bad Unicode escape in JSON at position 21 (same)", "RangeError: Maximum call stack size exceeded (same)", - "TypeError: expected number at $[0].qty, got undefined (plain ok)", - "TypeError: expected number at $[1].qty, got boolean (plain ok)", - "TypeError: expected number at $[0].qty, got string (plain ok)", + "ok 1 undefined (plain ok)", + "ok 2 1 (plain ok)", + "ok 1 x (plain ok)", "ok 1 2 (plain ok)", "", ]); @@ -211,7 +212,9 @@ for (const text of inputs) { // string arrays; null for object and number; missing fields; object vs // array at a field and at the root; duplicate keys; truncated JSON; 1e400 // and a lone surrogate; extra nested members; reordered keys; and an Item[] - // root. Every line equals the perf/harness (checked-route-only) output. + // root. A value the check rejects continues as its parsed value, so every + // line but the truncated input's (whose SyntaxError text is approximate) + // equals Node's. const r = await compileAndRun( "schema-parse-wrong-types", `interface Item { sku: string; qty: number; ok: boolean } @@ -255,23 +258,23 @@ try { expect(r.stderr).toBe(""); expect(r.stdout.split("\n")).toEqual([ "ok 1 a 1 x:2:true t", - "err TypeError: expected number at $.id, got string", - "err TypeError: expected string at $.name, got number", - "err TypeError: expected number at $.items[0].qty, got string", - "err TypeError: expected boolean at $.items[0].ok, got undefined", - "err TypeError: expected boolean at $.items[0].ok, got number", - "err TypeError: expected array at $.items, got object", - "err TypeError: expected object at $.items[0], got null", - "err TypeError: expected string at $.tags[0], got number", + "ok 1 a 0 ", + "ok 1 2 0 ", + "ok 1 a 1 x:2:true ", + "ok 1 a 1 x:2:undefined ", + "ok 1 a 1 x:2:1 ", + "err TypeError: o.items.map is not a function", + "err TypeError: Cannot read properties of null (reading 'sku')", + "ok 1 a 0 1", "ok 1 a 0 ", "ok 2 a 0 ", - "err TypeError: expected number at $.id, got undefined", - "err TypeError: expected number at $.id, got null", - "err TypeError: expected object at $, got array", + "ok undefined a 0 ", + "ok null a 0 ", + "err TypeError: Cannot read properties of undefined (reading 'length')", "err SyntaxError: Unexpected end of JSON input", "ok Infinity \uFFFD 0 ", "ok 7 reordered 0 ", - "err expected number at $[1].qty, got string", + "ok 2", "", ]); }); @@ -305,6 +308,46 @@ console.log("unreachable", c.host); ); }); + test("optional record member: the failure names the deepest path inside it", async () => { + // `Partial` makes every member an `X | undefined` union. A present + // object that fails X used to report only the union's own path + // ("expected object | undefined at $.order, got object"); the record + // arm's builder now reports where inside it the value fails. + const r = await compileAndRun( + "optional-record-deep", + `interface Order { + owner: { id: string; plan: { tier: string } }; + total: number; +} +interface Envelope { order: Order; tags?: string[] } +const texts = [ + '{"order":{"owner":{"id":"u1"},"total":3}}', + '{"order":{"owner":{"id":7,"plan":{"tier":"pro"}},"total":3}}', + '{"order":{"owner":{"id":"u1","plan":{"tier":"pro"}},"total":3},"tags":["a",2]}', + '{"order":5}', +]; +for (const text of texts) { + try { + const e = JSON.parse(text) as Partial; + console.log("ok", e.order?.owner.id); + } catch (err) { + if (err instanceof TypeError) console.log(err.message); + } +} +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stdout).toBe( + [ + "expected object at $.order.owner.plan, got undefined", + "expected string at $.order.owner.id, got number", + "expected string at $.tags[1], got number", + "expected object | undefined at $.order, got number", + "", + ].join("\n"), + ); + }); + test("JSON null matches nothing", async () => { const r = await compileAndRun( "null-value", @@ -468,7 +511,60 @@ console.log("unreachable", t[1]); expect(r.stderr).toContain("Uncaught TypeError: expected array at $, got object"); }); + test("checked-cast fallback: a failed value leaving its region is checked where it leaves", async () => { + // A block-local `JSON.parse(text) as T` whose value fails validation + // continues as its parsed value (checked-cast fallback): reads in the + // binding's region behave as in Node. Where the value flows into a typed + // slot outside that region (a typed parameter, a typed outer binding, a + // typed return), it is checked there: a value that fits continues, one + // that does not throws that conversion's TypeError at that use, where + // Node continues. Module-level bindings keep the check at the cast. + const r = await compileAndRun( + "fallback-escapes", + `interface Order { id: string; deployment: { id: string; plan: string } } +function deliver(o: Order): string { return o.deployment.id + "/" + o.deployment.plan; } +function plan(text: string): string { + const order = JSON.parse(text) as Order; + console.log("read", order.deployment?.id, order.deployment?.plan); + const id: string = order.id; + return id; +} +function handle(text: string): void { + const order = JSON.parse(text) as Order; + console.log("id", order.id); + try { console.log(deliver(order)); } catch (e) { console.log((e as Error).message); } +} +handle('{"id":"a","deployment":{"id":"d","plan":"pro"},"extra":1}'); +handle('{"id":"b","deployment":{"id":"d"}}'); +console.log(plan('{"id":"c","deployment":{}}')); +try { console.log(plan('{"id":7,"deployment":{"id":"d","plan":"p"}}')); } +catch (e) { console.log((e as Error).message); } +let top: Order; +try { top = JSON.parse('{"id":"t"}') as Order; console.log(top.id); } +catch (e) { console.log((e as Error).message); } +`, + ); + expect(r.exitCode).toBe(0); + expect(r.stderr).toBe(""); + expect(r.stdout).toBe( + [ + "id a", + "d/pro", + "id b", + "expected string at $.deployment.plan, got undefined", + "read undefined undefined", + "c", + "read d p", + "expected string at $, got number", + "expected object at $.deployment, got undefined", + "", + ].join("\n"), + ); + }); + test("tuple failure-path RC stress: partial tuples release on unwind", async () => { + // The rejected value continues as its parsed value (checked-cast + // fallback); the partial tuple the check built still releases. const r = await compileAndRun( "tuple-rc-failure", `let recovered = 0; @@ -482,14 +578,18 @@ for (let i = 0; i < 100; i = i + 1) { } console.log(recovered); `, + "ts", + true, ); expect(r.exitCode).toBe(0); - expect(r.stdout).toBe("100\n"); + expect(r.stdout).toBe("-200\n"); }); test("failure-path RC stress: repeated mismatches through catch stay clean", async () => { // Meaningful mostly under SCRIPTC_SAN=1 (ASan + RC audit at exit): every - // iteration builds a partial record/array that must release on unwind. + // iteration builds a partial record/array that must release when the + // check rejects it, and the parsed value continues (checked-cast + // fallback), so `e2.v` reads "no" and the count becomes NaN as in Node. const r = await compileAndRun( "rc-failure-stress", `type Entry = { tags: string[]; v: number }; @@ -510,9 +610,11 @@ for (let i = 0; i < 100; i = i + 1) { } console.log(recovered); `, + "ts", + true, ); expect(r.exitCode).toBe(0); - expect(r.stdout).toBe("200\n"); + expect(r.stdout).toBe("NaN\n"); }); test("index-signature shapes CAPTURE extras — and a wrong-typed extra throws with its key path", async () => { @@ -685,13 +787,19 @@ console.log(total); // The static-any lane's exit stance (the checked cast's rule applied // to implicit any→typed flows): Node assigns the mismatched value and // carries on; scriptc throws the catchable TypeError instead of - // returning a wrong result — the divergence is deliberate and documented. + // returning a wrong result where the value leaves the binding it was + // checked into. A block-local binding read only after the check keeps + // the value instead (checked-cast fallback), as Node does. const r = await compileAndRun( "any-lying-exit", `const a: any = "not a number"; +function width(n: number): number { + return n * 2; +} try { const n: number = a; - console.log("unreachable", n); + console.log("kept", n, typeof n); + console.log(width(n)); } catch (e) { console.log((e as Error).name); } @@ -699,7 +807,7 @@ console.log("recovered"); `, ); expect(r.exitCode).toBe(0); - expect(r.stdout).toBe("TypeError\nrecovered\n"); + expect(r.stdout).toBe("kept not a number string\nTypeError\nrecovered\n"); }); test("a lying cast into a union that cannot represent the value throws the stranded trap catchably", async () => { diff --git a/tests/harness/inheritance.test.ts b/tests/harness/inheritance.test.ts index 27a176764..df79a6235 100644 --- a/tests/harness/inheritance.test.ts +++ b/tests/harness/inheritance.test.ts @@ -100,7 +100,7 @@ console.log(a.id(), a.speak(), b.speak()); // base-typed call site dispatches through it: both classes are // constructed, so the loaded slot selects a direct call of each. expect(llvm).toMatch(/%sc_vtt_Animal = type \{ %ScrVt, ptr \}/); - expect(llvm).toMatch(/@sc_vtable_Dog = [^\n]*ptr @sc_b?f__x25_Dog_speak\b/); + expect(llvm).toMatch(/%sc_vtt_Animal \{[^\n]*ptr @sc_b?f__x25_Dog_speak\b[^\n]*; class Dog\n/); expect(llvm).toMatch( /getelementptr inbounds %sc_vtt_Animal, ptr %\w+, i64 0, i32 1[\s\S]*icmp eq ptr %\w+, @sc_b?f__x25_(?:Animal|Dog)_speak\n/, ); @@ -168,7 +168,9 @@ console.log(c.v, c.label, plain.label); ); // The overridden getter dispatches through its slot... expect(llvm).toMatch(/%sc_vtt_Cell = type \{ %ScrVt, ptr \}/); - expect(llvm).toMatch(/@sc_vtable_UpperCell = [^\n]*ptr @sc_b?f__x25_UpperCell_get_x3a_label\b/); + expect(llvm).toMatch( + /%sc_vtt_Cell \{[^\n]*ptr @sc_b?f__x25_UpperCell_get_x3a_label\b[^\n]*; class UpperCell\n/, + ); expect(llvm).toMatch( /getelementptr inbounds %sc_vtt_Cell, ptr %\w+, i64 0, i32 1[\s\S]*icmp eq ptr %\w+, @sc_b?f__x25_(?:Cell|UpperCell)_get_x3a_label\n/, ); @@ -265,7 +267,7 @@ console.log(b.v, plain.v); // dispatch dynamically here. expect(llvm).toMatch(/%sc_vtt_Box = type \{ %ScrVt, ptr, ptr \}/); expect(llvm).toMatch( - /@sc_vtable_SealedBox = [^\n]*ptr @sc_b?f__x25_SealedBox_get_x3a_v(\.virtual)?, ptr @sc_b?f__x25_SealedBox_set_x3a_v\b/, + /%sc_vtt_Box \{[^\n]*ptr @sc_b?f__x25_SealedBox_get_x3a_v(\.virtual)?, ptr @sc_b?f__x25_SealedBox_set_x3a_v\b[^\n]*; class SealedBox\n/, ); expect(llvm).toMatch(/getelementptr inbounds %sc_vtt_Box, ptr %\w+, i64 0, i32 2/); expect(llvm).toMatch(/sc_f__x25_SealedBox_set_x3a_v/); diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index 168fdb6bd..f5f3fdeeb 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -91,13 +91,13 @@ const KNOWN: Record = { "node24-web-stream-encoding": { run: "SC1090" }, "node24-web-stream-errors": { run: "SC1090" }, "node24-web-stream-lifecycle": { run: "SC1090" }, - "node24-web-stream-pipe-abort": { run: "SC1090" }, + "node24-web-stream-pipe-abort": { run: "SC2020" }, "node24-web-stream-pipe-errors": { run: "SC1090" }, "node24-web-stream-pipes": { run: "SC1090" }, "node24-web-stream-queues": { run: "SC1090" }, - "node24-web-stream-readable-lifecycle": { run: "SC1090" }, + "node24-web-stream-readable-lifecycle": { run: "SC2020" }, "node24-web-stream-readers": { run: "SC1090" }, - "node24-web-stream-tee": { run: "SC1090" }, + "node24-web-stream-tee": { run: "SC2020" }, "node24-web-stream-transforms": { run: "SC1090" }, "package-imports": { build: "SC2013" }, "promise-bridge": { build: "SC1090" }, diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index 1e1905368..2b705edae 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -205,6 +205,24 @@ test("node-types: events.once over typed EventEmitter listeners under @types/nod expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: events.once with a signal over streams, raced, under @types/node", async () => { + const outDir = outDirFor("events-once-signal-race"); + const entry = join(nodeTypesDir, "events-once-signal-race.ts"); + const result = await compile(entry, { + outPath: join(outDir, "events-once-signal-race"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: socket.address and resetAndDestroy under @types/node", async () => { const outDir = outDirFor("net-reset-address"); const entry = join(nodeTypesDir, "net-reset-address.ts"); @@ -1175,3 +1193,21 @@ test("node-types: terminal properties and filesystem error metadata match Node", expect(native.stdout).toBe(node.stdout); expect(comparableStderr(native.stderr)).toBe(node.stderr); }); + +test("node-types: process.versions entries @types/node declares as strings read undefined when absent", async () => { + const outDir = outDirFor("process-versions-absent"); + const entry = join(nodeTypesDir, "process-versions-absent.ts"); + const result = await compile(entry, { + outPath: join(outDir, "process-versions-absent"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); diff --git a/tests/harness/self-hosting-emission-cases.ts b/tests/harness/self-hosting-emission-cases.ts index cf3babf6f..efba5e019 100644 --- a/tests/harness/self-hosting-emission-cases.ts +++ b/tests/harness/self-hosting-emission-cases.ts @@ -243,7 +243,11 @@ export function emissionCases(): EmissionCase[] { const forest = structuredClone(hierarchy); forest.classes!.push(cls("Unrelated"), cls("Leaf", "Child")); forest.classes![3]!.fields = [...child.fields]; - add("class forest numbering", forest, ["class Leaf (vt at 1)", "class Unrelated { value }"]); + add("class forest numbering", forest, [ + "class Leaf (class id in rc)", + "class Unrelated { value }", + "@sc_vta_Base", + ]); const abstract = emissionModule(); const abstractRoot = cls("Abstract"); diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index f924845f1..83cc0d2fd 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -31,6 +31,7 @@ const features: NativeLinkFeatures = { searchParams: false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false, diff --git a/tests/harness/self-hosting-runtime-features.test.ts b/tests/harness/self-hosting-runtime-features.test.ts index 8d65d72cd..b2f7e3062 100644 --- a/tests/harness/self-hosting-runtime-features.test.ts +++ b/tests/harness/self-hosting-runtime-features.test.ts @@ -51,6 +51,7 @@ const featureCalls: readonly [string, IrLibFn][] = [ ["searchParams", "sp.new"], ["qs", "qs.parse"], ["dynBuiltinClass", "dyn.builtinClass"], + ["dynBuiltinStream", "dyn.builtinStreamClass"], ["parseArgs", "util.parseArgs"], ["parseArgs", "util.isDeepStrictEqual"], ["parseArgs", "util.styleText"], diff --git a/tests/harness/self-hosting-runtime-pack.test.ts b/tests/harness/self-hosting-runtime-pack.test.ts index f8a280ef4..2b9e51969 100644 --- a/tests/harness/self-hosting-runtime-pack.test.ts +++ b/tests/harness/self-hosting-runtime-pack.test.ts @@ -32,6 +32,7 @@ const base: NativeLinkFeatures = { searchParams: false, qs: false, dynBuiltinClass: false, + dynBuiltinStream: false, parseArgs: false, stream: false, net: false,