Merge main

This commit is contained in:
Chris Tate
2026-10-20 18:21:35 -05:00
471 changed files with 14078 additions and 2596 deletions
+46 -28
View File
@@ -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."
+6
View File
@@ -219,6 +219,12 @@ jobs:
Move-Item "$expanded\clang+llvm-22.1.8-x86_64-pc-windows-msvc" $llvm
echo "LLVM_DIR=$llvm\lib\cmake\llvm" >> $env:GITHUB_ENV
echo "$llvm\bin" >> $env:GITHUB_PATH
# A musl CLI ships tsc-ts's musl executable, and these jobs run on glibc
# runners, where pnpm installs only the glibc platform packages.
- name: Let pnpm install musl platform packages
if: endsWith(matrix.cli_target, '-musl')
run: |
printf 'supportedArchitectures:\n libc: [current, musl]\n' >> pnpm-workspace.yaml
- run: pnpm install --frozen-lockfile
- name: Build native packages (POSIX)
if: matrix.platform != 'windows-x64'
+67 -2
View File
@@ -6,6 +6,73 @@ All notable changes to scriptc will be documented in this file.
<!-- release:start -->
## 0.4.0
### Breaking changes
- **tsc-ts is the default TypeScript 7 checker** on Linux and macOS. scriptc installs it with the compiler (`tsc-ts` 0.0.5 and its platform package for the host). Programs get the same diagnostics and generated code as with tsgo, and compiling takes less checker memory. `SCRIPTC_CHECKER=tsgo` selects tsgo, which stays the default on Windows. A build cache entry made with one checker isn't reused with the other.
- **`--npm-static=auto`, the default, compiles more packages:** packages typed by their DefinitelyTyped twin (`@types/<name>`), and untyped dependencies that another package's JavaScript imports. A program whose dependencies were refused before can now compile them, and a twin whose declarations disagree with the package's JavaScript fails the build at the use. Use `--npm-static <pkg,…>` to name the packages to compile.
- **Program type errors are reported as type errors** instead of as refusals of the npm package that contains them.
### Highlights
- **WebAssembly:** builds translate every WebAssembly module whose bytes they can see into native code, and `WebAssembly.compile`, `instantiate`, `validate`, `new WebAssembly.Module` and `new WebAssembly.Instance` run them. There is no interpreter. `WebAssembly.Memory`, `Table` and `Global`, `Module.imports`, `Module.exports` and `Module.customSections`, and function references from tables are supported, and modules can be sent to workers. `scriptc coverage` lists the modules a program loads, with `--wasm <file.wasm>` for modules loaded from paths the sources don't spell out.
- **Async functions that suspend only at their `await`s run as stackless coroutines** instead of on fibers with their own stack, so a suspended call keeps only the values live across its awaits.
- **Discriminated unions of records are the record pointer itself,** with no box around each value.
- **The dynamic tier computes with NaN-boxed values:** numbers, booleans, `undefined` and `null` between operators no longer allocate. `scriptc coverage --explain-dynamic` lists the functions that run in the dynamic tier and why, and `--tier=static-only` keeps a program fully static.
- **npm packages:** esbuild's bundled CommonJS chunks, CommonJS cycles and lazily initialized ES modules compile statically, as do classic CommonJS packages (late and JSON `require`s, require cycles), class hierarchies across a package's modules, and the packages unblocked by type-only imports, `@types` packages, `punycode` and `constants`. chalk 5 and the jose root entry compile.
### Features
- **`@scriptc/experimental` adds `ThreadSlots`,** one value per key and thread on shared objects, read without a lock.
- **`node:test` adds `mock.timers`, `getTestContext`, `t.assert.snapshot` and `t.assert.fileSnapshot`,** the `TestContext` values `filePath`, `attempt`, `passed`, `workerId` and `tags`, module-level `before` and `after` hooks, the `SuiteContext` and hook contexts, and unnamed `test` and `describe` calls. A timed-out test cancels its running subtests.
- **`node:worker_threads` adds `BroadcastChannel`,** `setEnvironmentData`/`getEnvironmentData`, `receiveMessageOnPort`, `markAsUntransferable`, `markAsUncloneable`, `threadName`, `resourceLimits.stackSizeMb`, the `stdout`/`stderr`/`stdin` options and streams, and `worker[Symbol.asyncDispose]`.
- **`node:net` adds `SocketAddress` and `BlockList`,** and `net.connect` resolves host names.
- **`node:http` pools keep-alive sockets in `Agent`s,** uses `globalAgent` for default requests, reads `http.request` options at run time, and speaks HTTP over Unix domain sockets. `fetch` tunnels through `HTTP_PROXY`/`HTTPS_PROXY` with `CONNECT`.
- **`DisposableStack`, `Float16Array`, `Error.isError`, `Promise.try` and the regular expression `v` flag** are supported.
- **Iterator helpers** run on generators, `Map` and `Set` iterators, string and typed array iterators, `Iterator.from` and untyped iterators.
- **`Object.groupBy` and `Map.groupBy`** group Sets, Maps and generators.
- **`v8`, `stream/web`, `dns/promises`, `EvalError` and `graceful-fs`** are available, and `Temporal` is reported as absent, as in Node 24.
- **`util.promisify`** works over the crypto callback functions, dns lookups, `child_process.exec`'s callback forms and callback-style functions.
- **`events.once`** takes an `AbortSignal` and works over streams.
- **`AsyncLocalStorage` stores** carry into timer, immediate, `process.nextTick` and microtask callbacks.
- **JavaScript classes compile like TypeScript classes** when their layout or JSDoc types allow it.
- **Classes that extend `EventEmitter` or a stream class can be used as values,** and so can `EventEmitter`, the stream classes and the error classes themselves. Constructor functions can inherit from them.
- **Class instances can be frozen and their fields redefined.**
- **Method overrides may differ from the method they override** in parameter and result types, as long as each value converts.
- **Library profiles add the `cstring` class.**
- **An experimental young-object nursery** for compiled classes and records is available at compile time with `SCRIPTC_YOUNG_NURSERY=1`.
### Performance
- **Dense arrays have head room,** so `shift`, `splice(0, n)` and `unshift` runs cost amortized O(1), and `pop` of a packed array is inline.
- **Index loops over arrays and typed arrays are versioned on one bounds guard,** with stable lengths proven and typed-array memory tagged for alias analysis.
- **Retain and release pairs are removed** through private-array precision and last-use moves, borrowed string slices go into stack buffers, and records, tuples and spread updates stay in frame storage.
- **SHA-384 and SHA-512 use AVX2, AVX-512VL or the arm64 SHA-512 instructions,** and MD5's block function is unrolled.
- **String character tests, optional-string `switch`es, `String.fromCharCode` and long-string `length` are faster,** and `Buffer.write` widens ASCII to UTF-16 sixteen bytes per step.
- **The cycle collector walks dead cycles once,** frees large dead cycle sets in address order and no longer creates self-capture cycles on the release fast path.
- **Workers return their memory** while the event loop waits and when they finish, and default-region memory is committed as it is used.
- **Lifetime regions carry across `await`,** and freed region chunks are reused.
- **Large closure environments compile in linear time,** and two frontend scans no longer repeat.
- **Programs keep stack frames only when they can read `stack`.** Before, a computed-key read on a dynamic value kept frames in every function; tsc-ts's own check is back to 0.3.6's speed.
- **The child-process runtime unit links only into programs that spawn.**
### Fixes
- **Node 24 parity:** `util.format`, `util.types` and `util.inspect`, `URLSearchParams`, `querystring` (null-prototype results), `string_decoder`, `os`, `events` and `timers/promises` match Node 24. Class instances, iterators, iterator helper objects and holes in arrays print as Node prints them, and `%d`/`%i` format dynamic arguments as Node does.
- **Silent wrong output is fixed** for object identity, `Object.prototype` values, `Date.parse` (V8's date grammar), stored builtins, constructor returns, `arr && arr[0]`, ES5 class names, builtin function values, uninitialized `let` writes, module-export `this` and `1 ** Infinity`.
- **Strings compare in UTF-16 code-unit order.**
- **Dates format with Node's local time zone names,** convert to local time once for their local getters, and follow `TZ` changes. `Date.prototype.toJSON` gives `null` for invalid dates.
- **Errors:** thrown Maps, Sets, typed arrays, functions and arrays keep their identity. Errors list their own name where they were created, errors held as `any` have their own `stack` accessor and convert with `Error.prototype.toString`, and errors that cross into dynamic code carry Node's error properties.
- **`matchAll` rows and `exec` results have `index`, `input` and `groups`,** and named groups keep their null prototype through optional chains.
- **Unhandled rejections are reported after each timer and immediate callback,** and async generator completions settle on Node's microtask turns.
- **Missing `Record` and index-signature keys read as `undefined`,** and generic functions that read a missing element return `undefined`.
- **Sockets:** `'data'` listeners get strings after `setEncoding`, socket and stream chunks are Buffers, and an idle pooled http socket that receives data is destroyed.
- **Diagnostic code frames window long source lines.**
- **npm crashes, internal errors and false TDZ errors** found while compiling popular packages are fixed.
<!-- release:end -->
## 0.3.6
### Features
@@ -38,8 +105,6 @@ All notable changes to scriptc will be documented in this file.
- **`Object.setPrototypeOf` returns primitive receivers,** and `createHmac` without a key throws Node's error.
- **Boxed records that hold an async function link the dynamic promise unit.**
<!-- release:end -->
## 0.3.5
### Features
+1
View File
@@ -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 |
+7
View File
@@ -231,6 +231,13 @@
"4"
]
},
{
"name": "unicode-lexer",
"entry": "workloads/unicode-lexer.ts",
"args": [
"1"
]
},
{
"name": "log-summary",
"entry": "../builds/log-summary/main.ts",
@@ -0,0 +1,116 @@
// A lexer over a large bundled module whose comments and strings are in
// several languages: line starts, tokens by kind and comment text, read
// with charCodeAt as hand-written scanners do. Each pass relexes the whole
// text, as a bundler or an editor does after a change.
const comments = [
"// Berechnet die Größe des Puffers für übergroße Einträge",
"// Calcule la taille maximale avant la prochaine itération",
"// バッファのサイズを計算します",
"/* 计算缓冲区大小,然后返回结果 */",
"// Returns the cached value (fast path) — see the note above",
"/** Résumé: the état of the café's ñandú registry */",
];
const strings = ['"Grüße"', '"naïve façade"', '"東京"', '"ok ✓"', '"plain ascii"', "'mañana'"];
const names = ["buffer", "größe", "index", "resultat", "count", "value", "entry", "table"];
let seed = 11;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
function pick(list: readonly string[]): string {
return list[Math.floor(random() * list.length)]!;
}
function makeSource(lines: number): string {
const out: string[] = [];
for (let i = 0; i < lines; i++) {
const r = random();
if (r < 0.2) out.push(pick(comments));
else if (r < 0.6) out.push(` const ${pick(names)}${i} = ${pick(names)}(${Math.floor(random() * 1000)}, ${pick(strings)});`);
else if (r < 0.85) out.push(` if (${pick(names)} >= ${i % 97}) return ${pick(names)}.${pick(names)} + 1;`);
else out.push(`function ${pick(names)}${i}(a, b) { return a * b - ${i}; }`);
}
return out.join("\n");
}
interface Counts {
lines: number;
identifiers: number;
numbers: number;
strings: number;
comments: number;
commentChars: number;
punctuation: number;
}
function isIdentifierStart(ch: number): boolean {
return (ch >= 97 && ch <= 122) || (ch >= 65 && ch <= 90) || ch === 95 || ch === 36 || ch > 127;
}
function isIdentifierPart(ch: number): boolean {
return isIdentifierStart(ch) || (ch >= 48 && ch <= 57);
}
function lex(text: string): Counts {
const counts: Counts = { lines: 1, identifiers: 0, numbers: 0, strings: 0, comments: 0, commentChars: 0, punctuation: 0 };
let pos = 0;
const end = text.length;
while (pos < end) {
const ch = text.charCodeAt(pos);
if (ch === 10) {
counts.lines++;
pos++;
} else if (ch === 32 || ch === 9 || ch === 13) {
pos++;
} else if (ch === 47 && text.charCodeAt(pos + 1) === 47) {
const start = pos;
while (pos < end && text.charCodeAt(pos) !== 10) pos++;
counts.comments++;
counts.commentChars += pos - start;
} else if (ch === 47 && text.charCodeAt(pos + 1) === 42) {
const start = pos;
pos += 2;
while (pos < end && !(text.charCodeAt(pos) === 42 && text.charCodeAt(pos + 1) === 47)) {
if (text.charCodeAt(pos) === 10) counts.lines++;
pos++;
}
pos += 2;
counts.comments++;
counts.commentChars += pos - start;
} else if (ch === 34 || ch === 39) {
pos++;
while (pos < end && text.charCodeAt(pos) !== ch) pos++;
pos++;
counts.strings++;
} else if (ch >= 48 && ch <= 57) {
while (pos < end && text.charCodeAt(pos) >= 48 && text.charCodeAt(pos) <= 57) pos++;
counts.numbers++;
} else if (isIdentifierStart(ch)) {
while (pos < end && isIdentifierPart(text.charCodeAt(pos))) pos++;
counts.identifiers++;
} else {
counts.punctuation++;
pos++;
}
}
return counts;
}
function lineStarts(text: string): number[] {
const starts = [0];
for (let pos = 0; pos < text.length; pos++) if (text.charCodeAt(pos) === 10) starts.push(pos + 1);
return starts;
}
const scale = Number(process.argv[2] ?? "1");
const source = makeSource(60000);
const passes = Math.round(6 * scale);
let last = lex(source);
let total = last.identifiers + lineStarts(source).length;
for (let i = 1; i < passes; i++) {
last = lex(source);
total += last.identifiers + lineStarts(source).length;
}
console.log("units", source.length, "passes", passes, "total", total);
console.log(JSON.stringify(last));
+1 -1
View File
@@ -128,7 +128,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
<dd>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 <code>SC1010</code> blockers. Relative paths resolve from the current working directory. Accepted files end in <code>.d.ts</code>, <code>.d.mts</code>, or <code>.d.cts</code>.</dd>
<dt><code>--tier &lt;auto|static-only&gt;</code></dt>
<dd>The tier policy for code that has no static lowering. <code>auto</code> is the default. <code>static-only</code> 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 <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
<dd>The tier policy for code that has no static lowering. <code>auto</code> is the default. <code>static-only</code> 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 <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
<dt><code>--explain-dynamic[=&lt;name&gt;]</code></dt>
<dd>Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as <code>x</code>, <code>Point</code>, or <code>Point.x</code>, show only the matching units. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
+1 -1
View File
@@ -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
+7 -8
View File
@@ -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
@@ -1,4 +1,5 @@
- **EventEmitter:** Literal event names use fixed argument tuples.
Computed string names require a constant prefix or suffix that excludes `error`, meta events, and stream-internal events; their arguments must match the exact arity and support checked conversion.
Unrestricted names, symbols, names that may reach internal events, and typed `listeners()`/`rawListeners()` results for names with possible computed registrations are unsupported.
`events.once` accepts EventEmitter and stream instances with a literal event name and an optional object-literal `{ signal }`; a stream's `data` event, other options values, and EventTarget emitters are unsupported.
Reading `defaultMaxListeners` through a CommonJS destructured or member binding (`const { defaultMaxListeners } = require("events")`) is unsupported.
@@ -1,7 +1,10 @@
- **Streams and readline:** Stream chunks are Buffers or strings.
`NodeJS.ReadableStream` and `NodeJS.WritableStream`, and `stream.Readable` and `stream.Writable` under `@types/node`, accept every stream: when a program passes file, zlib, or `node:stream` streams through them, child-process pipes and `process.stdout` and `process.stderr` join those streams as `Readable` and `Writable` instances.
`readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream.
A `pipeline` stage that arrives as an untyped value must be a native stream at run time; other values throw a not-supported error.
When an untyped value reaches `NodeJS.ReadableStream` or `NodeJS.WritableStream`, those two interfaces hold any value, so JavaScript stream objects (constructor-function streams, plain objects with `on` and `pipe`) pass through TypeScript functions typed with them and come back to JavaScript as the same objects.
Member calls on such a value use the native stream when it is one and call the object's own members otherwise; `readable.pipe()` into a JavaScript destination follows Node's pipe protocol.
A `pipeline` stage and a `for await` source that arrive as untyped values must be native streams at run time; other values throw a not-supported error.
Native streams expose no `emit` to JavaScript, so a JavaScript stream whose own `pipe()` emits on its destination (the legacy `Stream` pipe) cannot pipe into one.
`Readable.from` accepts arrays, Sets, Map and Set iterators, generators, async generators, and class iterables and async iterables whose values are strings or Buffers, and in JavaScript any iterable or async iterable of them; a stream that mixes strings and Buffers is unsupported.
In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported.
Object-mode streams of other values (including `Readable.from` over a `Map`, whose values are entry arrays) are unsupported.
@@ -19,7 +19,11 @@ Native calls accept standard process streams; custom native streams and mutation
In JavaScript, a constructor function can inherit from `EventEmitter`, the legacy `Stream` constructor, or a stream class through `util.inherits` or `Object.setPrototypeOf`.
`EventEmitter.call(this)` and `Stream.call(this)` initialize the object as Node does, and its listener methods keep their state in the object's own `_events`, `_eventsCount`, and `_maxListeners` properties.
The `captureRejections` option and `events.errorMonitor` listeners are not supported on such objects.
Calling `Readable`, `Writable`, `Duplex`, `Transform`, or `PassThrough` with a constructor function's receiver throws `SC2020` where the call runs.
`Readable.call(this, options)` and the same call on `Writable`, `Duplex`, `Transform`, or `PassThrough` give the object a native stream.
The stream calls the object's `_read`, `_write`, `_final`, `_transform`, `_flush`, and `_destroy` with the object as `this`, and the stream prototypes' methods and accessors work on it.
Its listeners live in the native stream rather than in `_events`, and `_readableState` and `_writableState` are read-only views that answer the commonly read fields.
Typed code that receives such an object as a stream class uses its native stream, and piping a native stream into it works.
The options the native streams refuse (`objectMode`, `decodeStrings: false`, `signal`, and others) are refused here too, as are `_writev`, `_construct`, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, strings written in encodings other than UTF-8, and pipes into a destination that overrides `write()` or `end()`.
Other class constructors passed to `util.inherits` or `util.deprecate` are refused.
Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported.
A stored `util.deprecate` reports a `length` of 4, where Node reports 3.
@@ -1,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.
@@ -1,7 +0,0 @@
{
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"artifactVersion": "ee72820d45a79aa154f0",
"rowCount": 3880
}
+9 -1
View File
@@ -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(
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-darwin-arm64",
"version": "0.3.6",
"version": "0.4.0",
"description": "Native scriptc compiler for darwin-arm64",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-darwin-x64",
"version": "0.3.6",
"version": "0.4.0",
"description": "Native scriptc compiler for darwin-x64",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-arm64-gnu",
"version": "0.3.6",
"version": "0.4.0",
"description": "Native scriptc compiler for linux-arm64-gnu",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-arm64-musl",
"version": "0.3.6",
"version": "0.4.0",
"description": "Native scriptc compiler for linux-arm64-musl",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-x64-gnu",
"version": "0.3.6",
"version": "0.4.0",
"description": "Native scriptc compiler for linux-x64-gnu",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-linux-x64-musl",
"version": "0.3.6",
"version": "0.4.0",
"description": "Native scriptc compiler for linux-x64-musl",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/cli-win32-x64-msvc",
"version": "0.3.6",
"version": "0.4.0",
"description": "Native scriptc compiler for win32-x64-msvc",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "scriptc",
"version": "0.3.6",
"version": "0.4.0",
"description": "Compile ordinary TypeScript and JavaScript to small, fast native executables and WebAssembly modules that run without Node.js",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
+1 -1
View File
@@ -6569,7 +6569,7 @@ declare module "events" {
export { internal as EventEmitter };
// The event's arguments, as checked-dynamic values.
export function once(
emitter: EventEmitter,
emitter: EventEmitter | NodeJS.ReadableStream | NodeJS.WritableStream,
eventName: string,
options?: { signal?: AbortSignal },
): Promise<unknown[]>;
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@scriptc/compiler",
"version": "0.3.6",
"version": "0.4.0",
"description": "The scriptc compiler — TypeScript/JavaScript frontend, typed IR, LLVM backend",
"license": "Apache-2.0",
"homepage": "https://scriptc.dev",
@@ -40,7 +40,7 @@
},
"dependencies": {
"@scriptc/runtime": "workspace:*",
"tsc-ts": "0.0.3",
"tsc-ts": "0.0.5",
"typescript": "7.0.2",
"typescript5": "npm:typescript@5.9.3"
},
@@ -26,6 +26,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,
@@ -43,6 +43,7 @@ export async function compilePackedLibrary(
searchParams: options.searchParams ?? false,
qs: false,
dynBuiltinClass: false,
dynBuiltinStream: false,
parseArgs: false,
stream: false,
net: false,
@@ -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 });
+151 -27
View File
@@ -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<string, IrFunction>,
sizeType: "i32" | "i64" = "i64",
): Map<string, LlClassMeta> {
const metaMap = new Map<string, LlClassMeta>();
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<LlClassMeta>();
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
+88 -4
View File
@@ -39,6 +39,7 @@ import {
isDirentShape,
isDynTypedRefType,
isRefCounted,
isUnitType,
typeKey,
} from "../../ir/ir.js";
import { dynDesc, streamTypedRefEligible } from "../../ir/analysis.js";
@@ -130,6 +131,8 @@ export interface DynHost extends WalkerHost {
/** The shared adapter record: live array capsules read its `arrayOps`, so a
* narrower return type would hand the native compiler a copy without it. */
liveDynRefAdapter(t: IrType): LlStreamTypedRefAdapter;
/** See moduleUsesDynBuiltinStream. */
usesDynBuiltinStream?(): boolean;
liveDynUnionRefAdapter(t: IrType & { kind: "union" }): string;
dynPromiseAdapter(t: IrType): string;
isErrorClass(className: string): boolean;
@@ -140,6 +143,19 @@ export interface DynHost extends WalkerHost {
const FN_ATTRS = "#0";
/** scr_dyn_builtin_stream_check's kind for a runtime stream class (the
* numbering of scr_dyn_builtin_class.c), or null. */
function builtinStreamKind(className: string): number | null {
const kinds: Record<string, number> = {
"%Readable": 2,
"%Writable": 3,
"%Duplex": 4,
"%Transform": 5,
"%PassThrough": 6,
};
return kinds[className] ?? null;
}
export class LlDyn {
private readonly dynMatchers = new Map<string, string>();
private readonly dynBuilders = new Map<string, string>();
@@ -682,6 +698,46 @@ export class LlDyn {
/** Emit the standard in-helper pending check: on a pending exception
* run `cleanup` and return `dummy`. */
/** The failure block of a union dynCheck whose value matched no arm. A
* value of the one container kind exactly one arm can take (an object
* for the union's only record arm, an array for its only array arm,
* beside unit and scalar arms) runs that arm's builder, which throws the
* deepest failing path and the reason there (`expected string at
* $.order.owner.id, got undefined`) instead of the union's own outer
* path. Other values, and an arm builder that unexpectedly succeeds,
* fall through to the union's message. Only the failure path changes. */
private unionArmFailure(B: BlockBuilder, def: IrUnionDef, kind: string): void {
const scalar = (arm: IrType): boolean =>
isUnitType(arm) ||
arm.kind === "f64" ||
arm.kind === "bool" ||
arm.kind === "string" ||
arm.kind === "bigint";
const containers = def.arms.filter((arm) => !scalar(arm));
const arm = containers.length === 1 ? containers[0]! : null;
const container =
arm?.kind === "array"
? DYN_KIND.ARR
: arm?.kind === "record" && !this.host.recordsById.get(arm.shapeId)?.tuple
? DYN_KIND.OBJ
: null;
if (!arm || container === null) return;
const host = this.host;
const deep = B.newLabel("dcu.deep");
const outer = B.newLabel("dcu.outer");
const same = B.tmp();
B.line(`${same} = icmp eq i32 ${kind}, ${container}`);
B.condBr(same, deep, outer);
B.startBlock(deep);
const built = B.tmp();
B.line(`${built} = call ptr @${this.dynCheckHelper(arm)}(ptr %d, ptr %path)`);
this.pendingBail(B, "dcu.deep", () => {}, "ptr null");
const rc = vAdapters(host, arm);
B.line(`call void ${rc.release}(ptr ${built})`);
B.br(outer);
B.startBlock(outer);
}
private pendingBail(B: BlockBuilder, hint: string, cleanup: () => void, dummy: string): void {
const p = B.tmp();
for (const line of this.host.pendingTestLines(p)) B.line(line.trimStart());
@@ -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;
@@ -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), ``);
+48 -30
View File
@@ -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<string, LlStreamTypedRefAdapter>();
private dynBuiltinStreamUse: boolean | undefined;
/** See moduleUsesDynBuiltinStream. */
get usesDynBuiltinStream(): boolean {
return (this.dynBuiltinStreamUse ??= moduleUsesDynBuiltinStream(this.mod));
}
/** Runtime-arm dispatchers for live union values. */
readonly liveDynUnionRefAdapters = new Map<string, string>();
readonly dynPromiseAdapters = new Map<string, string>();
@@ -1405,6 +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 {
@@ -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;
@@ -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`,
`}`,
``,
@@ -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}`,
);
@@ -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);
@@ -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");
});
@@ -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(),
@@ -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":
@@ -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();
@@ -444,6 +444,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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<string, string> = {
"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<string, string> = {
"procStream.write": "scr_proc_stream_write",
"stream.stdioWritable": "scr_stream_stdio_writable",
"stream.unboxDyn": "scr_stream_unbox_dyn",
"stream.pipeDyn": "scr_stream_pipe_dyn",
"stream.unpipeDyn": "scr_stream_unpipe_dyn",
"process.kill": "scr_process_kill_named",
"process.killNum": "scr_process_kill",
// fs.promises (scr_bytes_io.c / scr_lib.c): the promise forms settle
@@ -1459,6 +1465,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.classBasePrototype": "scr_dyn_class_base_prototype",
"dyn.classInherit": "scr_dyn_class_inherit",
"dyn.builtinClass": "scr_dyn_builtin_class",
"dyn.builtinStreamClass": "scr_dyn_builtin_stream_class",
"dyn.builtinInstanceOf": "scr_dyn_builtin_instanceof",
"dyn.functionInstanceOf": "scr_dyn_function_instanceof",
"dyn.classSuper": "scr_dyn_class_super",
@@ -1537,6 +1544,8 @@ export function mockTimerSym(
export const USES_TIMERS_LIB_FNS = new Set<string>([
"process.emitWarning",
// Constructor-function streams defer their events on the stream ticks.
"dyn.builtinStreamClass",
// Decipheriv.setAuthTag reports DEP0182 on the next tick.
"crypto.cipherNew",
// WebCrypto results settle through the deferred crypto queue.
@@ -36,6 +36,29 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
host.emitPendingCheck();
return out;
}
if (e.fn === "stream.dynNative" || e.fn === "stream.dynNativePipe") {
// Checked-dynamic stream slots: the boxed native stream (+1) or NULL.
if (e.type.kind !== "union")
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} result is not a union`);
const def = host.unionsById.get(e.type.unionId);
const streamTag = def ? def.arms.findIndex((a) => a.kind === "object") : -1;
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
if (streamTag < 0 || nullTag < 0)
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} union lacks its arms`);
const value = host.emitExpr(e.args[0]!);
const side = host.emitExpr(e.args[1]!);
// A pipe or unpipe destination in programs with constructor-function
// streams can be one of those streams too (scr_dyn_builtin_stream.c),
// as stream.unboxDyn does there.
const fn =
e.fn === "stream.dynNativePipe" && host.usesDynBuiltinStream
? "scr_dyn_builtin_stream_dyn_native"
: "scr_stream_dyn_native";
host.declare(`declare ptr @${fn}(ptr, double)`);
const raw = B.tmp();
B.line(`${raw} = call ptr @${fn}(ptr ${value.name}, double ${side.name}) ; +1 or NULL`);
return host.wrapNullable(raw, raw, def!.arms[streamTag]!, streamTag, e.type, nullTag);
}
if (e.fn === "stream.onData") {
// The callback MOVES into the stream's listener registry; the
// adapter is per callback shape — runtime-provided for the
@@ -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;
+41 -16
View File
@@ -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);
@@ -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).
@@ -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)}`,
+15 -5
View File
@@ -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)` : ""}`,
@@ -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
@@ -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(
+7
View File
@@ -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
@@ -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;
@@ -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;
@@ -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,
@@ -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,
@@ -108,6 +108,7 @@ const features = {
searchParams: false,
qs: false,
dynBuiltinClass: false,
dynBuiltinStream: false,
parseArgs: false,
stream: false,
net: false,
@@ -45,6 +45,7 @@ const BASE: NativeLinkFeatures = {
searchParams: false,
qs: false,
dynBuiltinClass: false,
dynBuiltinStream: false,
parseArgs: false,
stream: false,
net: false,
@@ -84,6 +84,97 @@ test("a computed key reads the stack when the program spells it", () => {
expect(observesStacks(mod([fn("%main", [computed])]))).toBe(false);
});
test('a computed key that cannot be "stack" reads no stack', () => {
const value: IrExpr = { loc, kind: "varRef", localId: "d", type: DYN };
const spelled: IrStmt = {
loc,
kind: "exprStmt",
expr: { loc, kind: "strLit", value: "stack", type: STRING },
};
const read = (key: IrExpr): IrStmt => ({
loc,
kind: "exprStmt",
expr: { loc, kind: "dynKeyGet", value, key, type: DYN },
});
const index: IrExpr = { loc, kind: "varRef", localId: "i", type: F64 };
const indexText: IrExpr = { loc, kind: "toString", operand: index, type: STRING };
// A number key, and a key converted from one.
expect(observesStacks(mod([fn("%main", [read(index), spelled])]))).toBe(false);
expect(observesStacks(mod([fn("%main", [read(indexText), spelled])]))).toBe(false);
// An immutable local answers with its initializer (the shape of the
// dispatch helpers' formatted index keys).
const key: IrExpr = { loc, kind: "varRef", localId: "key", type: STRING };
const local = (init: IrExpr, mutable = false): IrFunction => ({
...fn("%main", [{ loc, kind: "varDecl", localId: "key", init }, read(key), spelled]),
locals: [{ id: "key", name: "key", type: STRING, mutable }],
});
expect(observesStacks(mod([local(indexText)]))).toBe(false);
expect(observesStacks(mod([local(indexText, true)]))).toBe(true);
// A parameter of a function called by name answers with its arguments.
const helper: IrFunction = {
...fn("readKey", [read(key)]),
params: [{ localId: "key", name: "key", type: STRING }],
locals: [{ id: "key", name: "key", type: STRING, mutable: false }],
};
const callWith = (arg: IrExpr): IrStmt => ({
loc,
kind: "exprStmt",
expr: { loc, kind: "call", callee: "readKey", args: [arg], type: DYN },
});
const numbers = mod([fn("%main", [callWith(indexText), spelled]), helper]);
expect(observesStacks(numbers)).toBe(false);
const stack = mod([
fn("%main", [
callWith(indexText),
callWith({ loc, kind: "strLit", value: "stack", type: STRING }),
]),
helper,
]);
expect(observesStacks(stack)).toBe(true);
});
test("a key forwarded through functions with several call sites is searched once", () => {
// f0 calls f1 twice, f1 calls f2 twice, and so on, each passing its
// parameter on: 2^40 call paths reach the read in f40.
const depth = 40;
const value: IrExpr = { loc, kind: "varRef", localId: "d", type: DYN };
const key: IrExpr = { loc, kind: "varRef", localId: "key", type: STRING };
const forward = (callee: string): IrStmt => ({
loc,
kind: "exprStmt",
expr: { loc, kind: "call", callee, args: [key], type: VOID },
});
const chain = (root: IrExpr): IrModule => {
const functions: IrFunction[] = [];
for (let i = 0; i <= depth; i++) {
const body: IrStmt[] =
i < depth
? [forward(`f${i + 1}`), forward(`f${i + 1}`)]
: [{ loc, kind: "exprStmt", expr: { loc, kind: "dynKeyGet", value, key, type: DYN } }];
functions.push({
...fn(`f${i}`, body),
params: [{ localId: "key", name: "key", type: STRING }],
locals: [{ id: "key", name: "key", type: STRING, mutable: false }],
});
}
const spelled: IrStmt = {
loc,
kind: "exprStmt",
expr: { loc, kind: "strLit", value: "stack", type: STRING },
};
const start: IrStmt = {
loc,
kind: "exprStmt",
expr: { loc, kind: "call", callee: "f0", args: [root], type: VOID },
};
return mod([fn("%main", [start, spelled]), ...functions]);
};
expect(observesStacks(chain({ loc, kind: "strLit", value: "message", type: STRING }))).toBe(
false,
);
expect(observesStacks(chain({ loc, kind: "strLit", value: "stack", type: STRING }))).toBe(true);
});
test("programs without stack reads are not", () => {
expect(observesStacks(mod([fn("%main", [call("helper")]), fn("helper", [ret])]))).toBe(false);
});
@@ -3,7 +3,7 @@
* it answers true. */
import { exprFunctionReference, moduleFunctionReferences } from "../ir/function-references.js";
import { MAY_THROW_LIB_FNS } from "../ir/builtin-effects.js";
import type { IrExpr, IrFunction, IrModule, IrStmt } from "../ir/ir.js";
import type { IrExpr, IrFunction, IrModule, IrStmt, IrType } from "../ir/ir.js";
import { MAY_THROW_ARR_METHODS } from "../ir/ir.js";
import { everyExprChild, everyStmtChild, everyStmtList } from "../ir/traverse.js";
import { unreachableStatements } from "./may-throw.js";
@@ -13,16 +13,135 @@ import { writesPublished } from "./publication.js";
/** Lib calls that read a captured stack trace. */
const STACK_READS: ReadonlySet<string> = new Set(["error.stack"]);
/** Key types whose property names are never `stack`: a number, bigint or
* boolean key names its decimal or `true`/`false` property. */
const NON_STACK_KEY_TYPES: ReadonlySet<IrType["kind"]> = new Set(["f64", "bigint", "bool"]);
/** Whether a computed key can be the string "stack", the question a read
* on a checked-dynamic value asks before it counts as a stack read.
* - A string literal answers by its value; a key of a number, bigint or
* boolean type, or one converted from such a value, cannot be "stack".
* - An immutable local answers with its one initializer, and a parameter of
* a function the program only calls by name answers with the arguments
* its call sites pass.
* - Any other key can be "stack" when the program spells the literal
* "stack" somewhere, since the literal can reach the key through fields,
* containers or calls; a key the program assembles from other strings or
* from data it reads keeps the static answer. */
class StackKeys {
private readonly spelled: boolean;
private readonly calls = new Map<string, { caller: IrFunction; args: IrExpr[] }[]>();
private readonly named: Set<string>;
private readonly inits = new Map<IrFunction, Map<string, IrExpr | null>>();
/** Settled answers by function and local: true where a search reached a
* key that can be "stack", false where a search that found none passed. */
private readonly answers = new Map<string, boolean>();
constructor(mod: IrModule) {
this.spelled = spellsStack(mod);
this.named = new Set(moduleFunctionReferences(mod));
// Class methods, accessors and constructors are called through dispatch.
const classNames = new Set((mod.classes ?? []).map((cls) => cls.name));
for (const fn of mod.functions) {
if (isClassMember(fn.name, classNames)) this.named.add(fn.name);
everyStmtList(fn.body, {
stmt: () => true,
expr: (expr) => {
if (expr.kind === "call") {
const sites = this.calls.get(expr.callee) ?? [];
sites.push({ caller: fn, args: expr.args });
this.calls.set(expr.callee, sites);
}
const referenced = exprFunctionReference(expr);
if (referenced !== undefined) this.named.add(referenced);
return true;
},
});
}
}
mayBeStack(fn: IrFunction, key: IrExpr): boolean {
const visited = new Set<string>();
const answer = this.keyMayBeStack(fn, key, visited);
// A search that found no way to "stack" explored everything it visited
// to the end, so none of it can be "stack" either.
if (!answer) for (const visit of visited) this.answers.set(visit, false);
return answer;
}
private keyMayBeStack(fn: IrFunction, key: IrExpr, visited: Set<string>): boolean {
if (key.kind === "strLit") return key.value === "stack";
if (!this.spelled) return false;
if (NON_STACK_KEY_TYPES.has(key.type.kind)) return false;
if (key.kind === "toString" && NON_STACK_KEY_TYPES.has(key.operand.type.kind)) return false;
if (key.kind === "dynFrom" && NON_STACK_KEY_TYPES.has(key.value.type.kind)) return false;
if (key.kind !== "varRef") return true;
const visit = `${fn.name}\0${key.localId}`;
const known = this.answers.get(visit);
if (known !== undefined) return known;
// Each local is searched once per question: a cycle, or a second path
// to a local already searched, adds no value of its own.
if (visited.has(visit)) return false;
visited.add(visit);
const answer = this.localMayBeStack(fn, key.localId, visited);
if (answer) this.answers.set(visit, true);
return answer;
}
private localMayBeStack(fn: IrFunction, localId: string, visited: Set<string>): boolean {
const local = fn.locals.find((candidate) => candidate.id === localId);
if (local === undefined || local.mutable) return true;
const index = fn.params.findIndex((param) => param.localId === localId);
if (index !== -1) {
const sites = this.calls.get(fn.name);
if (this.named.has(fn.name) || sites === undefined) return true;
return sites.some(
(site) =>
site.args.length !== fn.params.length ||
this.keyMayBeStack(site.caller, site.args[index]!, visited),
);
}
const init = this.initializerOf(fn, localId);
return init === null || this.keyMayBeStack(fn, init, visited);
}
/** The one initializer of an immutable local, or null when it has none
* or more than one declaration. */
private initializerOf(fn: IrFunction, localId: string): IrExpr | null {
let inits = this.inits.get(fn);
if (inits === undefined) {
inits = new Map();
everyStmtList(fn.body, {
stmt: (stmt) => {
if (stmt.kind === "varDecl")
inits!.set(stmt.localId, inits!.has(stmt.localId) ? null : (stmt.init ?? null));
return true;
},
expr: () => true,
});
this.inits.set(fn, inits);
}
return inits.get(localId) ?? null;
}
}
/** Whether `name` follows the `%<class>.<member>` convention of class
* methods, accessors and constructors for one of `classNames`. */
function isClassMember(name: string, classNames: ReadonlySet<string>): boolean {
if (!name.startsWith("%")) return false;
for (let dot = name.indexOf("."); dot !== -1; dot = name.indexOf(".", dot + 1)) {
if (classNames.has(name.slice(1, dot))) return true;
}
return false;
}
/** A typed `error.stack` read, or a read on a checked-dynamic value that
* can reach an error's own `stack` accessor: the constant key `stack`, or
* a computed key when `computedKeys` (the program spells "stack", so a
* computed key can name it). A key the program assembles from other
* strings or from data it reads keeps the static answer. */
function readsStack(expr: IrExpr, computedKeys: boolean): boolean {
* can reach an error's own `stack` accessor: one whose key can be "stack"
* (StackKeys). */
function readsStack(fn: IrFunction, expr: IrExpr, keys: StackKeys): boolean {
if (expr.kind === "libCall") return STACK_READS.has(expr.fn);
if (expr.kind !== "dynKeyGet") return false;
if (expr.key.kind === "strLit") return expr.key.value === "stack";
return computedKeys;
return keys.mayBeStack(fn, expr.key);
}
/** Whether the module contains the string literal "stack". */
@@ -36,10 +155,10 @@ function spellsStack(mod: IrModule): boolean {
);
}
function containsStackRead(fn: IrFunction, computedKeys: boolean): boolean {
function containsStackRead(fn: IrFunction, keys: StackKeys): boolean {
return !everyStmtList(fn.body, {
stmt: () => true,
expr: (expr) => !readsStack(expr, computedKeys),
expr: (expr) => !readsStack(fn, expr, keys),
});
}
@@ -65,22 +184,14 @@ function visitLiveExprs(body: IrStmt[], visit: (expr: IrExpr) => void): void {
* and those reads must not cost every call of the program a frame.
* Programs without any stack read answer false without the walk. */
export function observesStacks(mod: IrModule): boolean {
const computedKeys = spellsStack(mod);
if (!mod.functions.some((fn) => containsStackRead(fn, computedKeys))) return false;
const keys = new StackKeys(mod);
if (!mod.functions.some((fn) => containsStackRead(fn, keys))) return false;
const byName = new Map(mod.functions.map((fn) => [fn.name, fn]));
const classNames = new Set((mod.classes ?? []).map((cls) => cls.name));
const pending: string[] = [...moduleFunctionReferences(mod)];
// Class methods, accessors and constructors are reachable through
// dispatch the IR does not name per site: all of them are roots.
for (const fn of mod.functions) {
if (!fn.name.startsWith("%")) continue;
for (let dot = fn.name.indexOf("."); dot !== -1; dot = fn.name.indexOf(".", dot + 1)) {
if (classNames.has(fn.name.slice(1, dot))) {
pending.push(fn.name);
break;
}
}
}
for (const fn of mod.functions) if (isClassMember(fn.name, classNames)) pending.push(fn.name);
const reached = new Set<string>();
let observed = false;
while (pending.length > 0 && !observed) {
@@ -90,7 +201,7 @@ export function observesStacks(mod: IrModule): boolean {
const fn = byName.get(name);
if (fn === undefined) continue;
visitLiveExprs(fn.body, (expr) => {
if (readsStack(expr, computedKeys)) observed = true;
if (readsStack(fn, expr, keys)) observed = true;
if (expr.kind === "call" && !reached.has(expr.callee)) pending.push(expr.callee);
const referenced = exprFunctionReference(expr);
if (referenced !== undefined && !reached.has(referenced)) pending.push(referenced);
@@ -51,6 +51,7 @@ export async function warmNativeCaches(
searchParams: false,
qs: false,
dynBuiltinClass: false,
dynBuiltinStream: false,
parseArgs: false,
stream: false,
net: false,
+3 -2
View File
@@ -73,8 +73,9 @@ Options:
--tier <auto|static-only>
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[=<name>]
coverage only: list the functions that operate on dynamic
values and the bindings, fields, functions, classes and
@@ -46,6 +46,7 @@ const native: EarlyExecutableNativeFeatures = {
searchParams: false,
qs: false,
dynBuiltinClass: false,
dynBuiltinStream: false,
parseArgs: false,
stream: false,
net: false,
@@ -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",
+21 -22
View File
@@ -1,4 +1,5 @@
import * as ts from "./ts7/syntax.js";
import { tokenStartOfNode } from "./ts7/node-functions.js";
export interface CjsLexedExports {
/** Names Node's lexer detects in THIS file (no reexport resolution). */
@@ -30,7 +31,7 @@ function identRunEnd(text: string, pos: number): number {
* a `a`-escaped name parses to the same AST as its plain spelling,
* but merve reads bytes and stops at the backslash. */
function sourceSpellsIdentifier(node: ts.Node, sf: ts.SourceFile): boolean {
const ch = sf.text[node.getStart(sf)];
const ch = sf.text[tokenStartOfNode(node)];
return ch !== undefined && isIdentStartChar(ch);
}
@@ -39,16 +40,16 @@ function sourceSpellsIdentifier(node: ts.Node, sf: ts.SourceFile): boolean {
* defineProperty getter's `return IDENT.IDENT` because `this` lexes as an
* identifier run — probed), escapes and private names do not. */
function isIdentTokenNode(node: ts.Node, sf: ts.SourceFile): boolean {
const start = node.getStart(sf);
const start = tokenStartOfNode(node);
const ch = sf.text[start];
return ch !== undefined && isIdentStartChar(ch) && identRunEnd(sf.text, start) === node.getEnd();
return ch !== undefined && isIdentStartChar(ch) && identRunEnd(sf.text, start) === node.end;
}
/** The identifier run at the node's START — the whole name when its
* spelling is plain, the byte prefix merve consumed when a unicode escape
* interrupts the run mid-name. */
function identPrefixOf(node: ts.Node, sf: ts.SourceFile): string {
const start = node.getStart(sf);
const start = tokenStartOfNode(node);
return sf.text.slice(start, identRunEnd(sf.text, start));
}
@@ -84,20 +85,17 @@ export function isModuleExports(e: ts.Expression): boolean {
* follows — so the recognizer descends the leftmost-child chain while it
* still starts where `e` starts. A ParenthesizedExpression breaks the
* chain naturally (its child starts after the `(`), matching merve. */
function leadingRequireOf(
e: ts.Expression,
sf: ts.SourceFile,
): { spec: string; end: number } | null {
const start = e.getStart(sf);
function leadingRequireOf(e: ts.Expression): { spec: string; end: number } | null {
const start = tokenStartOfNode(e);
let cur: ts.Node = e;
for (;;) {
const spec = bareRequireSpecOf(cur);
if (spec !== null && cur.getStart(sf) === start) return { spec, end: cur.getEnd() };
if (spec !== null && tokenStartOfNode(cur) === start) return { spec, end: cur.end };
let first: ts.Node | undefined;
ts.forEachChild(cur, (c) => {
first ??= c;
});
if (first === undefined || first.getStart(sf) !== start) return null;
if (first === undefined || tokenStartOfNode(first) !== start) return null;
cur = first;
}
}
@@ -179,13 +177,13 @@ function scanTableLiteral(
continue; // whitespace before `,` is skipped on the shorthand path
}
if (ts.isSpreadAssignment(prop)) {
const req = leadingRequireOf(prop.expression, sf);
const req = leadingRequireOf(prop.expression);
let consumedEnd: number;
if (req !== null) {
reexports.push(req.spec);
consumedEnd = req.end;
} else if (sourceSpellsIdentifier(prop.expression, sf)) {
consumedEnd = identRunEnd(text, prop.expression.getStart(sf));
consumedEnd = identRunEnd(text, tokenStartOfNode(prop.expression));
} else {
return; // `...{}` and friends stop the scan
}
@@ -213,7 +211,7 @@ function scanTableLiteral(
}
if (key === null) return; // numeric / computed / template keys stop the scan
const value = prop.initializer;
const req = leadingRequireOf(value, sf);
const req = leadingRequireOf(value);
if (req !== null) {
// A require VALUE adds the key, records the reexport, and stops
// the scan UNCONDITIONALLY — probed: `{ a: require('x'), b: v }`
@@ -223,7 +221,7 @@ function scanTableLiteral(
reexports.push(req.spec);
return;
}
const start = value.getStart(sf);
const start = tokenStartOfNode(value);
const ch0 = text[start];
if (ch0 === undefined || !isIdentStartChar(ch0)) return; // literal values stop WITHOUT adding the key
const consumedEnd = identRunEnd(text, start);
@@ -351,8 +349,8 @@ function starExportSpecOf(call: ts.CallExpression, sf: ts.SourceFile): string |
if (!sourceSpellsIdentifier(nameNode, sf)) return null;
const arg0 = call.arguments.nodes[0];
if (arg0 === undefined) return null;
if (!byteAdjacentParen(sf, nameNode.getEnd(), arg0.getStart(sf))) return null;
return leadingRequireOf(arg0, sf)?.spec ?? null;
if (!byteAdjacentParen(sf, nameNode.end, tokenStartOfNode(arg0))) return null;
return leadingRequireOf(arg0)?.spec ?? null;
}
/** True when the bytes between a callee name ending at `nameEnd` and its
@@ -384,7 +382,7 @@ function starAssignOf(stmt: ts.VariableStatement, sf: ts.SourceFile): [string, s
init.arguments.nodes.length >= 1
) {
const arg0 = init.arguments.nodes[0]!;
if (!byteAdjacentParen(sf, init.expression.getEnd(), arg0.getStart(sf))) return null;
if (!byteAdjacentParen(sf, init.expression.end, tokenStartOfNode(arg0))) return null;
const spec = bareRequireSpecOf(arg0);
if (spec !== null) return [decl.name.text, spec];
}
@@ -717,15 +715,16 @@ export function cjsLexedExportsOfFile(sf: ts.SourceFile): CjsLexedExports {
) {
exports.add(lhs.argumentExpression.text);
} else if (isModuleExports(lhs)) {
events.push({ pos: n.getStart(sf), kind: "assign", rhs: n.right });
events.push({ pos: tokenStartOfNode(n), kind: "assign", rhs: n.right });
}
} else if (ts.isCallExpression(n)) {
scanDefineProperty(n, sf, exports);
if (braceDepth === 0) {
const starSpec = starExportSpecOf(n, sf);
if (starSpec !== null) events.push({ pos: n.getStart(sf), kind: "spec", spec: starSpec });
if (starSpec !== null)
events.push({ pos: tokenStartOfNode(n), kind: "spec", spec: starSpec });
const loopId = starLoopIdOf(n, sf);
if (loopId !== null) events.push({ pos: n.getStart(sf), kind: "loop", id1: loopId });
if (loopId !== null) events.push({ pos: tokenStartOfNode(n), kind: "loop", id1: loopId });
}
} else if (ts.isVariableStatement(n) && braceDepth === 0) {
const assign = starAssignOf(n, sf);
@@ -745,7 +744,7 @@ export function cjsLexedExportsOfFile(sf: ts.SourceFile): CjsLexedExports {
for (const spec of starAssigns.get(ev.id1) ?? []) reexports.push(spec);
} else {
reexports = []; // every `module.exports =` clears — stars included (probed)
const req = leadingRequireOf(ev.rhs, sf);
const req = leadingRequireOf(ev.rhs);
if (req !== null) {
reexports.push(req.spec);
} else if (ts.isObjectLiteralExpression(ev.rhs)) {
@@ -130,7 +130,7 @@ function markerNamed(
function nameText(node: ts.Node | undefined): string {
if (node === undefined) return "<anonymous>";
if (ts.isIdentifier(node) || ts.isPrivateIdentifier(node)) return node.text;
return node.getText();
return ts.getTextOfNode(node);
}
function contractDiag(
@@ -73,7 +73,7 @@ export function lifetimeRegionMemberOf(
if (
!ts.isClassDeclaration(owner) ||
owner.name?.text !== "LifetimeRegion" ||
!isExperimentalDeclarationFile(owner.getSourceFile(), known)
!isExperimentalDeclarationFile(ts.getSourceFileOfNode(owner), known)
)
return null;
const name = access.name.text;
@@ -105,7 +105,7 @@ export function internTableMemberOf(
if (
!ts.isClassDeclaration(owner) ||
owner.name?.text !== "InternTable" ||
!isExperimentalDeclarationFile(owner.getSourceFile(), known)
!isExperimentalDeclarationFile(ts.getSourceFileOfNode(owner), known)
)
return null;
const name = access.name.text;
@@ -135,7 +135,7 @@ export function threadSlotsMemberOf(
if (
!ts.isClassDeclaration(owner) ||
owner.name?.text !== "ThreadSlots" ||
!isExperimentalDeclarationFile(owner.getSourceFile(), known)
!isExperimentalDeclarationFile(ts.getSourceFileOfNode(owner), known)
)
return null;
const name = access.name.text;
@@ -7,6 +7,7 @@ import { fallbackDtsPath } from "./dts-paths.js";
import { Ts7Host } from "./ts7/program-host.js";
import { Ts7Api } from "./ts7/rpc-api.js";
import { ModuleKind, ModuleResolutionKind } from "./ts7/enums.js";
import { getTextOfNode } from "./ts7/node-functions.js";
test("fork discovery batches only candidate bindings and preserves aliases and shadowing", () => {
const directory = mkdtempSync(join(tmpdir(), "scriptc-fork-discovery-"));
@@ -49,7 +50,7 @@ test("fork discovery batches only candidate bindings and preserves aliases and s
["first.ts", "second.ts", "third.ts"].map((name) => join(directory, name)),
);
expect(batch).toHaveBeenCalledTimes(1);
expect(batch.mock.calls[0]![0].map((node) => node.getText())).toEqual([
expect(batch.mock.calls[0]![0].map((node) => getTextOfNode(node))).toEqual([
"start",
"child",
"launch",
@@ -12,7 +12,7 @@ export function isNodeGlobal(program: ts.Program, expression: ts.Identifier): bo
return (
declarations.length > 0 &&
declarations.every((declaration) => {
const source = declaration.getSourceFile();
const source = ts.getSourceFileOfNode(declaration);
return (
source.isDeclarationFile &&
(tsgoPath(source.fileName) === fallbackDtsPath() || isNodeTypesPath(source.fileName))
@@ -61,7 +61,7 @@ export function isBuiltinMemberImport(
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
const target = checker.getAliasedSymbol(symbol);
const targetDecl = target.declarations[0];
if (targetDecl !== undefined && targetDecl.getSourceFile().isDeclarationFile) {
if (targetDecl !== undefined && ts.getSourceFileOfNode(targetDecl).isDeclarationFile) {
for (
let parent: ts.Node | undefined = targetDecl.parent;
parent !== undefined && !ts.isSourceFile(parent);
@@ -69,7 +69,8 @@ export function isBuiltinMemberImport(
) {
if (ts.isModuleDeclaration(parent) && ts.isStringLiteral(parent.name)) {
return (
canonicalBuiltinModule(parent.name.text) === moduleName && target.name === memberName
canonicalBuiltinModule(parent.name.text) === moduleName &&
ts.symbolName(target) === memberName
);
}
}
@@ -188,7 +189,7 @@ function staticString(
): string | null {
const current = strip(expr);
if (ts.isStringLiteralLike(current)) return current.text;
if (importMetaUrl(current)) return pathToFileURL(current.getSourceFile().fileName).href;
if (importMetaUrl(current)) return pathToFileURL(ts.getSourceFileOfNode(current).fileName).href;
if (ts.isTemplateExpression(current)) {
let value = current.head.text;
for (const span of current.templateSpans.nodes) {
@@ -16,7 +16,7 @@ export function inferredJsDiagnosticSuppressed(
const checker = program.getTypeChecker();
let suppressed = false;
ts.walkPreorder(file, (node) => {
if (node.getEnd() < diagnostic.pos || node.getStart(file) > diagnostic.end) return "skip";
if (node.end < diagnostic.pos || ts.tokenStartOfNode(node) > diagnostic.end) return "skip";
if (
diagnostic.code === 2565 &&
isJsSourceFileName(file.fileName) &&
@@ -30,7 +30,7 @@ export function inferredJsDiagnosticSuppressed(
ts.isPropertyDeclaration(decl) &&
!decl.type &&
!decl.initializer &&
!/@type\b/.test(file.text.slice(decl.pos, decl.getStart(file))),
!/@type\b/.test(file.text.slice(decl.pos, ts.tokenStartOfNode(decl))),
)
) {
// Bare JS fields exist with value undefined before the first write;
@@ -44,14 +44,14 @@ export function inferredJsDiagnosticSuppressed(
const declaration = signature && signature.declaration;
if (
declaration &&
isJsSourceFileName(declaration.getSourceFile().fileName) &&
isJsSourceFileName(ts.getSourceFileOfNode(declaration).fileName) &&
(ts.isFunctionDeclaration(declaration) ||
ts.isFunctionExpression(declaration) ||
ts.isMethodDeclaration(declaration)) &&
node.arguments.nodes.length > declaration.parameters.nodes.length &&
node.arguments.nodes[declaration.parameters.nodes.length]?.getStart(file) ===
ts.tokenStartOfNode(node.arguments.nodes[declaration.parameters.nodes.length]!) ===
diagnostic.pos &&
node.arguments.nodes[node.arguments.nodes.length - 1]?.getEnd() === diagnostic.end &&
node.arguments.nodes[node.arguments.nodes.length - 1]!.end === diagnostic.end &&
bodyReadsArguments(declaration)
) {
suppressed = true;
@@ -68,7 +68,7 @@ export function inferredJsDiagnosticSuppressed(
const iterable = checker.getTypeAtLocation(node.expression);
const iterator = checker
.getPropertiesOfType(iterable)
.find((property) => property.name.startsWith("__@iterator@"));
.find((property) => ts.symbolName(property).startsWith("__@iterator@"));
if (!iterator) return undefined;
const signatures = checker.getCallSignatures(
checker.getTypeOfSymbolAtLocation(iterator, node.expression),
@@ -88,7 +88,7 @@ export function inferredJsDiagnosticSuppressed(
declaration &&
ts.isMethodDeclaration(declaration) &&
declaration.type === undefined &&
isJsSourceFileName(declaration.getSourceFile().fileName) &&
isJsSourceFileName(ts.getSourceFileOfNode(declaration).fileName) &&
declaration.body?.statements?.nodes.length === 1 &&
ts.isThrowStatement(declaration.body.statements.nodes[0]!)
) {
+12 -8
View File
@@ -115,7 +115,7 @@ const CONTRACT_GLOBAL_TYPES = new Set(["Array", "ReadonlyArray", "Uint8Array"]);
type TypeBindings = Map<ts.SourceFile, Set<string>>;
function locOf(file: ts.SourceFile, node: ts.Node): SrcLoc {
return { file: file.fileName, start: node.getStart(), end: node.end };
return { file: file.fileName, start: ts.tokenStartOfNode(node), end: node.end };
}
function isExported(stmt: ts.Node): boolean {
@@ -248,7 +248,7 @@ function typeShape(
const lit = node.literal;
if (ts.isStringLiteral(lit)) return { k: "stringLit", text: lit.text };
if (lit.kind === ts.SyntaxKind.NullKeyword) return { k: "absent", unit: "null" };
return { k: "unsupported", text: node.getText(file) };
return { k: "unsupported", text: ts.getTextOfNode(node) };
}
if (ts.isArrayTypeNode(node))
return { k: "array", elem: typeShape(file, node.elementType, bindings) };
@@ -280,15 +280,19 @@ function typeShape(
if (ts.isConditionalTypeNode(node)) {
return {
k: "unsupported",
text: `a conditional type (${node.getText(file)})`,
text: `a conditional type (${ts.getTextOfNode(node)})`,
computed: "conditional",
};
}
if (ts.isMappedTypeNode(node)) {
return { k: "unsupported", text: `a mapped type (${node.getText(file)})`, computed: "mapped" };
return {
k: "unsupported",
text: `a mapped type (${ts.getTextOfNode(node)})`,
computed: "mapped",
};
}
if (ts.isTypeReferenceNode(node)) {
if (!ts.isIdentifier(node.typeName)) return { k: "unsupported", text: node.getText(file) };
if (!ts.isIdentifier(node.typeName)) return { k: "unsupported", text: ts.getTextOfNode(node) };
const name = node.typeName.text;
const global =
CONTRACT_GLOBAL_TYPES.has(name) && isUnshadowedGlobalType(file, node, name, bindings);
@@ -302,10 +306,10 @@ function typeShape(
return { k: "array", elem: typeShape(file, node.typeArguments.nodes[0]!, bindings) };
}
if ((node.typeArguments?.nodes.length ?? 0) > 0)
return { k: "unsupported", text: node.getText(file) };
return { k: "unsupported", text: ts.getTextOfNode(node) };
return { k: "ref", name };
}
return { k: "unsupported", text: node.getText(file) };
return { k: "unsupported", text: ts.getTextOfNode(node) };
}
/** Unwrap `expr as const` / parenthesized initializers. */
@@ -362,7 +366,7 @@ function envMsgArray(expr: ts.Expression): { env: string; msg: string }[] | null
/** One mergeable declaration site, `file:line` (1-based line). */
function siteOf(file: ts.SourceFile, node: ts.Node): string {
return `${file.fileName}:${ts.getLineAndCharacterOfPosition(file, node.getStart()).line + 1}`;
return `${file.fileName}:${ts.getLineAndCharacterOfPosition(file, ts.tokenStartOfNode(node)).line + 1}`;
}
/** Everything the sidecar projection needs from the program's syntax
@@ -35,7 +35,7 @@ export function entryFunctionExports(entry: ts.SourceFile): Map<string, EntryExp
if (out.has(name) && stmt.body === undefined) continue; // overload signature
out.set(name, {
name,
loc: { file: entry.fileName, start: stmt.getStart(), end: stmt.end },
loc: { file: entry.fileName, start: ts.tokenStartOfNode(stmt), end: stmt.end },
generic: (stmt.typeParameters?.nodes.length ?? 0) > 0,
async: mods?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true,
generator: stmt.asteriskToken !== undefined,
@@ -194,7 +194,7 @@ function onlyMethodCallReferences(lowerer: Lowerer, name: ts.Identifier): boolea
}
ts.forEachChild(node, visit);
};
visit(name.getSourceFile());
visit(ts.getSourceFileOfNode(name));
return ok;
}
@@ -79,7 +79,7 @@ export function extendAssertedRecordMembers(
symbol.declarations.length > 0 &&
symbol.declarations.every(
(d) =>
!lowerer.isStdlibFile(d.getSourceFile()) &&
!lowerer.isStdlibFile(ts.getSourceFileOfNode(d)) &&
(ts.isTypeLiteralNode(d) ||
ts.isInterfaceDeclaration(d) ||
ts.isTypeAliasDeclaration(d)),
@@ -16,7 +16,7 @@ export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: t
let written = false;
// Text pre-check keeps the file walk cheap: only same-named
// identifiers pay a symbol resolution.
const symText = sym.name;
const symText = ts.symbolName(sym);
const namesSym = (e: ts.Node): boolean =>
ts.isIdentifier(e) && e.text === symText && lowerer.resolveValueSymbol(e) === sym;
const scanTarget = (t: ts.Expression): void => {
@@ -31,7 +31,7 @@ export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: t
if (ts.isArrayLiteralExpression(e) || ts.isObjectLiteralExpression(e)) {
const walk = (n: ts.Node): void => {
if (namesSym(n)) written = true;
else n.forEachChild(walk);
else ts.forEachChild(n, walk);
};
walk(e);
}
@@ -54,9 +54,9 @@ export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: t
) {
scanTarget(n.initializer as ts.Expression);
}
n.forEachChild(visit);
ts.forEachChild(n, visit);
};
decl.getSourceFile().forEachChild(visit);
ts.forEachChild(ts.getSourceFileOfNode(decl), visit);
lowerer.neverReassignedCache.set(sym, !written);
return !written;
}
@@ -123,7 +123,7 @@ export function nullishValueUnitOf(
!decl ||
!ts.isVariableDeclaration(decl) ||
!ts.isIdentifier(decl.name) ||
decl.getSourceFile().isDeclarationFile ||
ts.getSourceFileOfNode(decl).isDeclarationFile ||
decl.initializer === undefined ||
!ts.isVariableStatement(decl.parent?.parent)
) {
@@ -154,7 +154,7 @@ export function nullishValueUnitOf(
/** True when any identifier resolving to `sym` sits inside a class
* heritage clause anywhere in the program. */
function usedInHeritageClause(lowerer: Lowerer, sym: ts.Symbol): boolean {
const symText = sym.name;
const symText = ts.symbolName(sym);
let found = false;
const visit = (n: ts.Node): void => {
if (found) return;
@@ -171,12 +171,12 @@ function usedInHeritageClause(lowerer: Lowerer, sym: ts.Symbol): boolean {
}
return;
}
n.forEachChild(visit);
ts.forEachChild(n, visit);
};
for (const file of lowerer.program.getImplementationSourceFiles()) {
if (found) break;
if (file.isDeclarationFile) continue;
file.forEachChild(visit);
ts.forEachChild(file, visit);
}
return found;
}
@@ -236,7 +236,7 @@ export function nullishGenericBindingUnitOf(
* value knowable. Compound assignments, ++/--, for-in/of cursors, and
* destructuring targets all disqualify. */
function allWritesNullish(lowerer: Lowerer, sym: ts.Symbol, decl: ts.Node): boolean {
const symText = sym.name;
const symText = ts.symbolName(sym);
const namesSym = (e: ts.Node): boolean =>
ts.isIdentifier(e) && e.text === symText && lowerer.resolveValueSymbol(e) === sym;
let ok = true;
@@ -253,7 +253,7 @@ function allWritesNullish(lowerer: Lowerer, sym: ts.Symbol, decl: ts.Node): bool
} else if (ts.isArrayLiteralExpression(lhs) || ts.isObjectLiteralExpression(lhs)) {
const walk = (m: ts.Node): void => {
if (namesSym(m)) ok = false;
else m.forEachChild(walk);
else ts.forEachChild(m, walk);
};
walk(lhs);
}
@@ -274,9 +274,9 @@ function allWritesNullish(lowerer: Lowerer, sym: ts.Symbol, decl: ts.Node): bool
t = (t as ts.ParenthesizedExpression).expression;
if (namesSym(t)) ok = false;
}
n.forEachChild(visit);
ts.forEachChild(n, visit);
};
decl.getSourceFile().forEachChild(visit);
ts.forEachChild(ts.getSourceFileOfNode(decl), visit);
return ok;
}
@@ -332,7 +332,7 @@ export function deadUnmappableBinding(
// the no-storage family never claims it (catch bindings sit outside a
// variable statement too and stay out the same way).
if (!ts.isVariableStatement(decl.parent?.parent)) return false;
const sf = decl.getSourceFile();
const sf = ts.getSourceFileOfNode(decl);
if (sf.isDeclarationFile || isJsSourceFile(sf)) return false;
if (decl.initializer !== undefined && !sideEffectFreeValueExpr(lowerer, decl.initializer))
return false;
@@ -344,7 +344,7 @@ export function deadUnmappableBinding(
// bug), so only survivors of the syntactic filters pay it. Mappable
// types keep their real storage.
if (lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(sym)) !== null) return false;
const symText = sym.name;
const symText = ts.symbolName(sym);
const namesSym = (e: ts.Node): boolean =>
ts.isIdentifier(e) && e.text === symText && lowerer.resolveValueSymbol(e) === sym;
let dead = true;
@@ -353,7 +353,7 @@ export function deadUnmappableBinding(
if (ts.isIdentifier(n) && n.text === symText) {
// Declaration-name occurrences are not reads.
if (n.parent !== undefined && ts.isVariableDeclaration(n.parent) && n.parent.name === n) {
n.forEachChild(visit);
ts.forEachChild(n, visit);
return;
}
// A plain-assignment LHS is a WRITE — dead only when the RHS
@@ -375,12 +375,12 @@ export function deadUnmappableBinding(
if (namesSym(n)) dead = false;
return;
}
n.forEachChild(visit);
ts.forEachChild(n, visit);
};
for (const file of lowerer.program.getImplementationSourceFiles()) {
if (!dead) break;
if (file.isDeclarationFile) continue;
file.forEachChild(visit);
ts.forEachChild(file, visit);
}
if (dead) lowerer.deadBindings.add(sym);
return dead;
@@ -25,7 +25,7 @@ export function builtinFunctionFenceValue(
`library functions as values ('${display}' has no function-value lowering; call it directly)`,
);
lowerer.runtimeFences.push(diag);
const line = ts.getLineAndCharacterOfPosition(node.getSourceFile(), loc.start).line + 1;
const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(node), loc.start).line + 1;
lowerer.liftedFns.push({
name,
params: [],
@@ -35,7 +35,7 @@ export function lowerDiagnosticsSubscriber(lowerer: Lowerer, node: ts.Expression
if (ts.isIdentifier(node) && node.text === "setImmediate") {
const sym = lowerer.checker.getSymbolAtLocation(node);
const decls = sym ? sym.declarations : [];
if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(d.getSourceFile()))) {
if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d)))) {
return {
kind: "libCall",
fn: "timers.setImmediateFnValue",
@@ -52,10 +52,10 @@ export function blobClassOf(lowerer: Lowerer, node: ts.Expression): BlobClass |
function blobReceiverClass(lowerer: Lowerer, node: ts.Expression): BlobClass | null {
const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node));
const sym = type.getAliasSymbol() ?? type.getSymbol();
if (!sym || (sym.name !== "Blob" && sym.name !== "File") || !lowerer.isStdlibSymbol(sym)) {
return null;
}
return sym.name;
if (!sym) return null;
const name = ts.symbolName(sym);
if ((name !== "Blob" && name !== "File") || !lowerer.isStdlibSymbol(sym)) return null;
return name;
}
function memberNameOf(access: ts.PropertyAccessExpression | ts.ElementAccessExpression) {
@@ -111,8 +111,18 @@ export function builtinClassDynValueOf(lowerer: Lowerer, node: ts.Expression): I
};
}
/** EventEmitter and the legacy Stream are dyn.builtinClass; the stream
* classes, which carry native stream state for constructor functions, are
* dyn.builtinStreamClass, so only programs that use them link the stream
* half. */
export function builtinClassValue(kind: number, loc: SrcLoc): IrExpr {
return { kind: "libCall", fn: "dyn.builtinClass", args: [numLit(kind, loc)], type: DYN, loc };
return {
kind: "libCall",
fn: kind <= 1 ? "dyn.builtinClass" : "dyn.builtinStreamClass",
args: [numLit(kind, loc)],
type: DYN,
loc,
};
}
/** `Base.call(this, …)` / `Base.apply(this, args)` on one of those class
@@ -130,7 +140,7 @@ export function lowerBuiltinClassCall(lowerer: Lowerer, call: ts.CallExpression)
kind: "dynInvoke",
recv: receiver,
method,
calleeName: callee.getText(),
calleeName: ts.getTextOfNode(callee),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
@@ -50,7 +50,7 @@ export function registerBuiltinCallableAlias(
// These checked native functions retain a runtime slot in JavaScript so
// aliases can escape without narrowing their arguments to string.
if (
isJsSourceFile(nameNode.getSourceFile()) &&
isJsSourceFile(ts.getSourceFileOfNode(nameNode)) &&
target.module === "util" &&
["stripVTControlCharacters", "toUSVString", "isDeepStrictEqual", "styleText"].includes(
target.member,
@@ -122,12 +122,14 @@ export function lowerChildMethodCall(
}
const listenerNode = stripTypeCasts(call.arguments.nodes[1]!);
if (
isJsSourceFile(listenerNode.getSourceFile()) &&
isJsSourceFile(ts.getSourceFileOfNode(listenerNode)) &&
(ts.isArrowFunction(listenerNode) || ts.isFunctionExpression(listenerNode)) &&
listenerNode.parameters.nodes.some((param) => param.dotDotDotToken) &&
listenerNode.parameters.nodes.every((param) => !param.type) &&
!/@(?:param|type)\b/.test(
listenerNode.getSourceFile().text.slice(listenerNode.pos, listenerNode.getStart()),
ts
.getSourceFileOfNode(listenerNode)
.text.slice(listenerNode.pos, ts.tokenStartOfNode(listenerNode)),
)
) {
for (const param of listenerNode.parameters.nodes) lowerer.checkedCallbackParams.add(param);
@@ -142,10 +144,10 @@ export function lowerChildMethodCall(
: lowerer.lowerExpr(call.arguments.nodes[1]!);
if (
narrowed.length > 0 ||
(isJsSourceFile(listenerNode.getSourceFile()) && cb.type.kind === "dyn") ||
(isJsSourceFile(ts.getSourceFileOfNode(listenerNode)) && cb.type.kind === "dyn") ||
(cb.type.kind === "func" &&
(cb.type.rest ||
(isJsSourceFile(listenerNode.getSourceFile()) &&
(isJsSourceFile(ts.getSourceFileOfNode(listenerNode)) &&
cb.type.params.some((param) => param.kind === "dyn"))))
) {
if (
@@ -756,7 +758,7 @@ function childStreamEncodedAt(
* admit null. */
function narrowedExitListenerParams(lowerer: Lowerer, listener: ts.Expression): ts.Symbol[] {
if (!ts.isArrowFunction(listener) && !ts.isFunctionExpression(listener)) return [];
if (isJsSourceFile(listener.getSourceFile())) return [];
if (isJsSourceFile(ts.getSourceFileOfNode(listener))) return [];
const symbols: ts.Symbol[] = [];
const wanted: readonly string[] = ["f64", "string"];
listener.parameters.nodes.slice(0, 2).forEach((param, i) => {
@@ -97,7 +97,7 @@ export function builtinClassAliasOwnsNoSlot(lowerer: Lowerer, symbol: ts.Symbol)
if (decl === undefined || !ts.isVariableDeclaration(decl)) return false;
if (builtinMemberRequireDecl(decl.name, decl.initializer)) return false;
if (streamClassAliasDecl(lowerer, decl.name, decl.initializer)) return false;
if (!isJsSourceFile(decl.getSourceFile())) return true;
if (!isJsSourceFile(ts.getSourceFileOfNode(decl))) return true;
let init = decl.initializer;
while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
if (init === undefined) return true;
@@ -157,9 +157,10 @@ export function runtimeClassValueOfType(lowerer: Lowerer, type: ts.Type): string
const symbol = type.getSymbol();
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
let info = lowerer.builtinEmitterInfoOf(symbol) ?? lowerer.builtinStreamInfoOf(symbol);
if (!info && symbol.name.endsWith("Constructor")) {
if (!info && ts.symbolName(symbol).endsWith("Constructor")) {
for (const [irName, rec] of RUNTIME_ERROR_CLASSES) {
if (`${rec.lib}Constructor` === symbol.name) info = lowerer.classes.get(irName) ?? null;
if (`${rec.lib}Constructor` === ts.symbolName(symbol))
info = lowerer.classes.get(irName) ?? null;
}
}
return info?.valueCtorParams !== undefined ? info.def.name : null;
@@ -136,7 +136,7 @@ export function zlibStreamModeOf(
if (alias !== null) return alias.kind === "zlib" ? alias.mode : null;
const target =
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
const mode = ZLIB_STREAM_MODES.get(target.name);
const mode = ZLIB_STREAM_MODES.get(ts.symbolName(target));
if (mode === undefined) return null;
return target.declarations.some((d) => ts.isClassDeclaration(d) && isZlibFallbackDecl(lowerer, d))
? mode
@@ -144,7 +144,7 @@ export function zlibStreamModeOf(
}
function isZlibFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean {
const file = d.getSourceFile();
const file = ts.getSourceFileOfNode(d);
if (!lowerer.isStdlibFile(file) || isNodeTypesPath(file.fileName)) return false;
for (let node: ts.Node | undefined = d.parent; node; node = node.parent) {
if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
@@ -170,7 +170,7 @@ export function fsConstantMemberRead(expr: ts.PropertyAccessExpression): IrExpr
type: DYN,
loc,
};
return isJsSourceFile(expr.getSourceFile())
return isJsSourceFile(ts.getSourceFileOfNode(expr))
? read
: { kind: "dynCheck", value: read, type: F64, loc };
}
@@ -240,7 +240,9 @@ function cryptoKdfInput(lowerer: Lowerer, node: ts.Expression): IrExpr {
const symbol = lowerer.typeOf(node).getSymbol();
if (
type?.kind === "bytes" ||
(symbol?.name === "ArrayBuffer" && lowerer.isStdlibSymbol(symbol))
(symbol !== undefined &&
ts.symbolName(symbol) === "ArrayBuffer" &&
lowerer.isStdlibSymbol(symbol))
) {
const value = lowerer.coerceInto(node, lowerer.lowerExpr(node), DYN);
return { kind: "libCall", fn: "bytes.bufferSource", args: [value], type: BYTES_U8, loc };
@@ -878,8 +880,8 @@ export function isCipherType(lowerer: Lowerer, type: ts.Type): boolean {
const symbol = type.getAliasSymbol() ?? type.getSymbol();
return (
!!symbol &&
CIPHER_TYPE_NAMES.has(symbol.name) &&
symbol.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile()))
CIPHER_TYPE_NAMES.has(ts.symbolName(symbol)) &&
symbol.declarations.some((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d)))
);
}
@@ -898,7 +900,7 @@ function v8CalleeText(node: ts.Expression): string {
if (ts.isPropertyAccessExpression(node))
return `${v8CalleeText(node.expression)}.${node.name.text}`;
if (ts.isCallExpression(node)) return `${v8CalleeText(node.expression)}(...)`;
return node.getText();
return ts.getTextOfNode(node);
}
/** Calls on native Cipheriv/Decipheriv handles dispatch through the
@@ -959,7 +961,7 @@ export function lowerCryptoHashMethodCall(
return {
kind: "seqExpr",
stmts: evaluated,
result: nodeThrowExpr(1, "", `${access.getText()} is not a function`, DYN, loc),
result: nodeThrowExpr(1, "", `${ts.getTextOfNode(access)} is not a function`, DYN, loc),
type: DYN,
loc,
};
@@ -9,7 +9,12 @@ import { varRef } from "../../../ir/build.js";
function isStdlibDisposableStack(lowerer: Lowerer, node: ts.Expression): boolean {
const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node));
for (const symbol of [type.getSymbol(), type.getAliasSymbol()]) {
if (symbol?.name === "DisposableStack" && lowerer.isStdlibSymbol(symbol)) return true;
if (
symbol !== undefined &&
ts.symbolName(symbol) === "DisposableStack" &&
lowerer.isStdlibSymbol(symbol)
)
return true;
}
return false;
}
@@ -27,9 +27,10 @@ const ASSERTION_ERROR_PROPERTIES: ReadonlySet<string> = new Set([
export function isExecExceptionType(lowerer: Lowerer, t: ts.Type): boolean {
const sym = t.getSymbol();
return (
sym?.name === "ExecException" &&
sym !== undefined &&
ts.symbolName(sym) === "ExecException" &&
sym.declarations.some(
(d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
(d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
)
);
}
@@ -250,7 +250,7 @@ export function lowerFsLadderCall(
);
}
if (bi.module !== "fs" && bi.module !== "fs/promises") return null;
if (!isJsSourceFile(expr.getSourceFile())) return null;
if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null;
const args = expr.arguments.nodes;
if (args.some(ts.isSpreadElement)) return null;
// Every ladder argument crosses as a dyn value; an argument that
@@ -1564,7 +1564,7 @@ export function lowerDirentMethodCall(
if (call.questionDotToken || access.questionDotToken) return null;
if (!lowerer.isStdlibMember(access)) return null;
const recvSym = lowerer.typeOf(access.expression).getSymbol();
if (recvSym?.name !== "Dirent") return null;
if (recvSym === undefined || ts.symbolName(recvSym) !== "Dirent") return null;
const receiver = lowerer.lowerExpr(access.expression);
if (receiver.type.kind !== "record") return null;
const shape = lowerer.shapes.get(receiver.type.shapeId);
@@ -19,7 +19,7 @@ import {
} from "../../../ir/ir.js";
function isFsFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean {
const file = d.getSourceFile();
const file = ts.getSourceFileOfNode(d);
if (!lowerer.isStdlibFile(file) || isNodeTypesPath(file.fileName)) return false;
for (let node: ts.Node | undefined = d.parent; node; node = node.parent) {
if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
@@ -153,11 +153,13 @@ export function fsStreamClassOf(
lowerer: Lowerer,
symbol: ts.Symbol | null | undefined,
): "ReadStream" | "WriteStream" | null {
if (!symbol || (symbol.name !== "ReadStream" && symbol.name !== "WriteStream")) return null;
if (!symbol) return null;
const name = ts.symbolName(symbol);
if (name !== "ReadStream" && name !== "WriteStream") return null;
const declared = symbol.declarations.some(
(d) => ts.isClassDeclaration(d) && isFsFallbackDecl(lowerer, d),
);
return declared ? symbol.name : null;
return declared ? name : null;
}
/** `new fs.ReadStream(path, options)` / `new fs.WriteStream(...)`: the
@@ -504,7 +504,7 @@ function jsonNullishArgument(lowerer: Lowerer, node: ts.Expression): boolean {
const symbol = lowerer.checker.getSymbolAtLocation(node);
return (
symbol !== undefined &&
symbol.declarations.every((decl) => lowerer.isStdlibFile(decl.getSourceFile()))
symbol.declarations.every((decl) => lowerer.isStdlibFile(ts.getSourceFileOfNode(decl)))
);
}
@@ -23,7 +23,7 @@ export function isKeyObjectClass(lowerer: Lowerer, node: ts.Expression): boolean
export function isKeyObjectValue(lowerer: Lowerer, node: ts.Expression): boolean {
const type = lowerer.typeOf(node);
const sym = type.getAliasSymbol() ?? type.getSymbol();
return !!sym && sym.name === "KeyObject" && lowerer.isStdlibSymbol(sym);
return !!sym && ts.symbolName(sym) === "KeyObject" && lowerer.isStdlibSymbol(sym);
}
/** `v instanceof KeyObject`. */
@@ -51,7 +51,7 @@ export function lowerKeyObjectCall(lowerer: Lowerer, call: ts.CallExpression): I
kind: "dynInvoke",
recv: lowerer.lowerExprExpecting(access.expression, DYN),
method: access.name.text,
calleeName: access.getText(),
calleeName: ts.getTextOfNode(access),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc: locOf(call),
@@ -135,7 +135,7 @@ function builtinModuleBindingOf(
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
const target = lowerer.checker.getAliasedSymbol(symbol);
const tdecl = target.declarations[0];
if (tdecl !== undefined && tdecl.getSourceFile().isDeclarationFile) {
if (tdecl !== undefined && ts.getSourceFileOfNode(tdecl).isDeclarationFile) {
for (
let p: ts.Node | undefined = tdecl.parent;
p !== undefined && !ts.isSourceFile(p);
@@ -143,7 +143,7 @@ function builtinModuleBindingOf(
) {
if (ts.isModuleDeclaration(p) && ts.isStringLiteral(p.name)) {
const module = canonicalBuiltinModule(p.name.text);
if (module !== null) return { module, member: target.name };
if (module !== null) return { module, member: ts.symbolName(target) };
break;
}
}
@@ -297,14 +297,14 @@ export function createRequireCalleeFileOf(
): ts.SourceFile | null {
const e = stripTypeCasts(callee);
if (ts.isCallExpression(e)) {
return createRequireBaseCallOf(lowerer, e) !== null ? e.getSourceFile() : null;
return createRequireBaseCallOf(lowerer, e) !== null ? ts.getSourceFileOfNode(e) : null;
}
if (!ts.isIdentifier(e)) return null;
const symbol = lowerer.checker.getSymbolAtLocation(e);
const decl = symbol ? symbol.declarations[0] : undefined;
if (!decl || !ts.isVariableDeclaration(decl) || decl.initializer === undefined) return null;
return createRequireBindingDecl(lowerer, decl.name, decl.initializer)
? decl.getSourceFile()
? ts.getSourceFileOfNode(decl)
: null;
}
@@ -57,7 +57,7 @@ export function lowerBuiltinModuleCall(
fn: BuiltinModuleFn,
loc: SrcLoc,
): IrExpr {
const name = expr.expression.getText();
const name = ts.getTextOfNode(expr.expression);
if (bi.module === "util/types") {
return lowerUtilTypeCall(lowerer, expr, bi, loc, name);
}
@@ -292,7 +292,7 @@ export function lowerImportMetaResolveCall(
);
}
const result = resolveImportMetaRuntime(
call.getSourceFile().fileName,
ts.getSourceFileOfNode(call).fileName,
specifier,
lowerer.targetPlatform,
lowerer.frontendServices,
@@ -314,8 +314,8 @@ function requireResolverBaseFile(lowerer: Lowerer, receiver: ts.Expression): ts.
const created = createRequireCalleeFileOf(lowerer, receiver);
if (created !== null) return created;
return lowerer.isStdlibGlobal(receiver, "require") &&
!isNodeEsmFile(receiver.getSourceFile(), lowerer.program)
? receiver.getSourceFile()
!isNodeEsmFile(ts.getSourceFileOfNode(receiver), lowerer.program)
? ts.getSourceFileOfNode(receiver)
: null;
}
@@ -20,7 +20,8 @@ export function isNavigatorGlobal(lowerer: Lowerer, node: ts.Expression): boolea
if (!ts.isIdentifier(expr) || expr.text !== "navigator") return false;
if (lowerer.isStdlibGlobal(expr, "navigator")) return true;
return (
isJsSourceFile(expr.getSourceFile()) && lowerer.checker.getSymbolAtLocation(expr) === undefined
isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
lowerer.checker.getSymbolAtLocation(expr) === undefined
);
}
@@ -31,8 +31,10 @@ export function netAddressClassOf(lowerer: Lowerer, node: ts.Expression): NetAdd
function netAddressValueClass(lowerer: Lowerer, node: ts.Expression): NetAddressClass | null {
const type = lowerer.typeOf(node);
const sym = type.getAliasSymbol() ?? type.getSymbol();
if (!sym || (sym.name !== "SocketAddress" && sym.name !== "BlockList")) return null;
return lowerer.isStdlibSymbol(sym) ? sym.name : null;
if (!sym) return null;
const name = ts.symbolName(sym);
if (name !== "SocketAddress" && name !== "BlockList") return null;
return lowerer.isStdlibSymbol(sym) ? name : null;
}
/** `new SocketAddress(options?)` and `new BlockList()`: the argument
@@ -127,7 +129,7 @@ export function lowerNetAddressCall(lowerer: Lowerer, call: ts.CallExpression):
kind: "dynInvoke",
recv: lowerer.lowerExprExpecting(access.expression, DYN),
method: name,
calleeName: access.getText(),
calleeName: ts.getTextOfNode(access),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
@@ -285,7 +285,7 @@ function lowerPerfArguments(lowerer: Lowerer, call: ts.CallExpression, loc: SrcL
});
continue;
}
const spelling = arg.expression.getText();
const spelling = ts.getTextOfNode(arg.expression);
const items = checkedIterableSpread(lowerer, lowerer.lowerExpr(arg.expression), spelling, loc);
stmts.push({
kind: "exprStmt",
@@ -527,7 +527,11 @@ export function fencePerfEntryKeyWalk(
* receiver's checker type. Its value is the runtime histogram handle. */
function isIntervalHistogram(lowerer: Lowerer, node: ts.Expression): boolean {
const symbol = lowerer.typeOf(node).getSymbol();
return symbol?.name === "IntervalHistogram" && lowerer.isStdlibSymbol(symbol);
return (
symbol !== undefined &&
ts.symbolName(symbol) === "IntervalHistogram" &&
lowerer.isStdlibSymbol(symbol)
);
}
/** IntervalHistogram's numeric members, as perf.eldStat selectors. */
@@ -192,14 +192,14 @@ export function lowerProcStreamMethodCall(
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
const loc = locOf(call);
if (isJsSourceFile(call.getSourceFile())) {
if (isJsSourceFile(ts.getSourceFileOfNode(call))) {
const receiver = lowerer.lowerExpr(access.expression);
if (receiver.type.kind === "dyn") {
return {
kind: "dynInvoke",
recv: receiver,
method: name,
calleeName: call.expression.getText(),
calleeName: ts.getTextOfNode(call.expression),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
@@ -214,7 +214,7 @@ export function lowerProcStreamMethodCall(
kind: "dynInvoke",
recv: receiver,
method: name,
calleeName: call.expression.getText(),
calleeName: ts.getTextOfNode(call.expression),
args: [],
type: DYN,
loc,
@@ -266,6 +266,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<string> = new Set([
"http_parser",
"v8",
"ares",
"uv",
"zlib",
"modules",
]);
/** A process.versions member read whose declaration promises a string the
* runtime does not have. @types/node declares the entries above as plain
* strings, so a checked string read would throw on the absent entry and
* equality with undefined would fold to false. These reads keep the
* undefined arm, like the optional stream properties. The shipped fallback
* types declare other entries `string | undefined` and never reach this.
* An element read with a computed key qualifies when the checker types it
* as a plain string. */
export function isOptionalProcessVersionsRead(lowerer: Lowerer, node: ts.Node): boolean {
let key: string | null;
if (ts.isPropertyAccessExpression(node)) key = node.name.text;
else if (ts.isElementAccessExpression(node))
key = ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null;
else return false;
if (key !== null && !ABSENT_DECLARED_VERSIONS.has(key)) return false;
if (!programMentionsVersions(lowerer)) return false;
const symbol = lowerer.typeOf(node.expression).getSymbol();
return (
symbol !== undefined &&
ts.symbolName(symbol) === "ProcessVersions" &&
symbol.declarations.some(
(d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
) &&
lowerer.mapTypeOf(lowerer.typeOf(node))?.kind === "string"
);
}
const mentionsVersions = new WeakMap<Lowerer, boolean>();
/** Whether any implementation source spells `versions`. Programs that
* never do skip the per-read checker queries above entirely. */
function programMentionsVersions(lowerer: Lowerer): boolean {
let known = mentionsVersions.get(lowerer);
if (known === undefined) {
known = lowerer.program
.getImplementationSourceFiles()
.some((sf) => !sf.isDeclarationFile && sf.text.includes("versions"));
mentionsVersions.set(lowerer, known);
}
return known;
}
export function lowerProcessStreamProperty(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
@@ -276,7 +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,
@@ -6,6 +6,7 @@ import { varRef } from "../../../ir/build.js";
import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js";
import { isJsSourceFile, locOf } from "../../program.js";
import {
BOOL,
DYN,
type IrExpr,
type IrFunction,
@@ -187,7 +188,11 @@ function lowerTuplePromiseAll(
* type is the checker's combined promise, and each entry's inner type
* must be that inner type, one of its union arms, or a sub-union of it
* (the backend's interned adapters wrap/re-tag fulfillments; a wider
* entry would need machinery that doesn't exist and fences).
* entry would need machinery that doesn't exist and fences). A
* Promise<never> entry fits any result, and an entry whose payload widens
* into the result's converts. When the checker's combined type has no
* representation, the result takes the type the entries that can fulfill
* share, or 'unknown'.
* Promise.all/allSettled/any fence with the sequential-await hint;
* resolve/reject and the rest fall to the member fence. Null for
* non-Promise receivers. */
@@ -214,14 +219,47 @@ export function lowerPromiseStaticCall(
"a non-empty array LITERAL of promises is the supported form: Promise.race([p, q])",
);
}
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
if (resultT?.kind !== "promise") {
lowerer.noLowering(
"Promise.race with this combined result type",
call,
"the entries' value types must combine into a representable union",
);
// A Promise<never> entry (the deadline idiom: `new Promise<never>((_,
// reject) => setTimeout(reject, ms))`) can only reject, so it races
// under any result type; its inner type maps to void.
const neverEntry = (el: ts.Expression): boolean => {
const t = lowerer.typeOf(el);
const arg =
ts.symbolNameOf(t.getSymbol()) === "Promise"
? lowerer.checker.getTypeArguments(t as ts.TypeReference)[0]
: undefined;
return arg !== undefined && (arg.flags & ts.TypeFlags.Never) !== 0;
};
const elements = argNode.elements.nodes;
let combined = lowerer.mapTypeOf(lowerer.typeOf(call));
let lowered: IrExpr[] | null = null;
if (combined?.kind !== "promise") {
// The checker's combined type has no representation (an 'any'
// element array such as events.once's any[], or 'void | any[]'): the
// result takes the one type the entries that can fulfill share, or
// else 'unknown' when each of them widens into it.
lowered = elements.map((el) => lowerer.lowerExpr(el));
const fulfilling = lowered.filter((_, i) => !neverEntry(elements[i]!));
const first = fulfilling[0]?.type;
const dynResult: IrType & { kind: "promise" } = { kind: "promise", inner: DYN };
if (first?.kind === "promise" && fulfilling.every((e) => typeEquals(e.type, first))) {
combined = first;
} else if (
fulfilling.length > 0 &&
fulfilling.every(
(e) => e.type.kind === "promise" && lowerer.promisePayloadWidens(e.type, dynResult),
)
) {
combined = dynResult;
} else {
lowerer.noLowering(
"Promise.race with this combined result type",
call,
"the entries' value types must combine into a representable union",
);
}
}
const resultT = combined;
const resultArms =
resultT.inner.kind === "union"
? (lowerer.unions.get(resultT.inner.unionId)?.arms ?? [])
@@ -238,19 +276,8 @@ export function lowerPromiseStaticCall(
}
return resultArms.some((a) => typeEquals(a, inner));
};
// A Promise<never> entry (the deadline idiom: `new Promise<never>((_,
// reject) => setTimeout(reject, ms))`) can only reject, so it races
// under any result type; its inner type maps to void.
const neverEntry = (el: ts.Expression): boolean => {
const t = lowerer.typeOf(el);
const arg =
t.getSymbol()?.name === "Promise"
? lowerer.checker.getTypeArguments(t as ts.TypeReference)[0]
: undefined;
return arg !== undefined && (arg.flags & ts.TypeFlags.Never) !== 0;
};
const entries = argNode.elements.nodes.map((el) => {
const entry = lowerer.lowerExpr(el);
const entries = elements.map((el, i) => {
const entry = lowered?.[i] ?? lowerer.lowerExpr(el);
if (entry.type.kind !== "promise") {
lowerer.noLowering(
"Promise.race over non-promise entries",
@@ -259,6 +286,10 @@ export function lowerPromiseStaticCall(
);
}
if (!compatible(entry.type.inner) && !(entry.type.inner.kind === "void" && neverEntry(el))) {
// A payload that widens into the result's (into 'unknown', or a
// checked conversion) keeps its promise and settlement turn.
if (lowerer.promisePayloadWidens(entry.type, resultT))
return lowerer.coerceToExpected(entry, resultT);
lowerer.unsupported(
"SC1090",
el,
@@ -318,12 +349,12 @@ export function lowerPromiseStaticCall(
return { kind: "intrinsic", name: "promise.all", args: [entriesArr], type: resultT, loc };
}
if (!isJsSourceFile(call.getSourceFile())) {
if (!isJsSourceFile(ts.getSourceFileOfNode(call))) {
const tuple = lowerTuplePromiseAll(lowerer, call, elems);
if (tuple) return tuple;
}
if (
isJsSourceFile(call.getSourceFile()) &&
isJsSourceFile(ts.getSourceFileOfNode(call)) &&
elems.every((entry) => lowerer.dynConvertible(entry.type))
) {
return checkedPromiseAll(
@@ -344,7 +375,7 @@ export function lowerPromiseStaticCall(
);
}
const entries = lowerer.lowerExpr(argNode);
if (isJsSourceFile(call.getSourceFile()) && entries.type.kind === "dyn")
if (isJsSourceFile(ts.getSourceFileOfNode(call)) && entries.type.kind === "dyn")
return checkedPromiseAll(lowerer, entries, loc);
// loadPlugin(p)))` — the loadPlugins shape, where the checker
@@ -403,7 +434,7 @@ export function lowerPromiseStaticCall(
`one iterable of promises is the supported form: Promise.${member}(ps)`,
);
}
if (isJsSourceFile(call.getSourceFile())) {
if (isJsSourceFile(ts.getSourceFileOfNode(call))) {
// JavaScript entries are checked-dynamic: any iterable, thenables
// and plain values included, exactly like Promise.all.
if (
@@ -670,6 +701,32 @@ export function lowerPromiseStaticCall(
lowerer.noLowering("Promise.resolve at a unit-typed promise", call);
}
const value = lowerer.lowerExprExpecting(argNode, resultT.inner);
// In JavaScript, a checked-dynamic argument may itself hold a promise
// (a TypeScript async callback's result: `Promise.resolve(fn())`):
// Promise.resolve answers THAT promise — the same object, adopted —
// and wraps every other value.
if (
isJsSourceFile(ts.getSourceFileOfNode(call)) &&
value.type.kind === "dyn" &&
resultT.inner.kind === "dyn"
) {
const local = lowerer.declareHiddenLocal("%resolveValue", DYN);
const ref: IrExpr = { kind: "varRef", localId: local.id, type: DYN, loc };
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }],
result: {
kind: "ternary",
cond: { kind: "dynTest", test: "promise", value: ref, type: BOOL, loc },
then: { kind: "dynCheck", value: ref, type: resultT, loc },
else_: { kind: "intrinsic", name: "promise.resolve", args: [ref], type: resultT, loc },
type: resultT,
loc,
},
type: resultT,
loc,
};
}
return { kind: "intrinsic", name: "promise.resolve", args: [value], type: resultT, loc };
}
return null; // reject lands on lowerPromiseRejectCall / the member fence
@@ -290,6 +290,9 @@ function generalChildStdioType(
const className = name === "stdin" ? "%Writable" : "%Readable";
const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (!mapped) return null;
// Checked-dynamic stream slots: the declared stream type maps to dyn
// (dyn-streams.ts); the read still produces the bridged stream.
if (lowerer.dynNodeStreams && mapped.kind === "dyn") return { kind: "object", className };
const arms =
mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped];
return arms.some((arm) => arm.kind === "object" && arm.className === className)
@@ -46,10 +46,10 @@ function isReadlineModule(spec: string | null): boolean {
export function isReadlineTyped(lowerer: Lowerer, node: ts.Expression): boolean {
const t = lowerer.checker.getTypeAtLocation(node);
const sym = t.getAliasSymbol() ?? t.getSymbol();
if (sym?.name !== "Interface") return false;
if (sym === undefined || ts.symbolName(sym) !== "Interface") return false;
return sym.declarations.some((d) => {
if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false;
if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) return false;
return isReadlineModule(ambientModuleOf(d));
});
}
@@ -74,7 +74,7 @@ const RL_EVENT_LIST =
* readline/promises question. */
function returnsPromise(lowerer: Lowerer, call: ts.CallExpression): boolean {
const t = lowerer.checker.getTypeAtLocation(call);
return (t.getSymbol()?.name ?? "") === "Promise";
return (ts.symbolNameOf(t.getSymbol()) ?? "") === "Promise";
}
/** The `{ signal }` options argument of the promise question: the signal
@@ -107,10 +107,10 @@ function questionSignal(lowerer: Lowerer, node: ts.Expression | undefined): ts.E
function isReadlineUiTyped(lowerer: Lowerer, node: ts.Expression): boolean {
const t = lowerer.checker.getTypeAtLocation(node);
const sym = t.getAliasSymbol() ?? t.getSymbol();
if (sym?.name !== "Readline") return false;
if (sym === undefined || ts.symbolName(sym) !== "Readline") return false;
return sym.declarations.some((d) => {
if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false;
if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) return false;
const spec = ambientModuleOf(d);
return spec === "readline/promises" || spec === "node:readline/promises";
});
@@ -10,10 +10,10 @@ export function isBuiltinClassInstance(
): boolean {
const type = lowerer.checker.getTypeAtLocation(node);
const symbol = type.getAliasSymbol() ?? type.getSymbol();
if (symbol?.name !== name) return false;
if (symbol === undefined || ts.symbolName(symbol) !== name) return false;
return symbol.declarations.some(
(declaration) =>
(ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) &&
lowerer.isStdlibFile(declaration.getSourceFile()),
lowerer.isStdlibFile(ts.getSourceFileOfNode(declaration)),
);
}

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