mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Merge main
This commit is contained in:
+98
-25
@@ -136,6 +136,8 @@ jobs:
|
||||
run: pnpm generate:ts7-ast --check
|
||||
- name: Verify the generated IR field readers
|
||||
run: pnpm generate:ir-fields --check
|
||||
- name: Verify the generated IR codec
|
||||
run: pnpm generate:ir-codec --check
|
||||
|
||||
native_macos:
|
||||
needs: [validation_scope, lanes]
|
||||
@@ -464,6 +466,10 @@ jobs:
|
||||
name: native-linux-x64-gnu-shape-overlay
|
||||
path: ${{ runner.temp }}
|
||||
- run: tar -xzf "$RUNNER_TEMP/native-linux-x64-gnu.tar.gz"
|
||||
# The npm suites' Node island cases load Node from a libnode pack, and
|
||||
# fail rather than skip in this lane when none is configured.
|
||||
- uses: ./.github/actions/libnode
|
||||
if: matrix.suite == 'npm' || matrix.suite == 'npm-static'
|
||||
- name: Run the suite with the overlay on
|
||||
run: |
|
||||
case "${{ matrix.suite }}" in
|
||||
@@ -686,11 +692,17 @@ jobs:
|
||||
run: pnpm test:checker-identity --optimization dev
|
||||
|
||||
llvm_artifacts_linux_x64:
|
||||
needs: [validation_scope, lanes]
|
||||
needs: [validation_scope, lanes, native_linux]
|
||||
if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }}
|
||||
name: test (Linux x64 LLVM artifacts, no clang)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
# The glibc helper and runtime pack come from native_linux, as the macOS
|
||||
# artifact jobs take theirs from native_macos. The musl packages and the
|
||||
# WASI smoke run in llvm_artifacts_linux_x64_musl and
|
||||
# llvm_artifacts_linux_x64_wasi: together with a rebuild of the glibc
|
||||
# packages, which a runtime change forces, the one job took 25 to 32
|
||||
# minutes.
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
@@ -700,32 +712,13 @@ jobs:
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: pnpm
|
||||
- uses: ./.github/actions/llvm-linux
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- uses: ./.github/actions/zig
|
||||
- uses: ./.github/actions/native-packages
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
target: linux-x64-gnu
|
||||
- name: Build and verify Linux x64 musl release packages
|
||||
run: |
|
||||
pnpm --filter @scriptc/llvm-linux-x64-musl build:native
|
||||
CC=zig AR=zig pnpm --filter @scriptc/runtime-linux-x64-musl build:native
|
||||
helper=packages/llvm-linux-x64-musl/bin/scriptc-llvm-codegen
|
||||
readelf -l "$helper" > "$RUNNER_TEMP/musl-helper-program-headers"
|
||||
if grep -q INTERP "$RUNNER_TEMP/musl-helper-program-headers"; then
|
||||
echo "musl helper must be a static executable" >&2
|
||||
exit 1
|
||||
fi
|
||||
docker run --rm --volume "$GITHUB_WORKSPACE:/work:ro" alpine:3.22 \
|
||||
/work/$helper version --format=json | node -e '
|
||||
const value = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
|
||||
if (value.default_target !== "x86_64-unknown-linux-musl" || !value.supported_targets?.includes("wasm32-unknown-wasi")) process.exit(1);
|
||||
'
|
||||
node packages/runtime-linux-x64-musl/scripts/verify.mjs
|
||||
- uses: ./.github/actions/native-packages
|
||||
with:
|
||||
target: wasm32-wasi
|
||||
helper: "false"
|
||||
name: native-linux-x64-gnu
|
||||
path: ${{ runner.temp }}
|
||||
- run: tar -xzf "$RUNNER_TEMP/native-linux-x64-gnu.tar.gz"
|
||||
- run: pnpm build
|
||||
- name: Linux helper object format and no-clang output contract
|
||||
run: |
|
||||
@@ -751,6 +744,82 @@ jobs:
|
||||
SCRIPTC_NO_CACHE=1 SCRIPTC_LEGACY_C_PIPELINE=0 node packages/cli/dist/main.js build tests/corpus/001-hello.ts -o /tmp/hello
|
||||
test "$(/tmp/hello)" = "hello world"
|
||||
'
|
||||
|
||||
llvm_artifacts_linux_x64_musl:
|
||||
needs: [validation_scope, lanes, native_linux]
|
||||
if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }}
|
||||
name: test (Linux x64 LLVM artifacts, no clang, musl)
|
||||
runs-on: ubuntu-24.04
|
||||
# The static musl helper and runtime pack, restored from the
|
||||
# native-packages cache when their inputs are unchanged: about 9 minutes
|
||||
# when both are built. The runtime build emits its units through the
|
||||
# host's glibc helper (runtime-unit.mjs), which comes from native_linux.
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 11.1.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: pnpm
|
||||
- uses: ./.github/actions/llvm-linux
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- uses: ./.github/actions/zig
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: native-linux-x64-gnu
|
||||
path: ${{ runner.temp }}
|
||||
- run: tar -xzf "$RUNNER_TEMP/native-linux-x64-gnu.tar.gz"
|
||||
- uses: ./.github/actions/native-packages
|
||||
with:
|
||||
target: linux-x64-musl
|
||||
- name: Verify the Linux x64 musl release helper
|
||||
run: |
|
||||
helper=packages/llvm-linux-x64-musl/bin/scriptc-llvm-codegen
|
||||
readelf -l "$helper" > "$RUNNER_TEMP/musl-helper-program-headers"
|
||||
if grep -q INTERP "$RUNNER_TEMP/musl-helper-program-headers"; then
|
||||
echo "musl helper must be a static executable" >&2
|
||||
exit 1
|
||||
fi
|
||||
docker run --rm --volume "$GITHUB_WORKSPACE:/work:ro" alpine:3.22 \
|
||||
/work/$helper version --format=json | node -e '
|
||||
const value = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
|
||||
if (value.default_target !== "x86_64-unknown-linux-musl" || !value.supported_targets?.includes("wasm32-unknown-wasi")) process.exit(1);
|
||||
'
|
||||
|
||||
llvm_artifacts_linux_x64_wasi:
|
||||
needs: [validation_scope, lanes, native_linux]
|
||||
if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }}
|
||||
name: test (Linux x64 LLVM artifacts, no clang, wasm32)
|
||||
runs-on: ubuntu-24.04
|
||||
# The wasm32-wasi runtime pack (restored from the native-packages cache
|
||||
# when unchanged; its units go through native_linux's glibc helper) and
|
||||
# the WASI smoke tests, which take 6 to 9 minutes.
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 11.1.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: pnpm
|
||||
- uses: ./.github/actions/llvm-linux
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- uses: ./.github/actions/zig
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: native-linux-x64-gnu
|
||||
path: ${{ runner.temp }}
|
||||
- run: tar -xzf "$RUNNER_TEMP/native-linux-x64-gnu.tar.gz"
|
||||
- uses: ./.github/actions/native-packages
|
||||
with:
|
||||
target: wasm32-wasi
|
||||
helper: "false"
|
||||
- run: pnpm build
|
||||
- name: WASI helper object/runtime-pack smoke
|
||||
run: pnpm test packages/cli/test/wasi-runtime-pack.test.ts tests/harness/wasm-library.test.ts
|
||||
|
||||
@@ -1120,6 +1189,8 @@ jobs:
|
||||
ts7_parity,
|
||||
checker_identity,
|
||||
llvm_artifacts_linux_x64,
|
||||
llvm_artifacts_linux_x64_musl,
|
||||
llvm_artifacts_linux_x64_wasi,
|
||||
llvm_artifacts_macos_arm64,
|
||||
llvm_artifacts_macos_arm64_contracts,
|
||||
llvm_artifacts_macos_x64,
|
||||
@@ -1154,6 +1225,8 @@ jobs:
|
||||
lane ts7_parity "${{ needs.ts7_parity.result }}" "${{ needs.lanes.outputs.linux }}"
|
||||
lane checker_identity "${{ needs.checker_identity.result }}" "${{ needs.lanes.outputs.linux }}"
|
||||
lane llvm_artifacts_linux_x64 "${{ needs.llvm_artifacts_linux_x64.result }}" "${{ needs.lanes.outputs.linux }}"
|
||||
lane llvm_artifacts_linux_x64_musl "${{ needs.llvm_artifacts_linux_x64_musl.result }}" "${{ needs.lanes.outputs.linux }}"
|
||||
lane llvm_artifacts_linux_x64_wasi "${{ needs.llvm_artifacts_linux_x64_wasi.result }}" "${{ needs.lanes.outputs.linux }}"
|
||||
lane llvm_artifacts_macos_arm64 "${{ needs.llvm_artifacts_macos_arm64.result }}" "${{ needs.lanes.outputs.macos }}"
|
||||
lane llvm_artifacts_macos_arm64_contracts "${{ needs.llvm_artifacts_macos_arm64_contracts.result }}" "${{ needs.lanes.outputs.macos }}"
|
||||
lane llvm_artifacts_macos_x64 "${{ needs.llvm_artifacts_macos_x64.result }}" "${{ needs.lanes.outputs.macos }}"
|
||||
|
||||
@@ -231,8 +231,8 @@ jobs:
|
||||
llvm_dist: x64
|
||||
use_zig: true
|
||||
runs-on: ${{ matrix.runner }}
|
||||
# The Intel macOS build took 24 minutes for 0.3.2 and passed 30 for 0.3.3.
|
||||
timeout-minutes: 45
|
||||
# The Intel macOS build took 24 minutes for 0.3.2, 40 for 0.4.5, and passed 45 for 0.4.6.
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
@@ -35,6 +35,7 @@ output/
|
||||
/packages/runtime-*/artifacts/
|
||||
/packages/runtime-*/runtime-pack.json
|
||||
/packages/runtime-*/.runtime-pack-*
|
||||
/packages/runtime-*/runtime-pack.verified.json*
|
||||
# libnode packs: built by build:native, never committed
|
||||
/packages/libnode-*/libnode.so.*
|
||||
/packages/libnode-*/libnode.*.dylib
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"entry": "workloads/stream-buffered-drain.ts",
|
||||
"args": []
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Readable streams that buffer many chunks before anyone reads them, then
|
||||
// drain: 200,000 objects pushed into an object-mode Readable and taken
|
||||
// with read() in a 'readable' handler, and 100,000 small Buffers pushed
|
||||
// into a byte-mode Readable and piped into a Writable.
|
||||
import { Readable, Writable } from "node:stream";
|
||||
|
||||
const objects = new Readable({ objectMode: true, highWaterMark: 16, read() {} });
|
||||
for (let i = 0; i < 200000; i++) objects.push({ id: i, qty: i % 9 });
|
||||
objects.push(null);
|
||||
|
||||
let taken = 0;
|
||||
let qty = 0;
|
||||
objects.on("readable", () => {
|
||||
let row: { id: number; qty: number } | null;
|
||||
while ((row = objects.read() as { id: number; qty: number } | null) !== null) {
|
||||
taken++;
|
||||
qty += row.qty;
|
||||
}
|
||||
});
|
||||
objects.on("end", () => {
|
||||
console.log(`objects taken=${taken} qty=${qty}`);
|
||||
const bytes = new Readable({ read() {} });
|
||||
for (let i = 0; i < 100000; i++) bytes.push(Buffer.from([i & 0xff, (i >> 8) & 0xff, 7]));
|
||||
bytes.push(null);
|
||||
let length = 0;
|
||||
let sum = 0;
|
||||
const sink = new Writable({
|
||||
write(chunk: Buffer, _enc, cb) {
|
||||
length += chunk.length;
|
||||
for (const b of chunk) sum = (sum + b) % 65521;
|
||||
cb();
|
||||
},
|
||||
});
|
||||
sink.on("finish", () => console.log(`bytes length=${length} sum=${sum}`));
|
||||
bytes.pipe(sink);
|
||||
});
|
||||
@@ -309,14 +309,14 @@ A profile that already holds context-sensitive counts cannot start a second phas
|
||||
|
||||
### Block counts
|
||||
|
||||
A profile matches a function only while the function's control flow hashes as it did when the profile was collected, so a change to the program or to scriptc leaves the changed functions without counts. A block-count file beside the profile, with its name and a `.blocks` extension in place of `.profdata` (`app.blocks` beside `app.profdata`), describes each function by the labels of its blocks instead. `--profile-use` reads it when it exists and annotates the functions whose control flow changed (the mismatched ones; a function the profile has no record of keeps no counts): blocks that keep their place in the function take their recorded counts, and the counts of the other blocks follow from the counts of the blocks and edges around them. The report then adds a line, and the warning counts only the functions that remain without counts:
|
||||
A profile matches a function only while the function's control flow hashes as it did when the profile was collected, so a change to the program or to scriptc leaves the changed functions without counts. A block-count file beside the profile, with its name and a `.blocks` extension in place of `.profdata` (`app.blocks` beside `app.profdata`), describes each function by the labels of its blocks instead. `--profile-use` reads it when it exists and annotates the functions whose control flow changed (the mismatched ones): blocks that keep their place in the function take their recorded counts, and the counts of the other blocks follow from the counts of the blocks and edges around them. The file also names the functions that never ran while the profile was collected, which a profile merged with `--sparse` has no record of. Those functions are compiled as cold code (an entry count of 0, optimized for size and placed with the other unlikely code), which shortens the build. A function the file does not name, for example one added since the profile was collected, keeps no counts and is optimized as without a profile. The report then adds a line, and the warning counts only the functions that remain without counts:
|
||||
|
||||
```
|
||||
scriptc: profile app-cs.profdata: 0 functions annotated, 1 mismatched, 0 without a record
|
||||
scriptc: profile app-cs.profdata: 1 function without profile counts annotated from app-cs.blocks
|
||||
```
|
||||
|
||||
A `--profile-use` build writes the block counts of the functions its profile annotates when the `SCRIPTC_RECORD_PROFILE_BLOCKS` environment variable names a path; such a build reuses no cached artifacts. For a context-sensitive profile, record them in the second phase, which the first-phase profile annotates:
|
||||
A `--profile-use` build writes the block counts of the functions its profile annotates, and the names of the functions it has no record of, when the `SCRIPTC_RECORD_PROFILE_BLOCKS` environment variable names a path; such a build reuses no cached artifacts. For a context-sensitive profile, record them in the second phase, which the first-phase profile annotates:
|
||||
|
||||
```console
|
||||
$ SCRIPTC_RECORD_PROFILE_BLOCKS=app-cs.blocks scriptc build app.ts --optimization=speed --profile-use app.profdata --profile-generate profiles-cs/%p.profraw -o app-instrumented-cs
|
||||
@@ -373,6 +373,8 @@ $ scriptc build app.ts --optimization=speed --cpu-dispatch -o app
|
||||
|
||||
The second copy makes the executable larger and the first build of the program slower, since LLVM compiles the program's code twice. Unchanged rebuilds are not affected. Both copies compute the same results: neither contracts floating-point operations into fused multiply-adds.
|
||||
|
||||
A function's `x86-64-v3` copy runs its baseline code when its optimized code has vectors of at most 128 bits, none wider, and no rounding or bit counting in a loop. In such code, `x86-64-v3` only lets LLVM's vectorizers move scalar work onto vector instructions, which can run slower. Its calls still go to the `x86-64-v3` copies of the functions it calls.
|
||||
|
||||
The `SCRIPTC_CPU_LEVEL` environment variable caps the level the executable uses, for example to test the baseline code on a newer machine. It takes a level from `0` to `4`, `baseline`, or a level name such as `x86-64-v3`; any other value selects the baseline code.
|
||||
|
||||
```console
|
||||
|
||||
@@ -100,6 +100,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/builtin-apis/file-streams.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/file-watching.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/crypto.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/assertions.mdx</include>
|
||||
@@ -172,6 +174,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/runtime-differences-from-node-js.mdx</include>
|
||||
|
||||
<include>../limitations/runtime-differences-from-node-js/event-order-across-native-code-and-islands.mdx</include>
|
||||
|
||||
### Array bounds and runtime errors
|
||||
|
||||
<include>../limitations/array-bounds-and-runtime-errors.mdx</include>
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
`assert.AssertionError` used as a value outside `instanceof` throws `SC2020`.
|
||||
The comparison methods, `ok`, `fail` and `ifError` are function values that assert as the methods do; called through a stored value, `ok` without a message reports `value == true`, where Node quotes the call, and `fail` takes at most one argument.
|
||||
`throws`, `rejects`, `doesNotThrow`, `doesNotReject`, `match` and `doesNotMatch` as values throw `SC2020`.
|
||||
`strictEqual` and `notStrictEqual` compare a number, string or boolean that can be `null` or `undefined`, such as `child.signalCode`, with any number, string, boolean, `null` or `undefined`.
|
||||
`notStrictEqual` of another value that can be `null` or `undefined`, such as `child.stdin`, against `null` or `undefined` compiles; `strictEqual` of one throws `SC2020`.
|
||||
`assert.deepEqual` and `assert.notDeepEqual` follow Node's loose comparison, including Maps, Sets, Dates, regular expressions, typed arrays, and errors; operands whose type can be cyclic or holds functions inside a structure are refused.
|
||||
`assert.partialDeepStrictEqual` follows Node's partial comparison over the same values, with the same refusals.
|
||||
Checked-dynamic values compared deeply have no cycle detection.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- **Date:** Spread constructor arguments and locale formatters are unsupported.
|
||||
- **Date:** Spread constructor arguments, `toLocaleDateString()`, and locale or options arguments to the locale formatters are unsupported.
|
||||
Dates are objects with identity: `===` and `==` compare identity, `new Date(date)` makes a new Date, setters change the one Date every reference holds, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in unions with other types, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators.
|
||||
Thrown and rejected Dates keep their identity; `structuredClone` and `postMessage` copy them.
|
||||
Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, the local and UTC calendar getters and setters, `setTime`, `getTimezoneOffset`, and single-string `Date.parse` compile.
|
||||
Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, the argument-free `toLocaleString()` and `toLocaleTimeString()` (the default locale's ICU patterns over the host time zone, as Node prints them), the local and UTC calendar getters and setters, `setTime`, `getTimezoneOffset`, and single-string `Date.parse` compile.
|
||||
Unary `+`, `-` and `Number(date)` read the time value; `String(date)`, template literals and `+` with a string read the `toString()` text; `date[Symbol.toPrimitive](hint)` compiles with a literal hint.
|
||||
A Date converted to `unknown` is the same Date object.
|
||||
See [Dates and timezones](#dates-and-timezones) for timezone data.
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
- **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.
|
||||
Unrestricted names, symbols, and names that may reach internal events are unsupported.
|
||||
An emitter reached through an untyped value shares one listener list with its typed uses, so untyped registrations and emits reach typed listeners and typed emits reach untyped listeners.
|
||||
An untyped `emit` of an event that has a listener with typed parameters converts its arguments to the event's typed tuple before any listener runs, and throws a `TypeError` when they do not convert.
|
||||
Through an untyped value, `listeners()` and `rawListeners()` of an event with a listener that has typed parameters are unsupported, and `emit('error')` of an object that is not an Error with no `error` listener is unsupported.
|
||||
Untyped `newListener` and `removeListener` listeners receive `undefined` in place of a listener registered with typed parameters.
|
||||
A `newListener` or `removeListener` listener registered through an untyped value receives `undefined` in place of a listener registered with typed parameters.
|
||||
A typed `error` or `errorMonitor` listener throws a `TypeError` when an emitted payload that is not an Error does not convert to its parameter.
|
||||
Reading EventEmitter methods as properties through an untyped value (`typeof emitter.listeners`) answers `undefined`.
|
||||
`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.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
- **File watching:** `fs.watch` returns an `FSWatcher` whose `on`, `once`, `off`, and `close` take `change`, `rename`, `error`, and `close` listeners.
|
||||
A listener may take the event name and the entry name, but the name arrives as a string (Node's default encoding), so a listener annotated with the `Buffer` arm of `@types/node`'s `change` overload has no lowering.
|
||||
Watching a directory on macOS with a listener that reads the entry name is refused with `SC2020` because FSEvents is not implemented yet; file watches and listeners that ignore the name work on every target.
|
||||
- **Stat polling:** `fs.watchFile` polls the resolved path every `interval` milliseconds (default 5007) and calls its listener with `(current, previous)` `Stats`; `fs.unwatchFile` removes the last listener registered with that callback, or every listener when called without one.
|
||||
The call must be a statement of its own, because the `StatWatcher` it returns in Node.js is not lowered, and `bigint: true` is refused.
|
||||
@@ -1,3 +1,4 @@
|
||||
- **Intl.Segmenter:** Unicode grapheme segmentation, iteration, and `containing()` compile, with the default locale or one language tag of the form language[-script][-region] written as a string literal (grapheme clusters do not depend on the locale).
|
||||
In JavaScript dependencies, word and sentence segmenters construct, and their `segment()` throws an SC2020 error.
|
||||
Locale negotiation over several locales, word and sentence segmentation, `resolvedOptions()`, and detached methods are unsupported.
|
||||
Word granularity compiles too, over ICU's UAX #29 word iterator at that locale; its segments carry `isWordLike` exactly as Node.js's do.
|
||||
In JavaScript dependencies, sentence segmenters construct, and their `segment()` throws an SC2020 error; in TypeScript sources the construction is refused.
|
||||
Locale negotiation over several locales, sentence segmentation, `resolvedOptions()`, and detached methods are unsupported.
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
- **Process memory:** `process.memoryUsage()` reads `rss` from the operating system and `heapTotal` and `heapUsed` from the runtime's allocator, so the heap figures start near zero and grow as the program allocates.
|
||||
`external` and `arrayBuffers` are `0`, because Buffer and typed-array storage is part of the same heap.
|
||||
`arrayBuffers` is the storage of the ArrayBuffers, typed arrays, Buffers, DataViews and SharedArrayBuffers the program holds, and `external` is the same storage without SharedArrayBuffers, as in Node.
|
||||
Node also counts its own startup allocations in both, so a program that holds no such storage reports `0` where Node reports a few megabytes.
|
||||
In a program with worker threads, `external` and the non-shared part of `arrayBuffers` count the calling thread's storage.
|
||||
The storage comes from the same allocator, so it also counts in `heapTotal` and `heapUsed`.
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
`stream.compose` accepts streams and async generator functions in any position, an async function as the last stage, and an array, a string, or a generator as the first stage; one stage is `Duplex.from` of it.
|
||||
A web `ReadableStream` is read as an async iterable by `Readable.from` and as the first stage of `compose` or `pipeline`, and through its reader by `Readable.fromWeb` (with `objectMode`, `highWaterMark`, `encoding`, and `signal` in an options object literal) and `Duplex.from`.
|
||||
In TypeScript, `new TransformStream()` without arguments builds the identity transform: its readable side is a web `ReadableStream`, its writable side supports `getWriter`, `abort`, `close`, and `locked`, and the writer supports `write`, `close`, `abort`, `releaseLock`, `ready`, `closed`, and `desiredSize`, with Node's backpressure (a write completes when the readable side reads its chunk).
|
||||
A web stream in a later compose stage and `Readable.toWeb` are unsupported, and in TypeScript, so are a `TransformStream` with a transformer or queuing strategies, the `WritableStream` constructor, `pipeTo`, and `pipeThrough`.
|
||||
In TypeScript, `tee`, `pipeTo`, and `pipeThrough`, the `WritableStream` constructor with an underlying sink, and a `TransformStream` with a transformer or queuing strategies compile: a program that uses one of them builds its `ReadableStream`, `WritableStream`, and `TransformStream` values with the same classes as JavaScript, and a `Response` or `Request` body has those members too.
|
||||
A TypeScript program that uses only `getReader`, `cancel`, `locked`, and the identity `TransformStream` keeps the smaller native streams.
|
||||
A web stream in a later compose stage and `Readable.toWeb` are unsupported.
|
||||
The `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream.
|
||||
`readable.compose(stage, { signal })` is `stream.compose(readable, stage)` with the signal added; the options are an object literal whose `signal` is a variable or a property read.
|
||||
`pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags.
|
||||
@@ -60,7 +62,7 @@
|
||||
The `readline/promises` `Readline` class writes to the same outputs.
|
||||
`readline.emitKeypressEvents` emits `'keypress'` events on `process.stdin` or a readable stream, shared with terminal interfaces on that stream as in Node; a listener's first parameter must admit `undefined`, which it receives for escape sequences.
|
||||
Chained listener registration is unsupported.
|
||||
`node:stream/web` exports the same classes as the globals; in TypeScript, only `ReadableStream` and the identity `TransformStream` have lowerings.
|
||||
`node:stream/web` exports the same classes as the globals; in TypeScript, `ReadableStream`, `WritableStream`, and `TransformStream` have lowerings.
|
||||
In JavaScript, `ReadableStream`, `WritableStream`, and `TransformStream` with their default readers, writers, and controllers, `CountQueuingStrategy`, `ByteLengthQueuingStrategy`, `TextEncoderStream`, and `TextDecoderStream`, as globals and as `node:stream/web` exports, are constructor values with Node's prototypes, brand checks, and errors.
|
||||
Their underlying sources, sinks, and transformers, queuing strategies with a `size` function, readers and writers, `tee`, `pipeTo` and `pipeThrough` with their options and an abort signal, `ReadableStream.from`, and async iteration settle promises in Node's order, and the streams take own properties.
|
||||
`TextDecoderStream` decodes UTF-8; its other encodings are unsupported.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
- **V8 heap statistics:** `node:v8` can be imported.
|
||||
`v8.getHeapStatistics()` returns Node's fields in Node's order, computed from the runtime allocator like `process.memoryUsage()`: `total_heap_size` and `total_physical_size` are the bytes the heap holds, `used_heap_size` the bytes allocated, `heap_size_limit` the machine's memory (the heap has no fixed ceiling), and `total_available_size` the limit minus the used bytes.
|
||||
Fields that describe V8 itself (`total_heap_size_executable`, `malloced_memory`, `peak_malloced_memory`, the global handle sizes, `external_memory`, and `does_zap_garbage`) are `0`, and `number_of_native_contexts` is `1`.
|
||||
`v8.getHeapStatistics()` returns Node's fields in Node's order, computed from the runtime allocator like `process.memoryUsage()`: `total_heap_size` and `total_physical_size` are the bytes the heap holds, `used_heap_size` the bytes allocated, `external_memory` the same figure as `process.memoryUsage().external`, `heap_size_limit` the machine's memory (the heap has no fixed ceiling), and `total_available_size` the limit minus the used bytes.
|
||||
Fields that describe V8 itself (`total_heap_size_executable`, `malloced_memory`, `peak_malloced_memory`, the global handle sizes, and `does_zap_garbage`) are `0`, and `number_of_native_contexts` is `1`.
|
||||
The values are approximate and are not V8 heap figures.
|
||||
The other `node:v8` members observe the V8 engine, such as heap snapshots, profiles, flags, and serialization, and throw `SC2020` where they are used.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
Some builtins can be stored and passed as functions:
|
||||
|
||||
- Lowered `node:path` functions across the bare, POSIX, and win32 modules, including optional `basename` and variadic `join`/`resolve`.
|
||||
Called through an untyped value, they validate their arguments with Node's `ERR_INVALID_ARG_TYPE`.
|
||||
A path module held by untyped code is a plain object with Node's members in Node's order.
|
||||
- Supported zero-argument `node:os` functions and `querystring.escape`/`unescape`.
|
||||
- `util.isDeepStrictEqual`, `styleText`, `stripVTControlCharacters`, `toUSVString`, `inherits`, `deprecate`, `callbackify`, `debuglog`, and `debug`.
|
||||
- `fs.existsSync`, `unlinkSync`, `chmodSync`, `chownSync`, `renameSync`, and `closeSync`, and `fs/promises.unlink`, `chmod`, and `rename`.
|
||||
|
||||
@@ -12,9 +12,11 @@ Stored global console values and detached output methods retain identity and acc
|
||||
Stored methods do not support format specifiers with substitution arguments.
|
||||
Other stored console APIs, custom `Console` instances, and console mutation are unsupported.
|
||||
|
||||
JavaScript can store `process.stdin`, `stdout`, and `stderr`, replace output `write` methods, and call saved methods with their stream receiver.
|
||||
JavaScript and TypeScript can store `process.stdin`, `stdout`, and `stderr`, replace output `write` methods, and call saved methods with their stream receiver.
|
||||
A replaced `write` method does not receive console's error-callback argument.
|
||||
Input supports data/end/error listeners, pause/resume, buffered reads, and TTY raw mode.
|
||||
Output writes are synchronous, with successful callbacks on the next-tick queue; queued backpressure is not modeled.
|
||||
Because no write is ever queued, `writableLength` is 0, `writableNeedDrain` is false, `writableHighWaterMark` is 65536, and `'drain'` never fires, so drain listeners register silently and `events.once(process.stdout, "drain")` stays pending, as it does in Node when no write returned false.
|
||||
Stored streams do not support piping, encoding changes, arbitrary properties, or other events.
|
||||
A TypeScript `NodeJS.WritableStream` parameter that only receives `process.stdout` or `process.stderr` supports `write(string)`.
|
||||
When other streams flow into the same parameter, the process streams join them as `Writable` instances with the full writable surface.
|
||||
|
||||
@@ -7,3 +7,5 @@
|
||||
Defaults, rest elements, and nested patterns over other modules are unsupported.
|
||||
A require cycle compiles when each of its modules assigns all of its exports before its first `require` and uses the other modules of the cycle only inside functions that its top-level code can't call.
|
||||
Other require cycles are rejected with `SC1016`.
|
||||
A top-level `return` ends the module's evaluation.
|
||||
It is unsupported when the module assigns exports after it, or when a function that exists before it runs reads a binding declared after it, such as a callback created above the `return` or a function declaration.
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
- **Event order across native code and islands:** Timers, worker output, and other macrotasks from the program and from JavaScript it runs in a Node island share one event loop under Node.js, so their order is fixed.
|
||||
A compiled program runs both on the event loop it hosts for Node.js, but it does not merge them into one order, so callbacks from the two sides can run in a different order than under Node.js, and the order can vary between runs.
|
||||
Only programs that depend on the ordering between native and island callbacks are affected.
|
||||
@@ -1,3 +1,3 @@
|
||||
After `socket.setEncoding("utf8")`, an inline `'data'` listener receives strings, as in Node, whatever type its chunk parameter declares.
|
||||
After `socket.setEncoding()`, an inline `'data'` listener receives strings, as in Node, whatever type its chunk parameter declares.
|
||||
A listener passed by reference, such as a named function whose parameter is typed `Buffer`, keeps its `Buffer` parameter, so a string chunk throws `TypeError: expected Uint8Array at $[0], got string`.
|
||||
Declare that parameter as `string` or `string | Buffer`, or pass an inline function.
|
||||
|
||||
@@ -30842,9 +30842,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -30911,9 +30911,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -32659,9 +32659,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented by a dedicated static compiler/runtime path.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -59,6 +59,8 @@
|
||||
"node-builtin.v8.getHeapStatistics": ["tests/corpus/v8-heap-statistics.ts", "tests/corpus/v8-require-optional.cjs"],
|
||||
"node-builtin.events": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"],
|
||||
"node-builtin.fs": ["tests/corpus/1403-buffer-fs.ts", "tests/corpus/2595-fs-arg-ladders.cjs", "tests/corpus/2919-fs-function-values.ts", "tests/corpus/2920-fs-promises-function-values.ts"],
|
||||
"node-builtin.fs.copyFileSync": ["tests/corpus/1403-buffer-fs.ts", "tests/corpus/2595-fs-arg-ladders.cjs", "tests/corpus/2919-fs-function-values.ts", "tests/corpus/2920-fs-promises-function-values.ts", "tests/corpus/8427-fs-copyfile-writefile-flags.ts"],
|
||||
"node-builtin.fs.writeFileSync": ["tests/corpus/1403-buffer-fs.ts", "tests/corpus/2595-fs-arg-ladders.cjs", "tests/corpus/2919-fs-function-values.ts", "tests/corpus/2920-fs-promises-function-values.ts", "tests/corpus/8427-fs-copyfile-writefile-flags.ts"],
|
||||
"node-builtin.fs.realpathSync.native": ["tests/corpus/fs-native-realpath.ts"],
|
||||
"node-builtin.fs.fchmodSync": ["tests/corpus/2950-fs-descriptor-publication.ts"],
|
||||
"node-builtin.fs.fstatSync": ["tests/corpus/2950-fs-descriptor-publication.ts"],
|
||||
@@ -71,7 +73,8 @@
|
||||
"node-builtin.fs.promises.utimes": ["tests/corpus/node24-fs-timestamps-promises.ts"],
|
||||
"node-builtin.fs.promises.access": ["tests/corpus/5542-fs-promises-parity.cjs", "tests/corpus/5543-fs-promises-typed.ts"],
|
||||
"node-builtin.fs.promises.appendFile": ["tests/corpus/5542-fs-promises-parity.cjs", "tests/corpus/5543-fs-promises-typed.ts"],
|
||||
"node-builtin.fs.promises.copyFile": ["tests/corpus/5542-fs-promises-parity.cjs", "tests/corpus/5543-fs-promises-typed.ts"],
|
||||
"node-builtin.fs.promises.copyFile": ["tests/corpus/5542-fs-promises-parity.cjs", "tests/corpus/5543-fs-promises-typed.ts", "tests/corpus/8427-fs-copyfile-writefile-flags.ts"],
|
||||
"node-builtin.fs.promises.writeFile": ["tests/corpus/1403-buffer-fs.ts", "tests/corpus/2595-fs-arg-ladders.cjs", "tests/corpus/2919-fs-function-values.ts", "tests/corpus/2920-fs-promises-function-values.ts", "tests/corpus/8427-fs-copyfile-writefile-flags.ts"],
|
||||
"node-builtin.fs.promises.mkdtemp": ["tests/corpus/5542-fs-promises-parity.cjs", "tests/corpus/5543-fs-promises-typed.ts"],
|
||||
"node-builtin.fs.promises.rmdir": ["tests/corpus/5542-fs-promises-parity.cjs", "tests/corpus/5543-fs-promises-typed.ts"],
|
||||
"node-builtin.fs.promises.truncate": ["tests/corpus/5542-fs-promises-parity.cjs", "tests/corpus/5543-fs-promises-typed.ts"],
|
||||
@@ -169,12 +172,12 @@
|
||||
"node-builtin.querystring": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/2917-builtin-callable-widths.ts"],
|
||||
"node-builtin.readline": ["tests/corpus/1475-readline-closed-stdin.ts"],
|
||||
"node-builtin.stream": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1746-stream-for-await.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/2629-stream-consumers.ts", "tests/corpus/2861-readable-iterator-close.ts"],
|
||||
"node-builtin.string_decoder": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts"],
|
||||
"node-builtin.string_decoder": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/8428-string-decoder-dynamic-chunks.ts"],
|
||||
"node-builtin.test": ["tests/harness/node-test.test.ts"],
|
||||
"node-builtin.timers": ["tests/corpus/1804-timers-module.ts", "tests/corpus/2093-timers-promises.ts"],
|
||||
"node-builtin.tls": ["tests/corpus/2331-tls-server-options-typed.ts", "tests/corpus/2557-tls-ca-store.ts"],
|
||||
"node-builtin.tty": ["tests/corpus/1448-stream-columns.ts", "tests/corpus/terminal-stream-properties.ts", "tests/harness/terminal-geometry.test.ts"],
|
||||
"node-builtin.url": ["tests/corpus/1794-searchparams-url-live.ts"],
|
||||
"node-builtin.url": ["tests/corpus/1794-searchparams-url-live.ts", "tests/corpus/8423-urlsearchparams-implicit-string.ts"],
|
||||
"node-builtin.url.pathToFileURL": ["tests/corpus/path-to-file-url-options.ts", "tests/corpus/node24-file-url-options.ts", "tests/corpus/node24-file-url-errors.js"],
|
||||
"node-builtin.url.fileURLToPath": ["tests/corpus/file-url-percent-decoding.ts", "tests/corpus/node24-file-url-options.ts", "tests/corpus/node24-file-url-errors.js"],
|
||||
"node-builtin.url.fileURLToPathBuffer": ["tests/corpus/node24-file-url-buffer.ts", "tests/corpus/node24-file-url-options.ts", "tests/corpus/node24-file-url-errors.js", "tests/fixtures/node-types/file-url-conversion.ts"],
|
||||
@@ -314,7 +317,7 @@
|
||||
{ "chapter": "process", "signatures": ["`process.hrtime([time])`", "`process.hrtime.bigint()`"], "status": "partial", "evidence": ["tests/corpus/process-hrtime.js"] },
|
||||
{ "chapter": "errors", "signatures": ["`stackTraceLimit` Type: {number}"], "status": "partial", "evidence": ["tests/corpus/error-stack-limit.ts"] },
|
||||
{ "chapter": "errors", "signatures": ["`stack` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/error-stack-frames.ts", "tests/corpus/error-stack-limit.ts"] },
|
||||
{ "chapter": "process", "signatures": ["`stdin` Type: {Stream}", "`stdout` Type: {Stream}", "`stderr` Type: {Stream}"], "status": "partial", "evidence": ["tests/corpus/stdio-values.mjs", "tests/corpus/stdio-write-overrides.mjs", "tests/corpus/stdio-write-errors.mjs", "tests/harness/event-loop.test.ts"] },
|
||||
{ "chapter": "process", "signatures": ["`stdin` Type: {Stream}", "`stdout` Type: {Stream}", "`stderr` Type: {Stream}"], "status": "partial", "evidence": ["tests/corpus/stdio-values.mjs", "tests/corpus/stdio-write-overrides.mjs", "tests/corpus/stdio-write-errors.mjs", "tests/corpus/stdout-write-patch.ts", "tests/corpus/process-stream-drain.ts", "tests/corpus/process-write-dynamic-chunks.ts", "tests/harness/event-loop.test.ts"] },
|
||||
{ "chapter": "process", "signatures": ["Signal events"], "status": "partial", "evidence": ["tests/corpus/1443-signal-handlers.ts", "tests/corpus/process-named-signals.cjs"] },
|
||||
{ "chapter": "process", "signatures": ["Event: `'uncaughtException'`", "Event: `'uncaughtExceptionMonitor'`"], "status": "partial", "evidence": ["tests/corpus/process-uncaught-sync.cjs", "tests/corpus/process-uncaught-loop.cjs", "tests/corpus/process-uncaught-module.mjs", "tests/corpus/process-uncaught-rejection.cjs", "tests/corpus/process-uncaught-monitor.cjs", "tests/corpus/process-uncaught-handler-throws.cjs"] },
|
||||
{ "chapter": "process", "signatures": ["`process.getBuiltinModule(id)`"], "status": "partial", "evidence": ["tests/corpus/process-builtin-values.js", "packages/compiler/test/process-builtin-values.test.ts"] },
|
||||
@@ -475,16 +478,17 @@
|
||||
{ "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.once"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] },
|
||||
{ "symbols": ["events.getEventListeners", "events.getMaxListeners", "events.listenerCount", "events.setMaxListeners", "events.defaultMaxListeners"], "status": "partial", "evidence": ["tests/corpus/5885-events-max-listener-helpers.ts", "tests/corpus/5846-events-esm-default-max.ts", "tests/corpus/5847-events-cjs-default-max.cjs"] },
|
||||
{ "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncLocalStorage.name", "AsyncLocalStorage.bind", "AsyncLocalStorage.snapshot"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs", "tests/corpus/5815-als-callback-context.ts", "tests/corpus/7110-als-completion-callbacks.ts", "tests/corpus/7111-als-bind-snapshot-options.ts", "tests/corpus/7112-als-bind-snapshot-dynamic.mjs", "tests/corpus/7119-als-net-event-context.mjs", "tests/corpus/7380-als-http-event-context.mjs", "tests/corpus/7383-als-stream-events.ts", "tests/corpus/7384-als-stream-events.mjs", "tests/corpus/7385-als-child-process-events.ts", "tests/corpus/7386-als-child-fork-events/main.mjs", "tests/corpus/7387-als-http2-events.mjs", "tests/corpus/7388-als-http2-compat-events.mjs", "tests/corpus/7389-als-http2-secure-events.mjs", "tests/corpus/7391-als-response-end-and-finished.mjs"] },
|
||||
{ "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts"] },
|
||||
{ "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts", "tests/corpus/node24-string-decoder.ts", "tests/corpus/5882-string-decoder-runtime-encoding.ts", "tests/corpus/8428-string-decoder-dynamic-chunks.ts"] },
|
||||
{ "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] },
|
||||
{ "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] },
|
||||
{ "symbols": ["stream.finished", "stream.pipeline", "stream.getDefaultHighWaterMark", "readable.push", "readable.unshift", "readable.read", "readable.pause", "readable.resume", "readable.isPaused", "readable.setEncoding", "readable.pipe", "readable.unpipe", "readable.destroy", "writable.write", "writable.end", "writable.cork", "writable.uncork", "writable.setDefaultEncoding"], "status": "partial", "evidence": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/5302-stream-promises-options.ts", "tests/corpus/5303-stream-finished-callback-opts.cjs", "tests/corpus/5425-pipeline-generator-stage.ts", "tests/corpus/5428-pipeline-generator-stages.cjs", "tests/corpus/5617-pipeline-function-ends.ts", "tests/corpus/5619-pipeline-function-ends.cjs"] },
|
||||
{ "symbols": ["net.createServer", "net.connect", "net.createConnection", "net.getDefaultAutoSelectFamilyAttemptTimeout", "net.setDefaultAutoSelectFamilyAttemptTimeout", "server.address", "server.close", "server.listen", "socket.destroy", "socket.end", "socket.pause", "socket.resume", "socket.setEncoding", "socket.setTimeout", "socket.write"], "status": "partial", "evidence": ["tests/corpus/2500-net-autosel-timeout.ts", "tests/corpus/2596-net-arg-ladders.cjs"] },
|
||||
{ "symbols": ["stream.finished", "stream.pipeline", "stream.getDefaultHighWaterMark", "readable.push", "readable.unshift", "readable.read", "readable.pause", "readable.resume", "readable.isPaused", "readable.setEncoding", "readable.pipe", "readable.unpipe", "readable.destroy", "writable.write", "writable.end", "writable.cork", "writable.uncork", "writable.setDefaultEncoding"], "status": "partial", "evidence": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/5302-stream-promises-options.ts", "tests/corpus/5303-stream-finished-callback-opts.cjs", "tests/corpus/5425-pipeline-generator-stage.ts", "tests/corpus/5428-pipeline-generator-stages.cjs", "tests/corpus/5617-pipeline-function-ends.ts", "tests/corpus/5619-pipeline-function-ends.cjs", "tests/corpus/writable-error-callbacks.ts"] },
|
||||
{ "symbols": ["net.createServer", "net.connect", "net.createConnection", "net.getDefaultAutoSelectFamilyAttemptTimeout", "net.setDefaultAutoSelectFamilyAttemptTimeout", "server.address", "server.close", "server.listen", "socket.destroy", "socket.end", "socket.pause", "socket.resume", "socket.setEncoding", "socket.setTimeout", "socket.write"], "status": "partial", "evidence": ["tests/corpus/2500-net-autosel-timeout.ts", "tests/corpus/2596-net-arg-ladders.cjs", "tests/corpus/net-socket-set-encoding-names.ts", "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", "tests/corpus/net-server-socket-peer-close.cjs", "tests/corpus/net-socket-encoding-pipe.cjs"] },
|
||||
{ "symbols": ["dgram.createSocket", "socket.address", "socket.bind", "socket.close", "socket.connect", "socket.ref", "socket.remoteAddress", "socket.send", "socket.unref"], "status": "partial", "evidence": ["tests/corpus/2597-dgram-send-ladders.cjs"] },
|
||||
{ "symbols": ["net.SocketAddress", "net.BlockList"], "status": "partial", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] },
|
||||
{ "symbols": ["SocketAddress.parse", "BlockList.isBlockList", "blockList.addAddress", "blockList.addAddresses", "blockList.addCIDR", "blockList.addCIDRs", "blockList.addRange", "blockList.addSubnet", "blockList.check", "blockList.clear", "blockList.fromJSON", "blockList.removeAddress", "blockList.removeCIDR", "blockList.removeRange", "blockList.removeSubnet", "blockList.toJSON"], "status": "supported", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] },
|
||||
{ "symbols": ["readline.createInterface", "rl.close"], "status": "partial", "evidence": ["tests/corpus/1475-readline-closed-stdin.ts", "tests/corpus/5878-readline-dynamic-streams.mjs"] },
|
||||
{ "symbols": ["test", "it", "test.skip", "test.todo", "test.only", "describe", "suite", "before", "after", "beforeEach", "afterEach", "context.test", "context.skip", "context.todo", "context.assert"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] },
|
||||
{ "symbols": ["http.request", "http.get", "http.createServer", "https.request", "https.get", "https.createServer", "request.end", "request.destroy", "request.setTimeout", "request.write", "response.end", "response.getHeader", "response.hasHeader", "response.removeHeader", "response.setHeader", "response.write", "response.writeHead", "headersTimeout", "requestTimeout", "timeout", "keepAliveTimeout", "keepAliveTimeoutBuffer"], "status": "partial", "evidence": ["tests/corpus/1780-http-res-surface.ts", "tests/corpus/2672-http-request-response-callback.ts", "tests/corpus/2689-http-server-timeout-properties.ts"] }
|
||||
{ "symbols": ["http.request", "http.get", "http.createServer", "https.request", "https.get", "https.createServer", "request.end", "request.destroy", "request.setTimeout", "request.write", "response.end", "response.getHeader", "response.hasHeader", "response.removeHeader", "response.setHeader", "response.write", "response.writeHead", "headersTimeout", "requestTimeout", "timeout", "keepAliveTimeout", "keepAliveTimeoutBuffer"], "status": "partial", "evidence": ["tests/corpus/1780-http-res-surface.ts", "tests/corpus/2672-http-request-response-callback.ts", "tests/corpus/2689-http-server-timeout-properties.ts"] },
|
||||
{ "symbols": ["fs.FSWatcher", "fs.watchFile", "fs.unwatchFile"], "status": "partial", "evidence": ["tests/corpus/1564-fs-watch.ts", "tests/corpus/fs-watch-file-names.ts", "tests/corpus/fs-watch-file-polling.ts"] }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ endif()
|
||||
|
||||
add_executable(scriptc-llvm-codegen
|
||||
src/cache.cpp
|
||||
src/cpu-copies.cpp
|
||||
src/diagnostics.cpp
|
||||
src/emit.cpp
|
||||
src/main.cpp
|
||||
|
||||
@@ -37,6 +37,8 @@ Repeating `--output` divides the program into that many partitions, which are co
|
||||
|
||||
With `--split codegen`, several outputs divide only code generation. The helper optimizes the module whole, as it does for a single output, then divides its functions in module order into runs of about the same instruction count, one per output, and generates code for every output on its own thread. Linking the outputs in order lays the functions out as one output would. A local definition that another output uses becomes hidden so the objects can reference each other; the others stay local. Every output after the first is generated from a copy of the optimized module read back from bitcode with its use-list order, so each function's machine code matches a single output except for jumps to definitions that another output also uses. When one run would hold more than three quarters of the module's instructions, the first output holds the whole module and the other outputs are empty objects. Placement depends only on the optimized module and the output count. `--split codegen` requires an optimized build and no `--cache-dir`; scriptc passes it for optimized executables with more than 128 KiB of LLVM text that stay one partition.
|
||||
|
||||
In a `--cpu-dispatch` module, each function has a baseline copy and an x86-64-v3 copy marked `scriptc-cpu-copy`. Partitioning places each x86-64-v3 copy with its baseline copy. After optimization, an x86-64-v3 copy whose code has vectors of at most 128 bits, none wider, and no rounding or bit-count intrinsic in a loop takes its baseline copy's optimized code, and its calls to baseline copies call their x86-64-v3 copies (`src/cpu-copies.h`).
|
||||
|
||||
WebAssembly code generation differs from LLVM's default pipeline in two ways that do not change its output. The pass that gives multiple-entry loops a single entry runs only on functions whose control flow is irreducible; its reachability analysis grows with the square of a function's block count even when it has nothing to change. Object emission also leaves out the module pass that collects external symbols for assembly declarations, so each function's machine code is freed once it is printed instead of when the whole module is compiled.
|
||||
|
||||
`--cache-dir <dir>` names a private directory the caller owns for one helper build. Simplified partitions are stored under the hash of their exact source and objects under the hash of the exact module after importing, each with a trailing digest and published by rename, so a rebuild recompiles only the partitions an edit can affect. Partitions drop the names of their struct types before the thin link, which would otherwise number same-named types from different partitions in an order that any edit can shift, changing the module after importing in partitions the edit did not touch.
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
#include "cpu-copies.h"
|
||||
|
||||
#include "llvm/ADT/STLExtras.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include "llvm/ADT/StringExtras.h"
|
||||
#include "llvm/ADT/StringMap.h"
|
||||
#include "llvm/Analysis/LoopInfo.h"
|
||||
#include "llvm/IR/DerivedTypes.h"
|
||||
#include "llvm/IR/Dominators.h"
|
||||
#include "llvm/IR/Function.h"
|
||||
#include "llvm/IR/GlobalVariable.h"
|
||||
#include "llvm/IR/InstrTypes.h"
|
||||
#include "llvm/IR/Instructions.h"
|
||||
#include "llvm/IR/IntrinsicInst.h"
|
||||
#include "llvm/IR/Intrinsics.h"
|
||||
#include "llvm/Transforms/Utils/Cloning.h"
|
||||
#include "llvm/Transforms/Utils/ValueMapper.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace llvm;
|
||||
|
||||
namespace scriptc {
|
||||
namespace {
|
||||
|
||||
// A function's name without the `.llvm.<n>` suffix of the thin link.
|
||||
StringRef stem(StringRef Name) {
|
||||
size_t At = Name.rfind(".llvm.");
|
||||
if (At == StringRef::npos)
|
||||
return Name;
|
||||
StringRef Suffix = Name.drop_front(At + 6);
|
||||
if (Suffix.empty() || !all_of(Suffix, isDigit))
|
||||
return Name;
|
||||
return Name.take_front(At);
|
||||
}
|
||||
|
||||
// The size of a vector type in bits, or 0 for another type.
|
||||
uint64_t vectorBits(Type *T) {
|
||||
auto *Vector = dyn_cast<FixedVectorType>(T);
|
||||
return Vector ? Vector->getPrimitiveSizeInBits().getFixedValue() : 0;
|
||||
}
|
||||
|
||||
// Whether x86-64-v3 changes only the body's vector code of at most 128 bits:
|
||||
// the body has such vectors, and nothing that the level compiles to faster
|
||||
// code than the baseline, which is vectors of 256 bits (AVX2), and in a
|
||||
// loop, rounding (SSE4.1; the baseline calls the C library) and bit counts
|
||||
// (POPCNT, LZCNT, TZCNT). Scalar code keeps its x86-64-v3 code, whose VEX
|
||||
// encodings need fewer register copies.
|
||||
bool onlyNarrowVectors(Function &F) {
|
||||
DominatorTree Dominators(F);
|
||||
LoopInfo Loops(Dominators);
|
||||
bool Narrow = false;
|
||||
for (const BasicBlock &Block : F)
|
||||
for (const Instruction &I : Block) {
|
||||
uint64_t Bits = vectorBits(I.getType());
|
||||
for (const Use &Operand : I.operands())
|
||||
Bits = std::max(Bits, vectorBits(Operand->getType()));
|
||||
if (Bits > 128)
|
||||
return false;
|
||||
Narrow |= Bits != 0;
|
||||
const auto *Call = dyn_cast<IntrinsicInst>(&I);
|
||||
if (!Call || !Loops.getLoopFor(&Block))
|
||||
continue;
|
||||
switch (Call->getIntrinsicID()) {
|
||||
case Intrinsic::floor:
|
||||
case Intrinsic::ceil:
|
||||
case Intrinsic::trunc:
|
||||
case Intrinsic::rint:
|
||||
case Intrinsic::nearbyint:
|
||||
case Intrinsic::round:
|
||||
case Intrinsic::roundeven:
|
||||
case Intrinsic::ctpop:
|
||||
case Intrinsic::ctlz:
|
||||
case Intrinsic::cttz:
|
||||
return false;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Narrow;
|
||||
}
|
||||
|
||||
// Replaces the body of Copy with a clone of Base's. The clone's direct calls
|
||||
// to baseline copies call the matching x86-64-v3 copies in Tier.
|
||||
void useBaselineBody(Function &Copy, Function &Base,
|
||||
const StringMap<Function *> &Tier) {
|
||||
GlobalValue::LinkageTypes Linkage = Copy.getLinkage();
|
||||
GlobalValue::VisibilityTypes Visibility = Copy.getVisibility();
|
||||
GlobalValue::UnnamedAddr UnnamedAddr = Copy.getUnnamedAddr();
|
||||
Copy.deleteBody();
|
||||
ValueToValueMapTy Map;
|
||||
for (auto [From, To] : zip(Base.args(), Copy.args()))
|
||||
Map[&From] = &To;
|
||||
SmallVector<ReturnInst *, 4> Returns;
|
||||
CloneFunctionInto(&Copy, &Base, Map,
|
||||
CloneFunctionChangeType::LocalChangesOnly, Returns);
|
||||
Copy.setLinkage(Linkage);
|
||||
Copy.setVisibility(Visibility);
|
||||
Copy.setUnnamedAddr(UnnamedAddr);
|
||||
for (BasicBlock &Block : Copy)
|
||||
for (Instruction &I : Block) {
|
||||
auto *Call = dyn_cast<CallBase>(&I);
|
||||
Function *Callee = Call ? Call->getCalledFunction() : nullptr;
|
||||
if (!Callee)
|
||||
continue;
|
||||
StringRef Name = stem(Callee->getName());
|
||||
if (!Name.consume_back(".base"))
|
||||
continue;
|
||||
auto It = Tier.find((Name + ".v3").str());
|
||||
if (It != Tier.end() &&
|
||||
It->second->getFunctionType() == Callee->getFunctionType())
|
||||
Call->setCalledFunction(It->second);
|
||||
}
|
||||
}
|
||||
|
||||
// Whether the module counts executions for a profile (`--profile-generate`,
|
||||
// also after inlining in a context-sensitive second phase): its functions
|
||||
// increment counters named after themselves (`__profc_<name>`).
|
||||
bool countsExecutions(const Module &M) {
|
||||
for (const GlobalVariable &Global : M.globals())
|
||||
if (Global.getName().starts_with("__profc_"))
|
||||
return true;
|
||||
for (const Function &F : M)
|
||||
if (F.isIntrinsic() && F.getName().starts_with("llvm.instrprof."))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether Base's body was optimized for fewer runs than Copy's: a profile
|
||||
// collected where the executable ran its x86-64-v3 copies gives their
|
||||
// baseline copies no runs, and the optimizer treats those as cold code.
|
||||
bool ranLess(const Function &Base, const Function &Copy) {
|
||||
std::optional<Function::ProfileCount> Runs = Copy.getEntryCount();
|
||||
if (!Runs)
|
||||
return false;
|
||||
std::optional<Function::ProfileCount> BaseRuns = Base.getEntryCount();
|
||||
return !BaseRuns || BaseRuns->getCount() < Runs->getCount();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void useBaselineBodies(Module &M) {
|
||||
// Every function by its stem, so the copies of one function find each
|
||||
// other whichever of them the thin link renamed.
|
||||
StringMap<Function *> Functions;
|
||||
bool HasCopies = false;
|
||||
for (Function &F : M) {
|
||||
Functions[stem(F.getName())] = &F;
|
||||
HasCopies |= F.hasFnAttribute(CpuCopyAttribute);
|
||||
}
|
||||
// A copy that took its baseline copy's body would count its executions
|
||||
// against the baseline copy, and a build with the profile would then
|
||||
// optimize the x86-64-v3 copy as if it never ran.
|
||||
if (!HasCopies || countsExecutions(M))
|
||||
return;
|
||||
std::vector<std::pair<Function *, Function *>> Replaced;
|
||||
for (Function &F : M) {
|
||||
if (F.isDeclaration() || !F.hasFnAttribute(CpuCopyAttribute))
|
||||
continue;
|
||||
StringRef Name = stem(F.getName());
|
||||
if (!Name.consume_back(".v3"))
|
||||
continue;
|
||||
auto It = Functions.find((Name + ".base").str());
|
||||
if (It == Functions.end())
|
||||
continue;
|
||||
Function *Base = It->second;
|
||||
if (Base->isDeclaration() || Base->hasAvailableExternallyLinkage() ||
|
||||
Base->getFunctionType() != F.getFunctionType() ||
|
||||
ranLess(*Base, F) || !onlyNarrowVectors(F))
|
||||
continue;
|
||||
Replaced.emplace_back(&F, Base);
|
||||
}
|
||||
for (auto [Copy, Base] : Replaced)
|
||||
useBaselineBody(*Copy, *Base, Functions);
|
||||
}
|
||||
|
||||
} // namespace scriptc
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "llvm/IR/Module.h"
|
||||
|
||||
namespace scriptc {
|
||||
|
||||
// The string attribute that marks the x86-64-v3 copies of a `--cpu-dispatch`
|
||||
// build. The compiler names a function's copies `<name>.base` and
|
||||
// `<name>.v3`; the thin link may rename either to `<copy>.llvm.<n>`.
|
||||
inline constexpr const char *CpuCopyAttribute = "scriptc-cpu-copy";
|
||||
|
||||
// Runs after optimization, before code generation. An x86-64-v3 copy whose
|
||||
// optimized body has vectors of at most 128 bits takes the optimized body of
|
||||
// its baseline copy instead, unless it has something else the baseline
|
||||
// compiles to slower code: a vector wider than 128 bits, or a rounding or
|
||||
// bit-count intrinsic in a loop. In 128-bit vector code the higher level
|
||||
// only adds instructions that the vectorizers prefer over scalar ones, and
|
||||
// these can run slower. Scalar code keeps its x86-64-v3 code. The copy keeps
|
||||
// its name, so its callers do not change, and the body's direct calls to
|
||||
// baseline copies call their x86-64-v3 copies, so that callees still run at
|
||||
// the level the executable chose.
|
||||
//
|
||||
// A copy is left as it is when its baseline copy is not defined in the
|
||||
// module, the two copies' types differ, or the profile gives the baseline
|
||||
// copy fewer runs (its body is then optimized as colder code). Every copy is
|
||||
// left as it is in a module that counts executions for a profile.
|
||||
void useBaselineBodies(llvm::Module &M);
|
||||
|
||||
} // namespace scriptc
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "emit.h"
|
||||
|
||||
#include "cache.h"
|
||||
#include "cpu-copies.h"
|
||||
#include "diagnostics.h"
|
||||
#include "partition.h"
|
||||
#include "profile-blocks.h"
|
||||
@@ -252,15 +253,20 @@ struct UnnameStructTypes : PassInfoMixin<UnnameStructTypes> {
|
||||
// over partitions: functions that received its counts, functions whose
|
||||
// control flow changed since it was collected (hash mismatch), and functions
|
||||
// it has no record of (new since it was collected, or never run in training
|
||||
// when the profile was merged with --sparse). A partition reused from the cache without its counts is counted
|
||||
// in Unknown. Recovered counts the mismatched functions that --profile-blocks
|
||||
// annotated, so it never exceeds Mismatched.
|
||||
// when the profile was merged with --sparse). A partition reused from the cache
|
||||
// without its counts is counted in Unknown. Recovered counts the mismatched
|
||||
// functions that --profile-blocks annotated, so it never exceeds Mismatched.
|
||||
// The two weights sum the profile's counters for the annotated and for the
|
||||
// mismatched functions, so a changed hot function weighs more than a changed
|
||||
// cold one.
|
||||
struct ProfileMatch {
|
||||
std::atomic<uint64_t> Annotated{0};
|
||||
std::atomic<uint64_t> Mismatched{0};
|
||||
std::atomic<uint64_t> Missing{0};
|
||||
std::atomic<uint64_t> Recovered{0};
|
||||
std::atomic<uint64_t> Unknown{0};
|
||||
std::atomic<uint64_t> AnnotatedCounts{0};
|
||||
std::atomic<uint64_t> MismatchedCounts{0};
|
||||
// Set once the annotation before inlining has run. The context-sensitive
|
||||
// annotation after inlining, which a single-module pipeline also runs,
|
||||
// reports its mismatched and missing functions again; they are not
|
||||
@@ -272,27 +278,32 @@ struct ProfileMatch {
|
||||
Mismatched += Other.Mismatched;
|
||||
Missing += Other.Missing;
|
||||
Recovered += Other.Recovered;
|
||||
AnnotatedCounts += Other.AnnotatedCounts;
|
||||
MismatchedCounts += Other.MismatchedCounts;
|
||||
}
|
||||
|
||||
std::string serialize() const {
|
||||
return formatv("{0} {1} {2} {3}", Annotated.load(), Mismatched.load(),
|
||||
Missing.load(), Recovered.load())
|
||||
return formatv("{0} {1} {2} {3} {4} {5}", Annotated.load(),
|
||||
Mismatched.load(), Missing.load(), Recovered.load(),
|
||||
AnnotatedCounts.load(), MismatchedCounts.load())
|
||||
.str();
|
||||
}
|
||||
|
||||
bool deserialize(StringRef Text) {
|
||||
SmallVector<StringRef, 4> Parts;
|
||||
SmallVector<StringRef, 6> Parts;
|
||||
Text.split(Parts, ' ');
|
||||
uint64_t Values[4];
|
||||
if (Parts.size() != 4)
|
||||
uint64_t Values[6];
|
||||
if (Parts.size() != 6)
|
||||
return false;
|
||||
for (size_t I = 0; I < 4; ++I)
|
||||
for (size_t I = 0; I < 6; ++I)
|
||||
if (Parts[I].getAsInteger(10, Values[I]))
|
||||
return false;
|
||||
Annotated += Values[0];
|
||||
Mismatched += Values[1];
|
||||
Missing += Values[2];
|
||||
Recovered += Values[3];
|
||||
AnnotatedCounts += Values[4];
|
||||
MismatchedCounts += Values[5];
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -374,23 +385,34 @@ bool hasProfileCounts(const Function &F) {
|
||||
|
||||
// Runs right after the profile annotation, before inlining and dead code
|
||||
// elimination change the functions it annotated: counts them and records
|
||||
// their block counts, then annotates the functions whose control flow
|
||||
// changed from FallbackCounts. A function the profile has no record of keeps
|
||||
// no counts, as without block counts. Returns the analyses that remain valid.
|
||||
// their block counts, then annotates from FallbackCounts the functions whose
|
||||
// control flow changed, and the functions the profile has no record of that
|
||||
// FallbackCounts records as never run. A sparse profile leaves out the
|
||||
// functions its training never ran; the block counts name them, so they get
|
||||
// an entry count of 0 and LLVM optimizes them as cold code, as a profile with
|
||||
// zero counts would make it. A function neither names keeps no counts, as
|
||||
// without block counts: it was added after the profile was collected.
|
||||
// Returns the analyses that remain valid.
|
||||
PreservedAnalyses afterProfileAnnotation(Module &M, ProfileMatch &Match) {
|
||||
std::vector<Function *> Mismatched;
|
||||
std::vector<Function *> Unrecorded;
|
||||
for (Function &F : M) {
|
||||
if (F.isDeclaration())
|
||||
continue;
|
||||
if (!hasProfileCounts(F)) {
|
||||
if (hasHashMismatchTag(F))
|
||||
Mismatched.push_back(&F);
|
||||
else
|
||||
Unrecorded.push_back(&F);
|
||||
continue;
|
||||
}
|
||||
++Match.Annotated;
|
||||
if (Recorder)
|
||||
Recorder->add(F);
|
||||
}
|
||||
if (Recorder)
|
||||
for (Function *F : Unrecorded)
|
||||
Recorder->addNeverRan(*F);
|
||||
if (!FallbackCounts)
|
||||
return PreservedAnalyses::all();
|
||||
ProfileSummaryInfo Summary(M);
|
||||
@@ -403,6 +425,13 @@ PreservedAnalyses afterProfileAnnotation(Module &M, ProfileMatch &Match) {
|
||||
++Match.Recovered;
|
||||
Preserved = PreservedAnalyses::none();
|
||||
}
|
||||
for (Function *F : Unrecorded) {
|
||||
auto Record = FallbackCounts->find(F->getName());
|
||||
if (Record == FallbackCounts->end() || Record->second.EntryCount != 0)
|
||||
continue;
|
||||
annotateFromBlocks(*F, Record->second, Summary);
|
||||
Preserved = PreservedAnalyses::none();
|
||||
}
|
||||
return Preserved;
|
||||
}
|
||||
|
||||
@@ -412,11 +441,77 @@ void reportProfileMatch(const ProfileMatch &Match) {
|
||||
{"mismatched", Match.Mismatched.load()},
|
||||
{"missing", Match.Missing.load()},
|
||||
{"recovered", Match.Recovered.load()},
|
||||
{"unknown_partitions", Match.Unknown.load()}};
|
||||
{"unknown_partitions", Match.Unknown.load()},
|
||||
{"annotated_counts", Match.AnnotatedCounts.load()},
|
||||
{"mismatched_counts", Match.MismatchedCounts.load()}};
|
||||
json::Object Response{{"ok", true}, {"profile", std::move(Profile)}};
|
||||
outs() << formatv("{0}\n", json::Value(std::move(Response)));
|
||||
}
|
||||
|
||||
// The sum of each function's counters in the indexed profile at `Path`, by
|
||||
// profile name, read once per process. An unreadable profile has no counts
|
||||
// (the annotation reports it).
|
||||
const StringMap<uint64_t> &profileCounts(const std::string &Path) {
|
||||
static std::once_flag Once;
|
||||
static StringMap<uint64_t> Counts;
|
||||
std::call_once(Once, [&Path] {
|
||||
Expected<std::unique_ptr<IndexedInstrProfReader>> Reader =
|
||||
IndexedInstrProfReader::create(Path, *vfs::getRealFileSystem());
|
||||
if (!Reader) {
|
||||
consumeError(Reader.takeError());
|
||||
return;
|
||||
}
|
||||
for (const NamedInstrProfRecord &Record : **Reader) {
|
||||
uint64_t Sum = 0;
|
||||
for (uint64_t Count : Record.Counts)
|
||||
Sum += Count;
|
||||
Counts[Record.Name] += Sum;
|
||||
}
|
||||
consumeError((*Reader)->getError());
|
||||
});
|
||||
return Counts;
|
||||
}
|
||||
|
||||
// Weighs one module's profile match by the profile's counters: collects the
|
||||
// annotated and the mismatched functions while the module is optimized,
|
||||
// then adds their counts to the match.
|
||||
class MatchWeigher {
|
||||
public:
|
||||
explicit MatchWeigher(const Module &M) {
|
||||
for (const Function &F : M)
|
||||
if (!F.isDeclaration())
|
||||
ProfileNames[F.getName()] = getIRPGOFuncName(F);
|
||||
}
|
||||
|
||||
void weigh(const std::string &ProfileUse, ProfileMatch &Match) const {
|
||||
const StringMap<uint64_t> &Counts = profileCounts(ProfileUse);
|
||||
auto CountsOf = [&Counts](StringRef Name) -> uint64_t {
|
||||
auto It = Counts.find(Name);
|
||||
return It == Counts.end() ? 0 : It->second;
|
||||
};
|
||||
// A context-sensitive profile annotates the module twice; each function
|
||||
// counts once.
|
||||
StringSet<> Seen;
|
||||
for (const std::string &Name : Annotated)
|
||||
if (Seen.insert(Name).second)
|
||||
Match.AnnotatedCounts += CountsOf(Name);
|
||||
// The mismatch warnings name functions by symbol.
|
||||
for (const std::string &Name : Mismatched) {
|
||||
auto It = ProfileNames.find(Name);
|
||||
StringRef ProfileName =
|
||||
It == ProfileNames.end() ? StringRef(Name) : StringRef(It->second);
|
||||
if (Seen.insert(ProfileName).second)
|
||||
Match.MismatchedCounts += CountsOf(ProfileName);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> Annotated;
|
||||
std::vector<std::string> Mismatched;
|
||||
|
||||
private:
|
||||
StringMap<std::string> ProfileNames;
|
||||
};
|
||||
|
||||
// Reads the profile's header once, before any partition needs it: it must
|
||||
// hold IR-level counts. Context-sensitive counts lay out the code by them;
|
||||
// a profile that has them cannot take a second set.
|
||||
@@ -515,13 +610,15 @@ struct Pipeline {
|
||||
});
|
||||
}
|
||||
|
||||
// Adds to Names the profile names of the functions the annotation gave
|
||||
// counts to, as soon as that pass has run.
|
||||
// Adds to Names the profile names of the functions the annotation before
|
||||
// inlining gave counts to, as soon as that pass has run.
|
||||
void collectAnnotated(std::vector<std::string> &Names) {
|
||||
Callbacks.registerAfterPassCallback(
|
||||
[&Names](StringRef PassID, Any IR, const PreservedAnalyses &) {
|
||||
if (PassID != "PGOInstrumentationUse")
|
||||
[&Names, Done = false](StringRef PassID, Any IR,
|
||||
const PreservedAnalyses &) mutable {
|
||||
if (Done || PassID != "PGOInstrumentationUse")
|
||||
return;
|
||||
Done = true;
|
||||
if (const auto *M = llvm::any_cast<const Module *>(&IR))
|
||||
for (const Function &F : **M) {
|
||||
std::optional<Function::ProfileCount> Entry = F.getEntryCount();
|
||||
@@ -714,13 +811,22 @@ emitModule(Module &M, TargetMachine &Machine, OptimizationLevel Level,
|
||||
StringRef OutputPath, std::vector<SmallString<256>> &Temporaries,
|
||||
ProfileMatch &Match) {
|
||||
bool Annotating = Profile && Profile->Action == PGOOptions::IRUse;
|
||||
std::string ProfileUse = Annotating ? Profile->ProfileFile : "";
|
||||
MatchWeigher Weigher(M);
|
||||
if (Annotating)
|
||||
M.getContext().setDiagnosticHandler(
|
||||
std::make_unique<ProfileMatchHandler>(Match));
|
||||
std::make_unique<ProfileMatchHandler>(Match, &Weigher.Mismatched));
|
||||
Pipeline P(Machine, Level, std::move(Profile));
|
||||
if (Annotating)
|
||||
if (Annotating) {
|
||||
// Registered first, so it lists the functions the profile itself
|
||||
// annotated, before block counts recover any.
|
||||
P.collectAnnotated(Weigher.Annotated);
|
||||
P.afterProfileAnnotation(Match);
|
||||
}
|
||||
P.PB.buildPerModuleDefaultPipeline(Level).run(M, P.MAM);
|
||||
if (Annotating)
|
||||
Weigher.weigh(ProfileUse, Match);
|
||||
useBaselineBodies(M);
|
||||
if (std::optional<EmitFailure> Failure =
|
||||
verify(M, "post_optimization_verification_failed"))
|
||||
return Failure;
|
||||
@@ -911,14 +1017,16 @@ compileOptimizedPartitions(const EmitOptions &Options,
|
||||
return Failures.set({"target_machine_failed", LookupError});
|
||||
LLVMContext Context;
|
||||
ProfileMatch PartitionMatch;
|
||||
std::vector<std::string> MismatchedNames;
|
||||
if (Annotating)
|
||||
Context.setDiagnosticHandler(
|
||||
std::make_unique<ProfileMatchHandler>(PartitionMatch));
|
||||
Context.setDiagnosticHandler(std::make_unique<ProfileMatchHandler>(
|
||||
PartitionMatch, &MismatchedNames));
|
||||
Expected<std::unique_ptr<Module>> Loaded =
|
||||
loadPartition(Options, Plan.Modules[I], I, Context, *Machine);
|
||||
if (!Loaded)
|
||||
return Failures.set({"partition_failed", toString(Loaded.takeError())});
|
||||
Module &Mod = **Loaded;
|
||||
MatchWeigher Weigher(Mod);
|
||||
if (!Options.ImportBitcode.empty()) {
|
||||
// See emit(): the attribute is inert without a sanitizer pass and
|
||||
// would block inlining of the imported runtime bodies.
|
||||
@@ -937,8 +1045,12 @@ compileOptimizedPartitions(const EmitOptions &Options,
|
||||
// Profile instrumentation or annotation happens here, before the thin
|
||||
// link; the post-link optimization reads the annotated weights.
|
||||
Pipeline P(*Machine, Level, profileOptions(Options));
|
||||
if (Annotating)
|
||||
if (Annotating) {
|
||||
// Registered first, so it lists the functions the profile itself
|
||||
// annotated, before block counts recover any.
|
||||
P.collectAnnotated(Weigher.Annotated);
|
||||
P.afterProfileAnnotation(PartitionMatch);
|
||||
}
|
||||
ModulePassManager MPM = P.PB.buildThinLTOPreLinkDefaultPipeline(Level);
|
||||
MPM.addPass(UnnameStructTypes());
|
||||
// The module hash is set at the thin link instead (see Names).
|
||||
@@ -948,6 +1060,8 @@ compileOptimizedPartitions(const EmitOptions &Options,
|
||||
MPM.run(Mod, P.MAM);
|
||||
}
|
||||
if (Annotating) {
|
||||
Weigher.Mismatched = std::move(MismatchedNames);
|
||||
Weigher.weigh(Options.ProfileUse, PartitionMatch);
|
||||
Match.add(PartitionMatch);
|
||||
// Stored before the summary, so a reused summary finds its counts.
|
||||
Cache.store(Key, "match", PartitionMatch.serialize());
|
||||
@@ -1046,6 +1160,13 @@ compileOptimizedPartitions(const EmitOptions &Options,
|
||||
CodeGeneration.run(Mod);
|
||||
return false;
|
||||
};
|
||||
else
|
||||
// The backend passes the module it owns; only the hook's signature is
|
||||
// const.
|
||||
Config.PreCodeGenModuleHook = [](unsigned, const Module &Optimized) {
|
||||
useBaselineBodies(const_cast<Module &>(Optimized));
|
||||
return true;
|
||||
};
|
||||
|
||||
// The link refers to module identifiers for its whole lifetime.
|
||||
std::vector<std::string> Names(Count);
|
||||
@@ -1445,15 +1566,23 @@ std::optional<EmitFailure>
|
||||
emitSplitCodegen(const EmitOptions &Options, Module &M, TargetMachine &Machine,
|
||||
OptimizationLevel Level, ProfileMatch &Match) {
|
||||
bool Annotating = !Options.ProfileUse.empty();
|
||||
MatchWeigher Weigher(M);
|
||||
if (Annotating)
|
||||
M.getContext().setDiagnosticHandler(
|
||||
std::make_unique<ProfileMatchHandler>(Match));
|
||||
std::make_unique<ProfileMatchHandler>(Match, &Weigher.Mismatched));
|
||||
{
|
||||
Pipeline P(Machine, Level, profileOptions(Options));
|
||||
if (Annotating)
|
||||
if (Annotating) {
|
||||
// Registered first, so it lists the functions the profile itself
|
||||
// annotated, before block counts recover any.
|
||||
P.collectAnnotated(Weigher.Annotated);
|
||||
P.afterProfileAnnotation(Match);
|
||||
}
|
||||
P.PB.buildPerModuleDefaultPipeline(Level).run(M, P.MAM);
|
||||
}
|
||||
if (Annotating)
|
||||
Weigher.weigh(Options.ProfileUse, Match);
|
||||
useBaselineBodies(M);
|
||||
if (std::optional<EmitFailure> Failure =
|
||||
verify(M, "post_optimization_verification_failed"))
|
||||
return Failure;
|
||||
|
||||
@@ -176,71 +176,108 @@ void balanceRoots(std::vector<Symbol> &Symbols, ArrayRef<unsigned> Roots,
|
||||
ArrayRef<char> Live, unsigned Count) {
|
||||
if (Count >= MinGroupedPartitions)
|
||||
return;
|
||||
// A root's size leaves out the digits of metadata references, which an
|
||||
// edit elsewhere in the module can renumber.
|
||||
auto TextBytes = [](StringRef Text) {
|
||||
uint64_t Bytes = 0;
|
||||
for (size_t I = 0; I < Text.size(); ++I) {
|
||||
++Bytes;
|
||||
if (Text[I] == '!')
|
||||
while (I + 1 < Text.size() && isDigit(Text[I + 1]))
|
||||
++I;
|
||||
}
|
||||
return Bytes;
|
||||
};
|
||||
DenseMap<unsigned, uint64_t> RootBytes;
|
||||
uint64_t Total = 0;
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I)
|
||||
if (Symbols[I].Kind == SymbolKind::Function && Live[I])
|
||||
RootBytes[Roots[I]] += Symbols[I].Header.size() + Symbols[I].Body.size();
|
||||
// Each root counts in whole 4 KiB units, so that an edit that grows a root
|
||||
// by less than that leaves every comparison, and every other root's
|
||||
// partition, unchanged.
|
||||
constexpr uint64_t Unit = 4096;
|
||||
for (auto &Entry : RootBytes) {
|
||||
Entry.second = (Entry.second + Unit - 1) / Unit * Unit;
|
||||
Total += Entry.second;
|
||||
}
|
||||
RootBytes[Roots[I]] +=
|
||||
TextBytes(Symbols[I].Header) + TextBytes(Symbols[I].Body);
|
||||
std::vector<unsigned> Order;
|
||||
for (auto &Entry : RootBytes)
|
||||
Order.push_back(Entry.first);
|
||||
llvm::sort(Order, [&](unsigned A, unsigned B) {
|
||||
return Symbols[A].Name < Symbols[B].Name;
|
||||
});
|
||||
DenseSet<unsigned> EntryRoots;
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I)
|
||||
if (Symbols[I].Entry && Live[I])
|
||||
EntryRoots.insert(Roots[I]);
|
||||
// A root of at least one unit counts in whole units, so that an edit that
|
||||
// grows it by less than that leaves every comparison, and every other
|
||||
// root's partition, unchanged.
|
||||
constexpr uint64_t Unit = 4096;
|
||||
uint64_t Total = 0;
|
||||
for (unsigned Root : Order)
|
||||
if (RootBytes[Root] >= Unit) {
|
||||
RootBytes[Root] = (RootBytes[Root] + Unit - 1) / Unit * Unit;
|
||||
Total += RootBytes[Root];
|
||||
}
|
||||
uint64_t Cap = Total * 5 / 4 / Count;
|
||||
std::vector<uint64_t> Load(Count, 0);
|
||||
// Partitions that take no further root.
|
||||
std::vector<char> Closed(Count, 0);
|
||||
DenseMap<unsigned, unsigned> Placed;
|
||||
auto FirstOpen = [&](unsigned Home) {
|
||||
for (unsigned K = 0; K < Count; ++K)
|
||||
if (!Closed[(Home + K) % Count])
|
||||
return (Home + K) % Count;
|
||||
return Home;
|
||||
};
|
||||
// A root without room anywhere stays in the first open partition from its
|
||||
// own.
|
||||
auto Place = [&](unsigned Root) {
|
||||
uint64_t Bytes = RootBytes[Root];
|
||||
unsigned Home = stableHash(Symbols[Root].Name) % Count, Chosen = Home;
|
||||
bool Found = false;
|
||||
for (unsigned K = 0; K < Count && !Found; ++K) {
|
||||
unsigned Home = stableHash(Symbols[Root].Name) % Count;
|
||||
unsigned Chosen = FirstOpen(Home);
|
||||
for (unsigned K = 0; K < Count; ++K) {
|
||||
unsigned P = (Home + K) % Count;
|
||||
if (!Closed[P] && Load[P] + Bytes <= Cap) {
|
||||
Chosen = P;
|
||||
Found = true;
|
||||
}
|
||||
}
|
||||
for (unsigned K = 0; K < Count && !Found; ++K) {
|
||||
unsigned P = (Home + K) % Count;
|
||||
if (!Closed[P]) {
|
||||
Chosen = P;
|
||||
Found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Load[Chosen] += Bytes;
|
||||
Placed[Root] = Chosen;
|
||||
};
|
||||
DenseSet<unsigned> EntryRoots;
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I)
|
||||
if (Symbols[I].Entry && Live[I])
|
||||
EntryRoots.insert(Roots[I]);
|
||||
// Entry roots first, each closing its partition, then roots of more than
|
||||
// half the cap, in name order, so that small roots fill around them; the
|
||||
// others follow in name order.
|
||||
// Entry roots first, each closing its partition; then roots of more than
|
||||
// half the cap, in name order, so that the others fill around them; then
|
||||
// the other roots of at least one unit, in name order.
|
||||
for (unsigned Root : Order)
|
||||
if (EntryRoots.contains(Root)) {
|
||||
Place(Root);
|
||||
Closed[Placed[Root]] = 1;
|
||||
}
|
||||
for (unsigned Root : Order)
|
||||
if (!EntryRoots.contains(Root) && RootBytes[Root] * 2 > Cap)
|
||||
if (!EntryRoots.contains(Root) && RootBytes[Root] >= Unit &&
|
||||
RootBytes[Root] * 2 > Cap)
|
||||
Place(Root);
|
||||
for (unsigned Root : Order)
|
||||
if (!EntryRoots.contains(Root) && RootBytes[Root] * 2 <= Cap)
|
||||
if (!EntryRoots.contains(Root) && RootBytes[Root] >= Unit &&
|
||||
RootBytes[Root] * 2 <= Cap)
|
||||
Place(Root);
|
||||
// Roots under one unit (runtime helpers, mostly) count in no load of the
|
||||
// larger roots, so a helper that an edit adds or removes moves no larger
|
||||
// root. Each goes to the first open partition from its name's whose load,
|
||||
// the helpers placed there included, is below the cap, or else to its
|
||||
// name's: the helpers fill the partitions with room, and once those are
|
||||
// full they spread by name instead of gathering in one partition (which
|
||||
// could then hold most of the module and have it optimized whole).
|
||||
std::vector<uint64_t> SmallLoad(Count, 0);
|
||||
for (unsigned Root : Order)
|
||||
if (!EntryRoots.contains(Root) && RootBytes[Root] < Unit) {
|
||||
unsigned Home = stableHash(Symbols[Root].Name) % Count;
|
||||
unsigned Chosen = FirstOpen(Home);
|
||||
for (unsigned K = 0; K < Count; ++K) {
|
||||
unsigned P = (Home + K) % Count;
|
||||
if (!Closed[P] && Load[P] + SmallLoad[P] < Cap) {
|
||||
Chosen = P;
|
||||
break;
|
||||
}
|
||||
}
|
||||
SmallLoad[Chosen] += RootBytes[Root];
|
||||
Placed[Root] = Chosen;
|
||||
}
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I)
|
||||
if (Symbols[I].Kind == SymbolKind::Function && Live[I])
|
||||
Symbols[I].Partition = Placed[Roots[I]];
|
||||
@@ -787,8 +824,30 @@ std::optional<ProgramPartitions> partitionProgramText(StringRef Text,
|
||||
Symbols[Root].Kind != SymbolKind::Function)
|
||||
Root = I;
|
||||
Roots[I] = Root;
|
||||
}
|
||||
// The x86-64-v3 copy of a --cpu-dispatch build's function (`<name>.v3`),
|
||||
// with the chain that ends at it, is placed with the chain of its baseline
|
||||
// copy (`<name>.base`), so that the helper can compare the two copies
|
||||
// after optimization (cpu-copies.h).
|
||||
DenseMap<unsigned, unsigned> CopyRoots;
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
StringRef Name = Symbols[I].Name;
|
||||
if (Symbols[I].Kind != SymbolKind::Function || !Live[I] ||
|
||||
Roots[I] != I || !Name.consume_back(".v3"))
|
||||
continue;
|
||||
auto Base = Index.find((Name + ".base").str());
|
||||
if (Base != Index.end() && Live[Base->second] &&
|
||||
Symbols[Base->second].Kind == SymbolKind::Function)
|
||||
CopyRoots[I] = Roots[Base->second];
|
||||
}
|
||||
for (unsigned I = 0; I < Symbols.size(); ++I) {
|
||||
if (Symbols[I].Kind == SymbolKind::Declaration || !Live[I])
|
||||
continue;
|
||||
auto Copy = CopyRoots.find(Roots[I]);
|
||||
if (Copy != CopyRoots.end())
|
||||
Roots[I] = Copy->second;
|
||||
if (Symbols[I].Kind == SymbolKind::Function)
|
||||
Symbols[I].Partition = stableHash(Symbols[Root].Name) % Count;
|
||||
Symbols[I].Partition = stableHash(Symbols[Roots[I]].Name) % Count;
|
||||
}
|
||||
placeGroups(Symbols, Roots, Count);
|
||||
balanceRoots(Symbols, Roots, Live, Count);
|
||||
|
||||
@@ -424,6 +424,16 @@ void annotateFromBlocks(Function &F, const FunctionBlocks &Record,
|
||||
markFrequency(F, EntryCount, MaxCount, Summary);
|
||||
}
|
||||
|
||||
void BlockRecorder::addNeverRan(const Function &F) {
|
||||
std::string Text;
|
||||
raw_string_ostream OS(Text);
|
||||
OS << "f ";
|
||||
writeEscaped(OS, F.getName());
|
||||
OS << " 0 0\n";
|
||||
std::lock_guard<std::mutex> Guard(Lock);
|
||||
Records.emplace(F.getName().str(), std::move(Text));
|
||||
}
|
||||
|
||||
void BlockRecorder::add(const Function &F) {
|
||||
std::optional<Function::ProfileCount> Entry = F.getEntryCount();
|
||||
if (!Entry || Entry->isSynthetic())
|
||||
|
||||
@@ -71,6 +71,8 @@ class BlockRecorder {
|
||||
public:
|
||||
// Records F's block counts, which the profile annotation just gave it.
|
||||
void add(const llvm::Function &F);
|
||||
// Records F, which the profile has no record of, as never run.
|
||||
void addNeverRan(const llvm::Function &F);
|
||||
// Writes the file through a private sibling; returns an error message on
|
||||
// failure.
|
||||
std::optional<std::string> write(llvm::StringRef Path);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"test:ts7": "node scripts/test-ts7.mjs",
|
||||
"test:checker-identity": "node scripts/checker-identity.mjs",
|
||||
"generate:ts7-ast": "node scripts/generate-ts7-ast-schema.mjs",
|
||||
"generate:ir-codec": "node scripts/generate-ir-codec.mjs",
|
||||
"generate:ir-fields": "node scripts/generate-ir-fields.mjs",
|
||||
"test:fetch-conformance": "vitest run tests/harness/fetch-conformance.test.ts",
|
||||
"test:test262": "tsx tests/test262/run.ts",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
@@ -22,8 +23,10 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
function fixture() {
|
||||
const root = mkdtempSync(
|
||||
join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-install-"),
|
||||
// installNativeCli resolves the installation directory, and on macOS /tmp
|
||||
// is a link to /private/tmp, so the fixture's paths start resolved.
|
||||
const root = realpathSync(
|
||||
mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-install-")),
|
||||
);
|
||||
roots.push(root);
|
||||
const directory = join(root, "installation");
|
||||
|
||||
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
minimum_os: "14.0",
|
||||
},
|
||||
program: { object, entry_symbol: "main" },
|
||||
runtime_abi: { version: 47, marker: "scr_runtime_abi_v47" },
|
||||
runtime_abi: { version: 48, marker: "scr_runtime_abi_v48" },
|
||||
runtime_pack: {
|
||||
kind: "precompiled",
|
||||
package: "@scriptc/runtime-darwin-arm64",
|
||||
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
() => null,
|
||||
(failure: { stderr?: string }) => failure,
|
||||
);
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v47");
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v48");
|
||||
});
|
||||
});
|
||||
|
||||
+6
-1
@@ -5,7 +5,12 @@
|
||||
* checks annotated listener parameters against it (unannotated non-empty
|
||||
* parameter lists have no static types and fence at the registration). */
|
||||
declare module "events" {
|
||||
class EventEmitter {
|
||||
/* Generic like @types/node's class: the type argument is the listener
|
||||
* MAP, a pure typing aid with no runtime layout (Node erases it too), so
|
||||
* `class Sink extends EventEmitter<Map>` adopts the same emitter base.
|
||||
* The map stays unconstrained here — the members below are the untyped
|
||||
* `...args: any[]` surface either way. */
|
||||
class EventEmitter<EventMap = Record<string, any[]>> {
|
||||
constructor();
|
||||
static defaultMaxListeners: number;
|
||||
/* Listeners registered under this symbol observe 'error' emits first
|
||||
|
||||
@@ -210,7 +210,7 @@ declare module "fs/promises" {
|
||||
export function access(path: string | URL, mode?: number): Promise<void>;
|
||||
export function truncate(path: string | URL, len?: number): Promise<void>;
|
||||
export function appendFile(path: string | URL, data: string | Uint8Array): Promise<void>;
|
||||
export function copyFile(src: string | URL, dest: string | URL): Promise<void>;
|
||||
export function copyFile(src: string | URL, dest: string | URL, mode?: number): Promise<void>;
|
||||
export function mkdtemp(prefix: string | URL): Promise<string>;
|
||||
export function rmdir(path: string | URL): Promise<void>;
|
||||
export function rename(oldPath: string | URL, newPath: string | URL): Promise<void>;
|
||||
|
||||
+54
-10
@@ -30,8 +30,10 @@ declare module "node:fs" {
|
||||
export function writeFileSync(
|
||||
path: string | URL,
|
||||
data: string,
|
||||
/* The options-record stance (see http.RequestOptions): flag fences
|
||||
* by name at the call site; undocumented keys drop like Node. */
|
||||
/* The options-record stance (see http.RequestOptions): an unsupported
|
||||
* flag fences by name at the call site; undocumented keys drop like
|
||||
* Node. The lowered flags are "w" (truncate-or-create) and "wx"
|
||||
* (exclusive create). */
|
||||
options?: {
|
||||
mode?: number;
|
||||
encoding?: "utf8" | "utf-8";
|
||||
@@ -157,9 +159,11 @@ declare module "node:fs" {
|
||||
export function lchownSync(path: string | URL, uid: number, gid: number): void;
|
||||
export function fchownSync(fd: number, uid: number, gid: number): void;
|
||||
export function truncateSync(path: string | URL, len?: number): void;
|
||||
/* The 2-argument form only (Node's mode flags have no lowering). The
|
||||
* destination is created or truncated carrying the SOURCE's mode. */
|
||||
export function copyFileSync(src: string | URL, dest: string | URL): void;
|
||||
/* The destination is created or truncated carrying the SOURCE's mode.
|
||||
* `mode` is libuv's copyfile flag bits: COPYFILE_EXCL (1) fails when the
|
||||
* destination exists, COPYFILE_FICLONE (2) requests a reflink and falls
|
||||
* back to a copy, COPYFILE_FICLONE_FORCE (4) requires one. */
|
||||
export function copyFileSync(src: string | URL, dest: string | URL, mode?: number): void;
|
||||
export function rename(
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
@@ -604,11 +608,29 @@ declare module "node:fs" {
|
||||
): void;
|
||||
/* fs.watch — the file-watching slice (scr_watch.c, kqueue EVFILT_VNODE):
|
||||
* "rename"/"change" events on ONE path, watcher.close() to stop. The
|
||||
* listener takes zero parameters or the eventType string (the filename
|
||||
* parameter has no lowering); an open watcher keeps the loop alive. A
|
||||
* path that cannot be opened THROWS Node's fs error synchronously. */
|
||||
* listener takes (), (eventType) or (eventType, filename); an open
|
||||
* watcher keeps the loop alive. A path that cannot be opened THROWS
|
||||
* Node's fs error synchronously. */
|
||||
export type WatchEventType = "rename" | "change";
|
||||
/* (eventType, filename): filename is the changed entry's name (null when
|
||||
* the platform does not provide one). */
|
||||
export type WatchListener<T> = (event: WatchEventType, filename: T | null) => void;
|
||||
export interface FSWatcher {
|
||||
close(): void;
|
||||
on(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this;
|
||||
on(event: "error", listener: (error: Error) => void): this;
|
||||
on(event: "close", listener: () => void): this;
|
||||
once(event: "change", listener: (eventType: string, filename: string | Buffer) => void): this;
|
||||
once(event: "error", listener: (error: Error) => void): this;
|
||||
once(event: "close", listener: () => void): this;
|
||||
addListener(
|
||||
event: "change",
|
||||
listener: (eventType: string, filename: string | Buffer) => void,
|
||||
): this;
|
||||
addListener(event: "error", listener: (error: Error) => void): this;
|
||||
addListener(event: "close", listener: () => void): this;
|
||||
off(event: "change" | "error" | "close", listener: (...args: any[]) => void): this;
|
||||
removeListener(event: "change" | "error" | "close", listener: (...args: any[]) => void): this;
|
||||
}
|
||||
/* The options-record stance (see http.RequestOptions): persistent: true,
|
||||
* recursive: false, and encoding: "utf8" state the lowered behavior and
|
||||
@@ -620,12 +642,34 @@ declare module "node:fs" {
|
||||
encoding?: string;
|
||||
[option: string]: unknown;
|
||||
}
|
||||
export function watch(path: string, listener?: (eventType: string) => void): FSWatcher;
|
||||
export function watch(path: string, listener?: WatchListener<string>): FSWatcher;
|
||||
export function watch(
|
||||
path: string,
|
||||
options: WatchOptions,
|
||||
listener?: (eventType: string) => void,
|
||||
listener?: WatchListener<string>,
|
||||
): FSWatcher;
|
||||
/* fs.watchFile — stat polling (scr_watch_file.c, libuv's uv_fs_poll
|
||||
* shape): the listener runs with (current, previous) Stats when a poll
|
||||
* sees a difference, the first poll only records the baseline, and a
|
||||
* path that cannot be stat'd polls as all-zero Stats. One poller per
|
||||
* resolved path is shared by every listener on it. interval defaults to
|
||||
* 5007 ms, persistent defaults to true; bigint: true (BigIntStats) has
|
||||
* no lowering. The call is a statement: Node's StatWatcher has no
|
||||
* lowering. */
|
||||
export interface StatWatcher {}
|
||||
export function watchFile(
|
||||
filename: string,
|
||||
options: { interval?: number; persistent?: boolean; bigint?: false },
|
||||
listener: (current: Stats, previous: Stats) => void,
|
||||
): StatWatcher;
|
||||
export function watchFile(
|
||||
filename: string,
|
||||
listener: (current: Stats, previous: Stats) => void,
|
||||
): StatWatcher;
|
||||
export function unwatchFile(
|
||||
filename: string,
|
||||
listener?: (current: Stats, previous: Stats) => void,
|
||||
): void;
|
||||
/* fs.promises IS the fs/promises module (Node's rule) — the namespace-
|
||||
* import form `fs.promises.readFile(...)` lowers through the same
|
||||
* fs/promises table as `import { readFile } from "node:fs/promises"`. */
|
||||
|
||||
@@ -257,6 +257,23 @@ declare module "net" {
|
||||
event: "request" | "checkContinue" | "checkExpectation",
|
||||
listener: (req: import("http").IncomingMessage, res: import("http").ServerResponse) => void,
|
||||
): void;
|
||||
/* off / removeListener: the LAST listener standing for the same
|
||||
* function leaves that event's list (Node's EventEmitter order). The
|
||||
* server registries that keep a removable list: error, close,
|
||||
* listening, connection/secureConnection, timeout, and drop. */
|
||||
off(event: "error", listener: (err: Error) => void): void;
|
||||
off(event: "close" | "listening", listener: () => void): void;
|
||||
off(event: "connection" | "secureConnection", listener: (socket: Socket) => void): void;
|
||||
off(event: "timeout", listener: (socket: Socket) => void): void;
|
||||
off(event: "drop", listener: (data?: DropArgument) => void): void;
|
||||
removeListener(event: "error", listener: (err: Error) => void): void;
|
||||
removeListener(event: "close" | "listening", listener: () => void): void;
|
||||
removeListener(
|
||||
event: "connection" | "secureConnection",
|
||||
listener: (socket: Socket) => void,
|
||||
): void;
|
||||
removeListener(event: "timeout", listener: (socket: Socket) => void): void;
|
||||
removeListener(event: "drop", listener: (data?: DropArgument) => void): void;
|
||||
}
|
||||
/* The 'drop' payload: the turned-away connection's addresses. */
|
||||
export interface DropArgument {
|
||||
|
||||
+57
-13
@@ -246,12 +246,13 @@ declare var process: {
|
||||
/* "v" plus versions.node: the compatibility target's release. */
|
||||
readonly version: string;
|
||||
/* The build-configuration snapshot Node exposes from its gyp config.
|
||||
* A scriptc binary has no V8 and no gyp, so `variables` answers the
|
||||
* honest capability record (v8_enable_i18n_support: 0 — no ICU; asan
|
||||
* mirrors the sanitizer build flag) and `target_defaults` is absent —
|
||||
* probe code takes its documented fallbacks. Reads of undeclared
|
||||
* variables go through the index signature (string | number | boolean
|
||||
* | undefined) and answer undefined. */
|
||||
* A scriptc binary has no V8 and no gyp, so `variables` answers what is
|
||||
* true of a compiled binary: v8_enable_i18n_support is 1 like Node's
|
||||
* (the read links ICU, which the binary then carries), while the gyp
|
||||
* variables it cannot answer (icu_path, asan, …) read undefined through
|
||||
* the index signature (string | number | boolean | undefined), and
|
||||
* `target_defaults` is absent — probe code takes its documented
|
||||
* fallbacks. */
|
||||
readonly config: {
|
||||
readonly variables: { readonly [name: string]: string | number | boolean | undefined };
|
||||
readonly target_defaults?: { readonly default_configuration: string };
|
||||
@@ -340,6 +341,12 @@ declare var process: {
|
||||
* (chunks stay bytes; decode with TextDecoder). */
|
||||
stdin: {
|
||||
readonly isTTY: boolean;
|
||||
/* The descriptor the stream is built over (Node reports 0 here and
|
||||
* 1/2 on stdout/stderr, whatever the underlying file is). */
|
||||
readonly fd: 0;
|
||||
/* True while a TTY stdin runs in raw mode; Node's own property is
|
||||
* undefined on a non-TTY stdin (the `x || false` idiom). */
|
||||
readonly isRaw: boolean;
|
||||
on(event: "data", listener: (chunk: Uint8Array) => void): void;
|
||||
on(event: "end", listener: () => void): void;
|
||||
on(event: "error", listener: (err: Error) => void): void;
|
||||
@@ -374,6 +381,12 @@ declare var process: {
|
||||
* removal see the same value. */
|
||||
on(event: "SIGINT" | "SIGTERM", listener: () => void): void;
|
||||
on(event: "exit", listener: (code: number) => void): void;
|
||||
/* prependListener — the same registration at the FRONT of the event's
|
||||
* listener list (Node's EventEmitter order), for the registries that
|
||||
* own a prepend entry ('exit' and the signal family). prependOnceListener
|
||||
* has no lowering. */
|
||||
prependListener(event: "SIGINT" | "SIGTERM", listener: () => void): void;
|
||||
prependListener(event: "exit", listener: (code: number) => void): void;
|
||||
/* 'warning' — Node's process-warning channel: listeners receive the
|
||||
* warning Error (as a dyn value); emitWarning and the runtime's
|
||||
* deprecation sites dispatch synchronously (SEMANTICS.md). */
|
||||
@@ -1115,9 +1128,9 @@ declare var ReadableStream: {
|
||||
from(iterable: string): ReadableStream<string>;
|
||||
readonly prototype: ReadableStream<unknown>;
|
||||
};
|
||||
/* The writable side of a TransformStream and its writer. The
|
||||
* WritableStream constructor has no native lowering; writable streams come
|
||||
* from `new TransformStream()`. */
|
||||
/* The writable side of a TransformStream and its writer. A program that
|
||||
* constructs a WritableStream, or a TransformStream with a transformer,
|
||||
* builds its streams with the web stream classes (scr_webstreams.c). */
|
||||
interface WritableStreamDefaultWriter<W = unknown> {
|
||||
readonly closed: Promise<void>;
|
||||
readonly desiredSize: number | null;
|
||||
@@ -1133,14 +1146,25 @@ interface WritableStream<W = unknown> {
|
||||
close(): Promise<void>;
|
||||
getWriter(): WritableStreamDefaultWriter<W>;
|
||||
}
|
||||
/* The identity transform: chunks written to `writable` are read from
|
||||
* `readable`, and a write completes when its chunk is read. */
|
||||
/* Without a transformer, the identity transform: chunks written to
|
||||
* `writable` are read from `readable`, and a write completes when its chunk
|
||||
* is read. */
|
||||
interface TransformStream<I = unknown, O = unknown> {
|
||||
readonly readable: ReadableStream<O>;
|
||||
readonly writable: WritableStream<I>;
|
||||
}
|
||||
interface Transformer<I = unknown, O = unknown> {
|
||||
start?(controller: TransformStreamDefaultController<O>): unknown;
|
||||
transform?(chunk: I, controller: TransformStreamDefaultController<O>): unknown;
|
||||
flush?(controller: TransformStreamDefaultController<O>): unknown;
|
||||
cancel?(reason?: unknown): unknown;
|
||||
}
|
||||
declare var TransformStream: {
|
||||
new <I = unknown, O = I>(): TransformStream<I, O>;
|
||||
new <I = unknown, O = I>(
|
||||
transformer?: Transformer<I, O>,
|
||||
writableStrategy?: QueuingStrategy<I>,
|
||||
readableStrategy?: QueuingStrategy<O>,
|
||||
): TransformStream<I, O>;
|
||||
readonly prototype: TransformStream<unknown, unknown>;
|
||||
};
|
||||
/* The other web stream interfaces. JavaScript sources construct and read
|
||||
@@ -1158,8 +1182,14 @@ interface WritableStreamDefaultController {
|
||||
readonly signal: AbortSignal;
|
||||
error(reason?: unknown): void;
|
||||
}
|
||||
interface UnderlyingSink<W = unknown> {
|
||||
start?(controller: WritableStreamDefaultController): unknown;
|
||||
write?(chunk: W, controller: WritableStreamDefaultController): unknown;
|
||||
close?(): unknown;
|
||||
abort?(reason?: unknown): unknown;
|
||||
}
|
||||
declare var WritableStream: {
|
||||
new <W = unknown>(sink?: object, strategy?: QueuingStrategy<W>): WritableStream<W>;
|
||||
new <W = unknown>(sink?: UnderlyingSink<W>, strategy?: QueuingStrategy<W>): WritableStream<W>;
|
||||
readonly prototype: WritableStream<unknown>;
|
||||
};
|
||||
declare var WritableStreamDefaultWriter: {
|
||||
@@ -1747,6 +1777,20 @@ declare module "tty" {
|
||||
/* The bytes queued behind the last write: the compiled binary's fd
|
||||
* 1/2 writes are synchronous, so it is always 0. */
|
||||
readonly writableLength: number;
|
||||
/* The descriptor behind the stream (1 for stdout, 2 for stderr). */
|
||||
readonly fd: number;
|
||||
/* Node's default for the process streams (65536 for a pipe, a file and a
|
||||
* TTY): a constant, because nothing is ever queued. */
|
||||
readonly writableHighWaterMark: number;
|
||||
/* Always false: a synchronous write never returns false, so no 'drain'
|
||||
* is ever pending. */
|
||||
readonly writableNeedDrain: boolean;
|
||||
/* 'drain' and the other events by name. Node returns the stream. */
|
||||
on(event: string, listener: (...args: any[]) => void): this;
|
||||
once(event: string, listener: (...args: any[]) => void): this;
|
||||
addListener(event: string, listener: (...args: any[]) => void): this;
|
||||
off(event: string, listener: (...args: any[]) => void): this;
|
||||
removeListener(event: string, listener: (...args: any[]) => void): this;
|
||||
getWindowSize(): [number, number];
|
||||
}
|
||||
export function isatty(fd: number): boolean;
|
||||
|
||||
@@ -19,10 +19,17 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ
|
||||
...(features.eventTarget ? { eventTarget: true } : {}),
|
||||
...(features.wasm ? { wasm: true } : {}),
|
||||
...(features.warningReplace ? { warningReplace: true } : {}),
|
||||
...(features.recordViewPartial ? { recordViewPartial: true } : {}),
|
||||
...(features.stdioExtended ? { stdioExtended: true } : {}),
|
||||
...(features.fsFlags ? { fsFlags: true } : {}),
|
||||
...(features.strdecDyn ? { strdecDyn: true } : {}),
|
||||
...(features.processExtras ? { processExtras: true } : {}),
|
||||
...(features.streamWriteErrors ? { streamWriteErrors: true } : {}),
|
||||
...(features.stdioWriteSlot ? { stdioWriteSlot: true } : {}),
|
||||
...(features.nodeIsland ? { nodeIsland: true } : {}),
|
||||
...(features.moduleNamespace ? { moduleNamespace: true } : {}),
|
||||
...(features.moduleNamespaceDyn ? { moduleNamespaceDyn: true } : {}),
|
||||
...(features.primitiveWrapper ? { primitiveWrapper: true } : {}),
|
||||
zlib: features.zlib,
|
||||
...(features.brotli ? { brotli: true } : {}),
|
||||
assert: features.assert,
|
||||
@@ -67,6 +74,7 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ
|
||||
dgramDyn: features.dgramDyn,
|
||||
textCodecDyn: features.textCodecDyn,
|
||||
watch: features.fsWatch,
|
||||
watchFile: features.fsWatchFile,
|
||||
foreignFfi: hasForeignFfiCallback(mod.ffiImports ?? []),
|
||||
nodeTest: features.nodeTest,
|
||||
tls: features.tls,
|
||||
@@ -79,5 +87,6 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ
|
||||
x509: features.x509,
|
||||
webcrypto: features.webcrypto,
|
||||
perfLoop: features.perfLoop,
|
||||
...(features.icu ? { intl: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ export async function compilePackedLibrary(
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
watch: false,
|
||||
watchFile: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
tls: false,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { existsSync, realpathSync, statSync } from "node:fs";
|
||||
import { mkdtemp, readFile, rename, rm, stat } from "node:fs/promises";
|
||||
import { copyFile, mkdtemp, readFile, rename, rm, stat } from "node:fs/promises";
|
||||
import { delimiter } from "node:path";
|
||||
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
@@ -21,7 +21,11 @@ import {
|
||||
toolchainEnvironmentFingerprint,
|
||||
} from "./toolchain-environment.js";
|
||||
import { type NativeArtifactDependency } from "./native/contracts.js";
|
||||
import { RuntimePackError, stageRuntimePackArtifacts } from "./runtime-pack.js";
|
||||
import {
|
||||
RuntimePackError,
|
||||
stageRuntimePackArtifacts,
|
||||
type RuntimePackSelection,
|
||||
} from "./runtime-pack.js";
|
||||
import type { NativeLinkPlan } from "./link-plan.js";
|
||||
import { inputsWithoutLld, lldLinkerArgs } from "./lld.js";
|
||||
import { hugeTextLinkerArgs, rewriteHugeTextExecutable } from "./huge-text.js";
|
||||
@@ -225,6 +229,38 @@ async function objectLinkerDependencyPaths(
|
||||
.sort();
|
||||
}
|
||||
|
||||
/** Install the selected runtime pack's RUN-time data files (ICU's
|
||||
* icudt78l.dat) beside the executable that links them. The compiler's link
|
||||
* step is the only moment the executable's directory is known, and a data
|
||||
* file next to the binary is what the runtime looks for first.
|
||||
*
|
||||
* A file already present at the pinned size is left alone: the pack ships
|
||||
* one data set per file name, and re-copying 11.1 MiB on every build would
|
||||
* be pure cost. The copy is atomic (private temporary, then rename) so two
|
||||
* concurrent builds of the same program never observe a partial file. */
|
||||
export async function installRuntimeDataFiles(
|
||||
selection: RuntimePackSelection,
|
||||
outputPath: string,
|
||||
): Promise<void> {
|
||||
if (selection.dataFiles.length === 0) return;
|
||||
const outputDir = dirname(resolve(outputPath));
|
||||
for (const source of selection.dataFiles) {
|
||||
const destination = join(outputDir, basename(source));
|
||||
const sourceSize = (await stat(source)).size;
|
||||
const existing = await stat(destination).catch(() => null);
|
||||
if (existing?.isFile() && existing.size === sourceSize) continue;
|
||||
const temporary = join(
|
||||
outputDir,
|
||||
`.${basename(destination)}.scriptc-${process.pid}-${randomUUID()}`,
|
||||
);
|
||||
await copyFile(source, temporary);
|
||||
await rename(temporary, destination).catch(async () => {
|
||||
await rm(destination, { force: true });
|
||||
await rename(temporary, destination);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function linkNativeExecutable(
|
||||
plan: NativeLinkPlan,
|
||||
options: {
|
||||
@@ -314,6 +350,7 @@ export async function linkNativeExecutable(
|
||||
await rm(plan.outputPath, { force: true });
|
||||
await rename(privateOut, plan.outputPath);
|
||||
});
|
||||
await installRuntimeDataFiles(plan.runtimePack, plan.outputPath);
|
||||
if (plan.target.platform === "darwin" && !plan.darwinDebugSymbols) {
|
||||
await rm(`${plan.outputPath}.dSYM`, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
BOOL,
|
||||
BYTES_U8,
|
||||
DYN,
|
||||
F64,
|
||||
STRING,
|
||||
VOID,
|
||||
funcOf,
|
||||
type IrExpr,
|
||||
@@ -260,6 +263,59 @@ test("receiver elision admits only synchronous receiver-free work", () => {
|
||||
expect(callbackIgnoresReceiver(fn)).toBe(false);
|
||||
});
|
||||
|
||||
test("receiver elision admits library work that runs no user code", () => {
|
||||
const str: IrExpr = { kind: "strLit", value: "a", type: STRING, loc };
|
||||
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
|
||||
const dyn = ref("d", DYN);
|
||||
const ignores = (expr: IrExpr): boolean => {
|
||||
const fn = fixture().functions[0]!;
|
||||
fn.body.unshift({ kind: "exprStmt", expr, loc });
|
||||
return callbackIgnoresReceiver(fn);
|
||||
};
|
||||
const lib = (fn: string, args: IrExpr[]): IrExpr =>
|
||||
({ kind: "libCall", fn, args, type: F64, loc }) as IrExpr;
|
||||
const intrinsic = (name: string, args: IrExpr[]): IrExpr =>
|
||||
({ kind: "intrinsic", name, args, type: VOID, loc }) as IrExpr;
|
||||
// String and number members over primitives; a callback operand may run.
|
||||
expect(ignores(lib("string.lastIndexOfFrom", [str, str, num(0)]))).toBe(true);
|
||||
expect(ignores(lib("num.sameValue", [num(0), num(1)]))).toBe(true);
|
||||
expect(ignores(lib("string.lastIndexOf", [str, closure("predicate")]))).toBe(false);
|
||||
expect(ignores(lib("perf.now", []))).toBe(true);
|
||||
// `new Error(message, options)` with a primitive message and no options.
|
||||
const message: IrExpr = { kind: "dynFrom", value: str, type: DYN, loc };
|
||||
expect(ignores(lib("error.newOptions", [message, message]))).toBe(true);
|
||||
expect(ignores(lib("error.newOptions", [message, dyn]))).toBe(false);
|
||||
// The module binding check only throws; console output may inspect.
|
||||
const initialized: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc };
|
||||
expect(ignores(intrinsic("module.tdzCheck", [initialized, str]))).toBe(true);
|
||||
expect(ignores(intrinsic("console.log", [dyn]))).toBe(false);
|
||||
// A typed array built from nothing or a typed source; a dynamic source
|
||||
// may be an iterable.
|
||||
const bytes = (source: IrExpr | null): IrExpr => ({
|
||||
kind: "bytesNew",
|
||||
source,
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
});
|
||||
expect(ignores(bytes(null))).toBe(true);
|
||||
expect(ignores(bytes(dyn))).toBe(false);
|
||||
});
|
||||
|
||||
test("receiver elision admits array for-of loops", () => {
|
||||
const fn = fixture().functions[0]!;
|
||||
const numbers = { kind: "array", elem: F64 } as const;
|
||||
const one: IrExpr = { kind: "numLit", value: 1, type: F64, loc };
|
||||
fn.locals.push({ id: "x", name: "x", type: F64, mutable: false });
|
||||
fn.body.unshift({
|
||||
kind: "forOf",
|
||||
localId: "x",
|
||||
iterable: { kind: "arrayLit", elems: [one], type: numbers, loc },
|
||||
body: [],
|
||||
loc,
|
||||
});
|
||||
expect(callbackIgnoresReceiver(fn)).toBe(true);
|
||||
});
|
||||
|
||||
function callsOf(name: string, callee: string, extra: IrStmt[] = []): IrFunction {
|
||||
return {
|
||||
name,
|
||||
|
||||
@@ -12,17 +12,18 @@ export const CPU_DISPATCH_TIERS: readonly TargetCpu[] = ["x86-64-v3"];
|
||||
const LEVEL = "@sc_cpu_level";
|
||||
|
||||
/** The attribute groups of the x86-64-v3 copies: #0's and #2's (see
|
||||
* emit-module.ts functionAttributes) with the level's `target-cpu`. */
|
||||
* emit-module.ts functionAttributes) with the level's `target-cpu`, and
|
||||
* `scriptc-cpu-copy`, by which the LLVM helper finds the copies. After
|
||||
* optimization, a copy whose code gains nothing from x86-64-v3 takes its
|
||||
* baseline copy's code (native/llvm-codegen/src/cpu-copies.h). */
|
||||
const COPY_GROUPS: ReadonlyMap<string, string> = new Map([
|
||||
["0", "3"],
|
||||
["2", "4"],
|
||||
]);
|
||||
|
||||
export function cpuDispatchAttributes(common: string): string[] {
|
||||
return [
|
||||
`attributes #3 = {${common} "target-cpu"="x86-64-v3" }`,
|
||||
`attributes #4 = { noinline${common} "target-cpu"="x86-64-v3" }`,
|
||||
];
|
||||
const copy = `"target-cpu"="x86-64-v3" "scriptc-cpu-copy"`;
|
||||
return [`attributes #3 = {${common} ${copy} }`, `attributes #4 = { noinline${common} ${copy} }`];
|
||||
}
|
||||
|
||||
/** Bits of CPUID leaf 1's ECX that x86-64-v3 needs: SSE3 (0), SSSE3 (9),
|
||||
|
||||
@@ -57,6 +57,7 @@ import { dynDesc, streamTypedRefEligible } from "../../ir/analysis.js";
|
||||
import { mangleRecordNew, mangleRecordStruct } from "../mangle.js";
|
||||
import { BlockBuilder } from "./blocks.js";
|
||||
import { llvmCommentText } from "./common.js";
|
||||
import { JS_BITS } from "./jsval-encoding.js";
|
||||
import {
|
||||
arrNewCall,
|
||||
elemAccess,
|
||||
@@ -219,6 +220,21 @@ export interface DynHost extends WalkerHost {
|
||||
classValueSubtypes(className: string): readonly string[];
|
||||
/** emitter.ts's pendingTestLines: the inline pending-exception test. */
|
||||
pendingTestLines(dest: string): string[];
|
||||
/** The closures with a jsval body (IrFunction.jsEntry) on targets with
|
||||
* jsval words, which the box builders give a jsval entry. */
|
||||
jsEntryClosures?(): readonly JsEntryClosure[];
|
||||
}
|
||||
|
||||
/** A closure with a jsval body (IrFunction.jsEntry), for the box builders. */
|
||||
export interface JsEntryClosure {
|
||||
/** The closure's entry symbol (ScrClosure.fn) and signature. */
|
||||
closure: string;
|
||||
params: readonly IrType[];
|
||||
ret: IrType;
|
||||
/** The jsval body's entry symbol and signature. */
|
||||
body: string;
|
||||
bodyParams: readonly IrType[];
|
||||
bodyRet: IrType;
|
||||
}
|
||||
|
||||
const FN_ATTRS = "#0";
|
||||
@@ -247,6 +263,7 @@ export class LlDyn {
|
||||
private readonly toDynFns = new Map<string, string>();
|
||||
private readonly dynFuncThunks = new Map<string, string>();
|
||||
private readonly dynFuncBoxes = new Map<string, string>();
|
||||
private readonly jsEntryThunks = new Map<string, string>();
|
||||
private readonly dynFuncAdapters = new Map<string, string>();
|
||||
private readonly messageListeners = new Map<string, string | null>();
|
||||
private readonly messageReaders = new Map<string, string>();
|
||||
@@ -290,6 +307,8 @@ export class LlDyn {
|
||||
return this.dynFuncThunkHelper(args[0] as IrType & { kind: "func" });
|
||||
case "dyn.dynFuncBoxHelper":
|
||||
return this.dynFuncBoxHelper(args[0] as IrType & { kind: "func" });
|
||||
case "dyn.jsEntryThunk":
|
||||
return this.jsEntryThunk(args[0] as string, args[1] as IrType[], args[2] as IrType);
|
||||
case "dyn.dynFuncAdapterHelper":
|
||||
return this.dynFuncAdapterHelper(args[0] as IrType & { kind: "func" });
|
||||
case "dyn.dynToStrHelper":
|
||||
@@ -1179,8 +1198,11 @@ export class LlDyn {
|
||||
const struct = mangleRecordStruct(sourceId);
|
||||
const base = host.nullableUnions.fieldBase(sourceId);
|
||||
const offsets = target.fields.map((field) => {
|
||||
const index = source.fields.findIndex((f) => f.name === field.name) + base;
|
||||
return `${S} ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 0, i32 ${index}) to ${S})`;
|
||||
const index = source.fields.findIndex((f) => f.name === field.name);
|
||||
// A partial view's target field the source does not declare has
|
||||
// no slot: all ones (SIZE_MAX) marks it absent (record-views.ts).
|
||||
if (index < 0) return `${S} -1`;
|
||||
return `${S} ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 0, i32 ${index + base}) to ${S})`;
|
||||
});
|
||||
const offsetTable = `${table}_${k}`;
|
||||
this.defs.push(
|
||||
@@ -5534,6 +5556,101 @@ export class LlDyn {
|
||||
return name;
|
||||
}
|
||||
|
||||
/** `sc_jt_<n>(ptr c, ptr args, size argc) -> ptr`, a function value's
|
||||
* jsval entry (ScrDynJsEntry) into a closure's jsval body `target`
|
||||
* (IrFunction.jsEntry, 64-bit targets): passes each argument word to the
|
||||
* body, a missing one as undefined and a dyn parameter's boxed, and
|
||||
* returns the body's result as an owned word. Like the closure's own
|
||||
* entry, the body consumes its arguments. With an exception pending the
|
||||
* result is undefined. */
|
||||
jsEntryThunk(target: string, params: readonly IrType[], ret: IrType): string {
|
||||
const existing = this.jsEntryThunks.get(target);
|
||||
if (existing) return existing;
|
||||
const name = `sc_jt_${this.jsEntryThunks.size}`;
|
||||
this.jsEntryThunks.set(target, name);
|
||||
this.host.journalAllocated?.(name, "dyn.jsEntryThunk", [target, params, ret]);
|
||||
const host = this.host;
|
||||
const B = new BlockBuilder();
|
||||
const undefinedWord = `inttoptr (i64 ${JS_BITS.undefined} to ptr)`;
|
||||
const args = params.map((p, i) => {
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
B.line(`store ptr ${undefinedWord}, ptr ${slot}`);
|
||||
const has = B.tmp();
|
||||
B.line(`${has} = icmp ult ${host.sizeType} ${i}, %argc`);
|
||||
const lHas = B.newLabel("jt.h");
|
||||
const lj = B.newLabel("jt.j");
|
||||
B.condBr(has, lHas, lj);
|
||||
B.startBlock(lHas);
|
||||
const ap = B.tmp();
|
||||
const av = B.tmp();
|
||||
B.line(`${ap} = getelementptr inbounds ptr, ptr %args, ${host.sizeType} ${i}`);
|
||||
B.line(`${av} = load ptr, ptr ${ap}`);
|
||||
B.line(`store ptr ${av}, ptr ${slot}`);
|
||||
B.br(lj);
|
||||
B.startBlock(lj);
|
||||
const word = B.tmp();
|
||||
B.line(`${word} = load ptr, ptr ${slot}`);
|
||||
const owned = B.tmp();
|
||||
if (p.kind === "jsval") {
|
||||
host.declare(`declare ptr @scr_js_retain(ptr)`);
|
||||
B.line(`${owned} = call ptr @scr_js_retain(ptr ${word})`);
|
||||
} else if (p.kind === "dyn") {
|
||||
host.declare(`declare ptr @scr_js_to_dyn_borrow(ptr)`);
|
||||
B.line(`${owned} = call ptr @scr_js_to_dyn_borrow(ptr ${word})`);
|
||||
} else {
|
||||
throw new InternalCompilerError(`llvm emitter bug: jsval entry parameter ${p.kind}`);
|
||||
}
|
||||
return `ptr ${owned}`;
|
||||
});
|
||||
const callArgs = [`ptr %c`, ...args].join(", ");
|
||||
if (ret.kind === "void") {
|
||||
B.line(`call void @${target}(${callArgs})`);
|
||||
this.pendingBail(B, "jtr", () => {}, `ptr ${undefinedWord}`);
|
||||
B.terminate(`ret ptr ${undefinedWord}`);
|
||||
} else {
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ${this.valTy(ret)} @${target}(${callArgs})`);
|
||||
this.pendingBail(B, "jtr", () => {}, `ptr ${undefinedWord}`);
|
||||
if (ret.kind === "jsval") {
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
} else if (ret.kind === "f64") {
|
||||
const raw = B.tmp();
|
||||
const nan = B.tmp();
|
||||
const bits = B.tmp();
|
||||
const word = B.tmp();
|
||||
B.line(`${raw} = bitcast double ${r} to i64`);
|
||||
B.line(`${nan} = fcmp uno double ${r}, ${r}`);
|
||||
B.line(`${bits} = select i1 ${nan}, i64 ${JS_BITS.canonicalNan}, i64 ${raw}`);
|
||||
B.line(`${word} = inttoptr i64 ${bits} to ptr`);
|
||||
B.terminate(`ret ptr ${word}`);
|
||||
} else if (ret.kind === "bool") {
|
||||
const bits = B.tmp();
|
||||
const word = B.tmp();
|
||||
B.line(`${bits} = select i1 ${r}, i64 ${JS_BITS.true}, i64 ${JS_BITS.false}`);
|
||||
B.line(`${word} = inttoptr i64 ${bits} to ptr`);
|
||||
B.terminate(`ret ptr ${word}`);
|
||||
} else {
|
||||
// dyn, or a typed result boxed as the closure's own entry would.
|
||||
const d = ret.kind === "dyn" ? r : this.toDynExpr(B, ret, r);
|
||||
if (ret.kind !== "dyn" && isRefCounted(ret))
|
||||
B.line(`call void ${releaseSym(host, ret)}(ptr ${r})`);
|
||||
host.declare(`declare ptr @scr_js_from_dyn(ptr)`);
|
||||
const word = B.tmp();
|
||||
B.line(`${word} = call ptr @scr_js_from_dyn(ptr ${d})`);
|
||||
B.terminate(`ret ptr ${word}`);
|
||||
}
|
||||
}
|
||||
this.defs.push(
|
||||
`define internal ptr @${name}(ptr %c, ptr %args, ${host.sizeType} %argc) ${FN_ATTRS} { ; jsval entry into ${llvmCommentText(target)}`,
|
||||
B.render(),
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
this.host.journalBuilt?.();
|
||||
return name;
|
||||
}
|
||||
|
||||
/** The box builder dynFrom emits for one closure signature. */
|
||||
dynFuncBoxHelper(t: IrType & { kind: "func" }): string {
|
||||
const key = typeKey(t);
|
||||
@@ -5547,11 +5664,43 @@ export class LlDyn {
|
||||
host.declare(`declare ptr @scr_closure_retain_v(ptr)`);
|
||||
host.declare(`declare ptr @scr_dyn_new_func(ptr, ptr, i32, ptr, ptr)`);
|
||||
const sigLit = host.cstr(key);
|
||||
// A closure of this signature with a jsval body gets the function
|
||||
// value's jsval entry, however the value is boxed: the closure's
|
||||
// entry pointer names it.
|
||||
const entries =
|
||||
t.rest === true
|
||||
? []
|
||||
: (host.jsEntryClosures?.() ?? []).filter(
|
||||
(c) =>
|
||||
c.params.length === t.params.length &&
|
||||
c.params.every((p, i) => typeKey(p) === typeKey(t.params[i]!)) &&
|
||||
typeKey(c.ret) === typeKey(t.ret),
|
||||
);
|
||||
const register: string[] = [];
|
||||
if (entries.length > 0) {
|
||||
host.declare(`declare void @scr_dyn_func_set_js(ptr, ptr, ptr)`);
|
||||
register.push(
|
||||
` %fnp = getelementptr inbounds %ScrClosure, ptr %c, i64 0, i32 1`,
|
||||
` %fn = load ptr, ptr %fnp`,
|
||||
);
|
||||
entries.forEach((c, i) => {
|
||||
const jsEntry = this.jsEntryThunk(c.body, c.bodyParams, c.bodyRet);
|
||||
register.push(
|
||||
` %is${i} = icmp eq ptr %fn, @${c.closure}`,
|
||||
` br i1 %is${i}, label %set${i}, label %next${i}`,
|
||||
`set${i}:`,
|
||||
` call void @scr_dyn_func_set_js(ptr %r, ptr @${thunk}, ptr @${jsEntry})`,
|
||||
` ret ptr %r`,
|
||||
`next${i}:`,
|
||||
);
|
||||
});
|
||||
}
|
||||
this.defs.push(
|
||||
`define internal ptr @${name}(ptr %v, ptr %fname) ${FN_ATTRS} { ; box ${key} into dyn`,
|
||||
`entry:`,
|
||||
` %c = call ptr @scr_closure_retain_v(ptr %v)`,
|
||||
` %r = call ptr @scr_dyn_new_func(ptr %c, ptr @${thunk}, i32 ${t.params.length - (t.restAbi === "typed" ? 1 : 0)}, ptr ${sigLit}, ptr %fname)`,
|
||||
...register,
|
||||
` ret ptr %r`,
|
||||
`}`,
|
||||
``,
|
||||
|
||||
@@ -148,7 +148,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.integerTempsEnabled = emitter.debug === null && !fn.async && !fn.generator;
|
||||
beginFunctionLoops(emitter, fn, numericFn);
|
||||
emitter.speculativeLoopsEnabled = LOOPS.of(emitter).countedLoopsEnabled;
|
||||
emitter.loopCopiesEnabled = true;
|
||||
emitter.loopCopiesEnabled = fn.plainLoops !== true;
|
||||
emitter.speculativeFunction = fn.name;
|
||||
emitter.speculation = null;
|
||||
emitter.speculationResume = null;
|
||||
|
||||
@@ -478,6 +478,10 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
const usesEvents = runtimeFeatures.processEvents;
|
||||
const usesChildProcess = runtimeFeatures.childProcess;
|
||||
const usesFsWatch = runtimeFeatures.fsWatch;
|
||||
// fs.watchFile programs register the poller unit's exit cleanup — the
|
||||
// same one-line gating (native/executable.ts links scr_watch_file.c on
|
||||
// the same predicate).
|
||||
const usesFsWatchFile = runtimeFeatures.fsWatchFile;
|
||||
// Programs that call into Node record the libnode pack and its Node
|
||||
// version before %main (scr_node_island.c).
|
||||
const usesNodeIsland = runtimeFeatures.nodeIsland;
|
||||
@@ -541,6 +545,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
if (workerStdioBox || workerStdinBox)
|
||||
emitter.declare(`declare void @scr_worker_stdio_install(ptr, ptr, ptr)`);
|
||||
if (usesFsWatch) emitter.declare(`declare void @scr_watch_install()`);
|
||||
if (usesFsWatchFile) emitter.declare(`declare void @scr_watch_file_install()`);
|
||||
if (usesNodeIsland) emitter.declare(`declare void @scr_node_island_install(ptr, ptr)`);
|
||||
// Interned now: the C-string constants flush before main assembles.
|
||||
const nodeIslandConfig = usesNodeIsland
|
||||
@@ -1226,6 +1231,9 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
// fs.watch programs fill the loop's watch hooks the same way —
|
||||
// scr_watch.c links only when this line is emitted.
|
||||
...(usesFsWatch ? [` call void @scr_watch_install()`] : []),
|
||||
// fs.watchFile programs register the poller unit's atexit cleanup the
|
||||
// same way — scr_watch_file.c links only when this line is emitted.
|
||||
...(usesFsWatchFile ? [` call void @scr_watch_file_install()`] : []),
|
||||
...(nodeIslandConfig
|
||||
? [
|
||||
` call void @scr_node_island_install(ptr ${nodeIslandConfig[0]}, ptr ${nodeIslandConfig[1]})`,
|
||||
@@ -1377,25 +1385,147 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The out-of-line hashed probe of a number-key map (scr_map_find_f64):
|
||||
* `keyLines` define the normalized key word %kb from the parameter, and
|
||||
* `runtimeKey` is the double passed to the runtime lookup. A map with at
|
||||
* most SCR_MAP_LINEAR_LIMIT entries keeps no bucket table: the absent-key
|
||||
* filter is tested before its entries are scanned. A bucket table is
|
||||
* probed without the filter: the filter keeps two of its 64 bits per key,
|
||||
* so past a few dozen keys it rejects few absent keys, and the probe's
|
||||
* first bucket read answers those. Compact tables tag each 24-bit entry
|
||||
* index (+1) with the hash's top byte; wider tables take the runtime
|
||||
* lookup. */
|
||||
/** The `.loop` lookup helpers only read memory and always return, as
|
||||
* declared: LLVM then merges two lookups of one key (a has() followed by
|
||||
* get()) before it inlines them. */
|
||||
const INLINED_LOOKUP_ATTRS = "alwaysinline memory(read) nounwind willreturn";
|
||||
|
||||
/** The number-key entry lookups (sc_map_entry_f64 and sc_map_entry_f64_int)
|
||||
* and their hashed probes. With `loop` they are the `.loop` flavor, which
|
||||
* lookups inside a loop call (emitNumberMapEntry): each of its functions is
|
||||
* inlined, so a lookup that finds its key in a bucket table makes no call,
|
||||
* and the loop keeps no argument moves or spills around one. */
|
||||
function numberMapEntryHelpers(sz: string, loop: boolean): string[] {
|
||||
const flavor = loop ? ".loop" : "";
|
||||
const attrs = loop ? `${INLINED_LOOKUP_ATTRS} ${FN_ATTRS}` : FN_ATTRS;
|
||||
let mapTmp = 0;
|
||||
const mapIndex = exactIndexLines("%k", "%nd", sz, () => `%ix${mapTmp++}`);
|
||||
return [
|
||||
`define internal ptr @sc_map_entry_f64${flavor}(ptr %m, double %k) ${attrs} {`,
|
||||
`entry:`,
|
||||
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`,
|
||||
...(sz === "i64"
|
||||
? [` %nd32 = load i32, ptr %ndp`, ` %nd = zext i32 %nd32 to i64`]
|
||||
: [` %nd = load i32, ptr %ndp`]),
|
||||
// A non-integral key inside the table is as absent as one outside it.
|
||||
...mapIndex.lines.map((line) => ` ${line}`),
|
||||
` br i1 ${mapIndex.ok}, label %slot, label %slow`,
|
||||
`slot:`,
|
||||
sz === "i64"
|
||||
? ` %i = add i64 ${mapIndex.wide}, 0`
|
||||
: ` %i = trunc i64 ${mapIndex.wide} to ${sz}`,
|
||||
` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`,
|
||||
` %dense = load ptr, ptr %dp`,
|
||||
` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`,
|
||||
` %s = load i32, ptr %sp`,
|
||||
` %empty = icmp eq i32 %s, 0`,
|
||||
` br i1 %empty, label %miss, label %hit`,
|
||||
`hit:`,
|
||||
` %e32 = sub i32 %s, 1`,
|
||||
sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`,
|
||||
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
|
||||
` %entries = load ptr, ptr %ep`,
|
||||
` %found = getelementptr inbounds %ScrMapNumEntry, ptr %entries, ${sz} %e`,
|
||||
` ret ptr %found`,
|
||||
`miss:`,
|
||||
` ret ptr null`,
|
||||
`slow:`,
|
||||
` %hashed = call ptr @sc_map_entry_f64_hashed${flavor}(ptr %m, double %k)`,
|
||||
` ret ptr %hashed`,
|
||||
`}`,
|
||||
``,
|
||||
// The same lookup for a key the emitter knows is an integer (`%ki`,
|
||||
// the double `%k`'s exact value): the direct index tests the integer
|
||||
// itself, so the double is converted only by the hashed probe. A
|
||||
// negative key is not below the index's size as an unsigned value.
|
||||
`define internal ptr @sc_map_entry_f64_int${flavor}(ptr %m, i64 %ki, double %k) ${attrs} {`,
|
||||
`entry:`,
|
||||
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`,
|
||||
` %nd32 = load i32, ptr %ndp`,
|
||||
` %nd = zext i32 %nd32 to i64`,
|
||||
` %inside = icmp ult i64 %ki, %nd`,
|
||||
` br i1 %inside, label %slot, label %slow`,
|
||||
`slot:`,
|
||||
sz === "i64" ? ` %i = add i64 %ki, 0` : ` %i = trunc i64 %ki to ${sz}`,
|
||||
` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`,
|
||||
` %dense = load ptr, ptr %dp`,
|
||||
` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`,
|
||||
` %s = load i32, ptr %sp`,
|
||||
` %empty = icmp eq i32 %s, 0`,
|
||||
` br i1 %empty, label %miss, label %hit`,
|
||||
`hit:`,
|
||||
` %e32 = sub i32 %s, 1`,
|
||||
sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`,
|
||||
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
|
||||
` %entries = load ptr, ptr %ep`,
|
||||
` %found = getelementptr inbounds %ScrMapNumEntry, ptr %entries, ${sz} %e`,
|
||||
` ret ptr %found`,
|
||||
`miss:`,
|
||||
` ret ptr null`,
|
||||
`slow:`,
|
||||
` %hashed = call ptr @sc_map_entry_f64_hashed_int${flavor}(ptr %m, i64 %ki, double %k)`,
|
||||
` ret ptr %hashed`,
|
||||
`}`,
|
||||
``,
|
||||
// Hashing (scr_map_find_f64): the SameValueZero key word and its
|
||||
// scr_key_mix hash. sc_map_entry_f64_int's probe takes the key's double
|
||||
// with its integer view: its bits are the key word, never NaN, and +0
|
||||
// for the integer 0. The caller converts the integer: cvtsi2sd writes
|
||||
// only the low half of its register, so in a function entry, where
|
||||
// LLVM cannot clear the register first, it waits for the register's
|
||||
// last value, in a loop of lookups often a value the previous
|
||||
// iteration loaded.
|
||||
...hashedNumberProbe(
|
||||
`sc_map_entry_f64_hashed${flavor}`,
|
||||
loop,
|
||||
"double %k",
|
||||
sz,
|
||||
[
|
||||
` %nan = fcmp uno double %k, %k`,
|
||||
` %zero = fcmp oeq double %k, 0.0`,
|
||||
` %kz = select i1 %zero, double 0.0, double %k`,
|
||||
` %kzb = bitcast double %kz to i64`,
|
||||
` %kb = select i1 %nan, i64 9221120237041090560, i64 %kzb`,
|
||||
],
|
||||
"%k",
|
||||
),
|
||||
...hashedNumberProbe(
|
||||
`sc_map_entry_f64_hashed_int${flavor}`,
|
||||
loop,
|
||||
"i64 %ki, double %k",
|
||||
sz,
|
||||
[
|
||||
` %kzero = icmp eq i64 %ki, 0`,
|
||||
` %kbits = bitcast double %k to i64`,
|
||||
` %kb = select i1 %kzero, i64 0, i64 %kbits`,
|
||||
],
|
||||
"%k",
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/** The hashed probe of a number-key map (scr_map_find_f64), out of line,
|
||||
* or inlined with `loop`: `keyLines` define the normalized key word %kb
|
||||
* from the parameter, and `runtimeKey` is the double passed to the runtime
|
||||
* lookup. A map with at most SCR_MAP_LINEAR_LIMIT entries keeps no bucket
|
||||
* table, and neither does a map with the direct index, whose lookups reach
|
||||
* the probe only for a key outside the index, which is absent. In a small
|
||||
* map the absent-key filter is tested before its entries are scanned.
|
||||
* A bucket table is probed without the filter: the filter keeps two of its
|
||||
* 64 bits per key, so past a few dozen keys it rejects few absent keys, and
|
||||
* the probe's first bucket read answers those. Compact tables tag each
|
||||
* 24-bit entry index (+1) with the hash's top byte; wider tables take the
|
||||
* runtime lookup. */
|
||||
function hashedNumberProbe(
|
||||
name: string,
|
||||
loop: boolean,
|
||||
param: string,
|
||||
sz: string,
|
||||
keyLines: string[],
|
||||
runtimeKey: string,
|
||||
): string[] {
|
||||
return [
|
||||
`define internal ptr @${name}(ptr %m, ${param}) noinline ${FN_ATTRS} {`,
|
||||
`define internal ptr @${name}(ptr %m, ${param}) ${loop ? INLINED_LOOKUP_ATTRS : "noinline"} ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
...keyLines,
|
||||
` %x1s = lshr i64 %kb, 30`,
|
||||
@@ -1415,7 +1545,14 @@ function hashedNumberProbe(
|
||||
` %eps = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
|
||||
` %ents = load ptr, ptr %eps`,
|
||||
` %linear = icmp eq ${sz} %nb, 0`,
|
||||
` br i1 %linear, label %filter, label %buckets`,
|
||||
` br i1 %linear, label %unbucketed, label %buckets`,
|
||||
// A map without a bucket table either indexes its keys directly, and
|
||||
// then holds none outside the index, or scans its entries.
|
||||
`unbucketed:`,
|
||||
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`,
|
||||
` %nd = load i32, ptr %ndp`,
|
||||
` %indexed = icmp ne i32 %nd, 0`,
|
||||
` br i1 %indexed, label %miss, label %filter`,
|
||||
`filter:`,
|
||||
` %f1s = lshr i64 %h, 44`,
|
||||
` %f1a = and i64 %f1s, 63`,
|
||||
@@ -1582,117 +1719,22 @@ export function helperDefs(emitter: LlEmitter): string[] {
|
||||
}
|
||||
// Number-key Map/Set lookups probe the runtime's direct index inline:
|
||||
// while ndense is nonzero every live key is an integer below it, so an
|
||||
// in-range integral key reads its slot (entry index + 1, 0 = absent) and
|
||||
// any other key is absent. Maps without the index (ndense 0) hash the
|
||||
// key in sc_map_entry_f64_hashed, which scans the entries of a small map
|
||||
// or probes a compact bucket table as scr_map_find_f64 does, and calls
|
||||
// the runtime lookup for a wide table. It stays out of line so that the
|
||||
// direct-index path stays small at every call site. Both only read
|
||||
// memory, so LLVM merges lookups of one key when nothing is stored in
|
||||
// between (a has() followed by get(), for example).
|
||||
if (emitter.decls.has(NUMBER_MAP_ENTRY_DECL)) {
|
||||
const sz = emitter.sizeType;
|
||||
let mapTmp = 0;
|
||||
const mapIndex = exactIndexLines("%k", "%nd", sz, () => `%ix${mapTmp++}`);
|
||||
// in-range integral key reads its slot (entry index + 1, 0 = absent).
|
||||
// Any other key goes to sc_map_entry_f64_hashed, which probes a compact
|
||||
// bucket table as scr_map_find_f64 does, calls the runtime lookup for a
|
||||
// wide table, and in a map without a bucket table answers absent when the
|
||||
// map has the index (a map with buckets keeps no index, scr_map.h) and
|
||||
// scans the entries otherwise. A lookup in a hashed map so tests the
|
||||
// index once. The probe stays out of line so that the direct-index path
|
||||
// stays small at every call site, except in the `.loop` flavor
|
||||
// (numberMapEntryHelpers). Both only read memory, so LLVM merges lookups
|
||||
// of one key when nothing is stored in between (a has() followed by
|
||||
// get(), for example).
|
||||
if (emitter.decls.has(NUMBER_MAP_ENTRY_DECL))
|
||||
defs.push(
|
||||
`define internal ptr @sc_map_entry_f64(ptr %m, double %k) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`,
|
||||
...(sz === "i64"
|
||||
? [` %nd32 = load i32, ptr %ndp`, ` %nd = zext i32 %nd32 to i64`]
|
||||
: [` %nd = load i32, ptr %ndp`]),
|
||||
// A non-integral key inside the table is as absent as one outside it.
|
||||
...mapIndex.lines.map((line) => ` ${line}`),
|
||||
` br i1 ${mapIndex.ok}, label %slot, label %outside`,
|
||||
`slot:`,
|
||||
sz === "i64"
|
||||
? ` %i = add i64 ${mapIndex.wide}, 0`
|
||||
: ` %i = trunc i64 ${mapIndex.wide} to ${sz}`,
|
||||
` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`,
|
||||
` %dense = load ptr, ptr %dp`,
|
||||
` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`,
|
||||
` %s = load i32, ptr %sp`,
|
||||
` %empty = icmp eq i32 %s, 0`,
|
||||
` br i1 %empty, label %miss, label %hit`,
|
||||
`hit:`,
|
||||
` %e32 = sub i32 %s, 1`,
|
||||
sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`,
|
||||
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
|
||||
` %entries = load ptr, ptr %ep`,
|
||||
` %found = getelementptr inbounds %ScrMapNumEntry, ptr %entries, ${sz} %e`,
|
||||
` ret ptr %found`,
|
||||
`miss:`,
|
||||
` ret ptr null`,
|
||||
`outside:`,
|
||||
` %dense.any = icmp ne ${sz} %nd, 0`,
|
||||
` br i1 %dense.any, label %miss, label %slow`,
|
||||
`slow:`,
|
||||
` %hashed = call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)`,
|
||||
` ret ptr %hashed`,
|
||||
`}`,
|
||||
``,
|
||||
// The same lookup for a key the emitter knows is an integer (`%ki`,
|
||||
// the double `%k`'s exact value): the direct index tests the integer
|
||||
// itself, so the double is converted only by the hashed probe. A
|
||||
// negative key is not below the index's size as an unsigned value.
|
||||
`define internal ptr @sc_map_entry_f64_int(ptr %m, i64 %ki, double %k) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` %ndp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 16`,
|
||||
` %nd32 = load i32, ptr %ndp`,
|
||||
` %nd = zext i32 %nd32 to i64`,
|
||||
` %inside = icmp ult i64 %ki, %nd`,
|
||||
` br i1 %inside, label %slot, label %outside`,
|
||||
`slot:`,
|
||||
sz === "i64" ? ` %i = add i64 %ki, 0` : ` %i = trunc i64 %ki to ${sz}`,
|
||||
` %dp = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 15`,
|
||||
` %dense = load ptr, ptr %dp`,
|
||||
` %sp = getelementptr inbounds i32, ptr %dense, ${sz} %i`,
|
||||
` %s = load i32, ptr %sp`,
|
||||
` %empty = icmp eq i32 %s, 0`,
|
||||
` br i1 %empty, label %miss, label %hit`,
|
||||
`hit:`,
|
||||
` %e32 = sub i32 %s, 1`,
|
||||
sz === "i32" ? ` %e = add i32 %e32, 0` : ` %e = zext i32 %e32 to ${sz}`,
|
||||
` %ep = getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 11`,
|
||||
` %entries = load ptr, ptr %ep`,
|
||||
` %found = getelementptr inbounds %ScrMapNumEntry, ptr %entries, ${sz} %e`,
|
||||
` ret ptr %found`,
|
||||
`miss:`,
|
||||
` ret ptr null`,
|
||||
`outside:`,
|
||||
` %dense.any = icmp ne i64 %nd, 0`,
|
||||
` br i1 %dense.any, label %miss, label %slow`,
|
||||
`slow:`,
|
||||
` %hashed = call ptr @sc_map_entry_f64_hashed_int(ptr %m, i64 %ki)`,
|
||||
` ret ptr %hashed`,
|
||||
`}`,
|
||||
``,
|
||||
// Hashing (scr_map_find_f64): the SameValueZero key word and its
|
||||
// scr_key_mix hash. sc_map_entry_f64_int's probe converts the integer
|
||||
// view to the key word, which is never NaN or -0, and so skips the
|
||||
// normalization.
|
||||
...hashedNumberProbe(
|
||||
"sc_map_entry_f64_hashed",
|
||||
"double %k",
|
||||
sz,
|
||||
[
|
||||
` %nan = fcmp uno double %k, %k`,
|
||||
` %zero = fcmp oeq double %k, 0.0`,
|
||||
` %kz = select i1 %zero, double 0.0, double %k`,
|
||||
` %kzb = bitcast double %kz to i64`,
|
||||
` %kb = select i1 %nan, i64 9221120237041090560, i64 %kzb`,
|
||||
],
|
||||
"%k",
|
||||
),
|
||||
...hashedNumberProbe(
|
||||
"sc_map_entry_f64_hashed_int",
|
||||
"i64 %ki",
|
||||
sz,
|
||||
[` %kf = sitofp i64 %ki to double`, ` %kb = bitcast double %kf to i64`],
|
||||
"%kf",
|
||||
),
|
||||
...numberMapEntryHelpers(emitter.sizeType, false),
|
||||
...numberMapEntryHelpers(emitter.sizeType, true),
|
||||
);
|
||||
}
|
||||
// Map/Set iteration steps read the entries array inline: an index is
|
||||
// live when it is below the entry count and its entry's cached hash is
|
||||
// nonzero (a tombstone's is zero), as scr_map_iter_live decides, or for
|
||||
|
||||
@@ -1147,6 +1147,14 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
(countedLoop?.guarded || pairedLoop?.guarded) &&
|
||||
!LOOPS.of(emitter).provenCountedGuards.has(s)
|
||||
) {
|
||||
// A function that emits each loop once takes only the copy without
|
||||
// the integer form, which needs no safety test.
|
||||
if (LOOPS.of(emitter).plainLoops) {
|
||||
LOOPS.of(emitter).countedLoopsEnabled = false;
|
||||
emitSlowCopy(emitter, s);
|
||||
LOOPS.of(emitter).countedLoopsEnabled = true;
|
||||
break;
|
||||
}
|
||||
const safe = countedLoop
|
||||
? emitCountedLoopSafety(emitter, countedLoop)
|
||||
: emitPairedLoopSafety(emitter, pairedLoop!);
|
||||
@@ -1278,26 +1286,26 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
}
|
||||
};
|
||||
// A counted loop over typed arrays, or a loop to a typed array's
|
||||
// length, polls between blocks of WORKER_POLL_BLOCK iterations instead
|
||||
// length, polls between blocks of workerPollBlock iterations instead
|
||||
// of in its body: the inner loop over a block then has no exit
|
||||
// besides its own bound, which LLVM can vectorize. A typed
|
||||
// array's length is fixed, so each block can take it as its limit when
|
||||
// the loop does not assign the array's binding.
|
||||
//
|
||||
// Inside another loop of this function, which polls at least once
|
||||
// every WORKER_POLL_BLOCK of its own iterations, a loop needs less:
|
||||
// every workerPollBlock of its own iterations, a loop needs less:
|
||||
// - a loop whose counters' ranges bound its runs, and those of the
|
||||
// loops in its body, to WORKER_POLL_BLOCK^2 needs no poll;
|
||||
// loops in its body, to workerPollBlock^2 needs no poll;
|
||||
// - an innermost counted loop is strip-mined, typed arrays or not,
|
||||
// and does not poll after its last block. A run of at most
|
||||
// WORKER_POLL_BLOCK iterations then never polls, and the countdown
|
||||
// workerPollBlock iterations then never polls, and the countdown
|
||||
// leaves its body. A run of at most WORKER_UNPOLLED_RUN iterations
|
||||
// is one block, polled once as it leaves.
|
||||
const pollBlock = workerPollBlock(emitter);
|
||||
const enclosed =
|
||||
emitter.mod.workers === true &&
|
||||
emitter.jumpTargets.some((target) => target.kind === "loop");
|
||||
const bounded =
|
||||
enclosed && boundedLoopRuns(emitter, s) <= WORKER_POLL_BLOCK * WORKER_POLL_BLOCK;
|
||||
const bounded = enclosed && boundedLoopRuns(emitter, s) <= pollBlock * pollBlock;
|
||||
const innermost = enclosed && !containsLoop(s.body);
|
||||
const stripMined =
|
||||
!bounded &&
|
||||
@@ -1305,7 +1313,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
integerSlot !== null &&
|
||||
((countedLoop !== null &&
|
||||
countedLimit !== null &&
|
||||
Math.abs(countedLoop.step) * WORKER_POLL_BLOCK <= 2 ** 52) ||
|
||||
Math.abs(countedLoop.step) * pollBlock <= 2 ** 52) ||
|
||||
(integerLoop !== null && integerArrayLoop === null && integerLoop.stableReceiver)) &&
|
||||
emitter.workerLoopPolls(s) &&
|
||||
(loopAccessesTypedArrays(s) || (countedLoop !== null && innermost));
|
||||
@@ -1314,10 +1322,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
// step on each side, has a run whose length its counters fix on entry.
|
||||
// With a small body, a run of at most WORKER_UNPOLLED_RUN steps takes a
|
||||
// plain copy of the loop without a poll, which polls once as it leaves
|
||||
// when it ran more than WORKER_POLL_BLOCK steps, like a strip-mined run;
|
||||
// when it ran more than workerPollBlock steps, like a strip-mined run;
|
||||
// longer runs take the loop as it is emitted otherwise.
|
||||
const shortRuns: { stride: number; left: () => string } | null =
|
||||
!innermost ||
|
||||
LOOPS.of(emitter).plainLoops ||
|
||||
bounded ||
|
||||
capturedLets.length > 0 ||
|
||||
!emitter.workerLoopPolls(s) ||
|
||||
@@ -1491,7 +1500,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
|
||||
emitPlainLoop(shortEnd, null);
|
||||
// A run longer than one block polls once as it leaves.
|
||||
B.startBlock(shortEnd);
|
||||
B.line(`${pollsAfter} = icmp sgt i64 ${left}, ${shortRuns.stride * WORKER_POLL_BLOCK}`);
|
||||
B.line(`${pollsAfter} = icmp sgt i64 ${left}, ${shortRuns.stride * pollBlock}`);
|
||||
B.condBr(pollsAfter, shortPoll, exit);
|
||||
B.startBlock(shortPoll);
|
||||
emitter.emitWorkerPoll();
|
||||
@@ -2811,8 +2820,15 @@ function emitInt32KindLoop(
|
||||
|
||||
/** At the top of a loop copy's header: lets a speculative loop find its
|
||||
* ordinary copy's header, and keeps its third copy out of the loop. */
|
||||
/** Iterations between cancellation polls of a strip-mined counted loop. */
|
||||
const WORKER_POLL_BLOCK = 64;
|
||||
/** Iterations between cancellation polls of a strip-mined counted loop. A
|
||||
* block holds at least one step of the vectorized loop over bytes, so that
|
||||
* a full block runs vector code: 64 iterations at the baseline (16-byte
|
||||
* vectors, interleaved 4 times), and 128 when the program's code is compiled
|
||||
* for x86-64-v3 or above, whose vectors take 32 bytes. */
|
||||
function workerPollBlock(emitter: LlEmitter): number {
|
||||
const cpu = emitter.targetCpu;
|
||||
return emitter.cpuDispatch || cpu === "x86-64-v3" || cpu === "x86-64-v4" ? 128 : 64;
|
||||
}
|
||||
|
||||
/** The longest run of a strip-mined loop inside another loop that polls
|
||||
* only as it leaves. */
|
||||
@@ -2855,7 +2871,8 @@ function boundedLoopRuns(emitter: LlEmitter, loop: IrStmt): number {
|
||||
if (stepped && range && exactSteps(range, stepped.step))
|
||||
trips = Math.floor((range.max - range.min) / Math.abs(stepped.step)) + 1;
|
||||
}
|
||||
if (!(trips <= WORKER_POLL_BLOCK * WORKER_POLL_BLOCK)) return Infinity;
|
||||
const pollBlock = workerPollBlock(emitter);
|
||||
if (!(trips <= pollBlock * pollBlock)) return Infinity;
|
||||
let nested = 0;
|
||||
const visit = (s: IrStmt): boolean => {
|
||||
if (s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf") {
|
||||
@@ -2909,17 +2926,17 @@ interface StripMinedCounter {
|
||||
limit: () => string;
|
||||
}
|
||||
|
||||
/** A counted `for` loop of a worker program as blocks of WORKER_POLL_BLOCK
|
||||
/** A counted `for` loop of a worker program as blocks of workerPollBlock
|
||||
* iterations, with the cancellation poll between blocks: the outer header
|
||||
* tests the counter against the loop's limit, each block runs the body up
|
||||
* to the nearer of the limit and WORKER_POLL_BLOCK steps on, and the poll
|
||||
* to the nearer of the limit and workerPollBlock steps on, and the poll
|
||||
* follows the block. It polls once per block, and the inner loop's only
|
||||
* exit besides `break` and `return` is its bound.
|
||||
*
|
||||
* With `runSlot` (for a loop that another loop of the function encloses), a
|
||||
* block that ends at the limit leaves the loop without polling, and a run
|
||||
* of at most WORKER_UNPOLLED_RUN iterations is one block, polled once as
|
||||
* it leaves when it took more than WORKER_POLL_BLOCK iterations. `runSlot`
|
||||
* it leaves when it took more than workerPollBlock iterations. `runSlot`
|
||||
* holds whether the current block is such a run. */
|
||||
function emitStripMinedCountedLoop(
|
||||
emitter: LlEmitter,
|
||||
@@ -2932,6 +2949,7 @@ function emitStripMinedCountedLoop(
|
||||
const B = emitter.B;
|
||||
const { slot, type, signed, step } = counter;
|
||||
const ascending = step > 0;
|
||||
const pollBlock = workerPollBlock(emitter);
|
||||
const compare = !signed ? "ult" : ascending ? "slt" : "sgt";
|
||||
const saturating = `llvm.${signed ? "sadd" : "uadd"}.sat.${type}`;
|
||||
const nearer = `llvm.${!signed ? "umin" : ascending ? "smin" : "smax"}.${type}`;
|
||||
@@ -2956,9 +2974,7 @@ function emitStripMinedCountedLoop(
|
||||
B.startBlock(block);
|
||||
const stepped = B.tmp();
|
||||
const bound = B.tmp();
|
||||
B.line(
|
||||
`${stepped} = call ${type} @${saturating}(${type} ${start}, ${type} ${step * WORKER_POLL_BLOCK})`,
|
||||
);
|
||||
B.line(`${stepped} = call ${type} @${saturating}(${type} ${start}, ${type} ${step * pollBlock})`);
|
||||
B.line(`${bound} = call ${type} @${nearer}(${type} ${stepped}, ${type} ${limit})`);
|
||||
if (runSlot === null) B.line(`store ${type} ${bound}, ptr ${boundSlot}`);
|
||||
else {
|
||||
@@ -2975,7 +2991,7 @@ function emitStripMinedCountedLoop(
|
||||
: `${left} = sub ${wrap} ${type} ${start}, ${limit}`,
|
||||
);
|
||||
B.line(`${whole} = icmp ule ${type} ${left}, ${stride * WORKER_UNPOLLED_RUN}`);
|
||||
B.line(`${long} = icmp ugt ${type} ${left}, ${stride * WORKER_POLL_BLOCK}`);
|
||||
B.line(`${long} = icmp ugt ${type} ${left}, ${stride * pollBlock}`);
|
||||
B.line(`${polled} = and i1 ${whole}, ${long}`);
|
||||
B.line(`${end} = select i1 ${whole}, ${type} ${limit}, ${type} ${bound}`);
|
||||
B.line(`store ${type} ${end}, ptr ${boundSlot}`);
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { CompilationTiming } from "../../timing.js";
|
||||
import { relieveHeapPressure } from "../../heap-pressure.js";
|
||||
import { analyzeFrameEscapes, type FrameEscapes } from "./frame-escapes.js";
|
||||
import { wasmBoundsChecks } from "./wasm-modules.js";
|
||||
import { jsTruthy } from "./jsval.js";
|
||||
import { jsImmediates, jsTruthy } from "./jsval.js";
|
||||
import { JsIcSites } from "../../ir/js-ic.js";
|
||||
import { type ArraySearchKind } from "./array-search.js";
|
||||
import { emitStringInputs } from "./string-lifetimes.js";
|
||||
@@ -286,6 +286,8 @@ import {
|
||||
cryptoIntThunkFor,
|
||||
type BytesThunkValue,
|
||||
fsRenameThunkFor,
|
||||
watchNameThunkFor,
|
||||
watchErrorThunkFor,
|
||||
streamCbThunkFor,
|
||||
zlibBytesThunkFor,
|
||||
} from "./expr-stream-callbacks.js";
|
||||
@@ -374,7 +376,7 @@ import {
|
||||
type ArenaPlan,
|
||||
} from "./arenas.js";
|
||||
import { children, type Node } from "./region-ir.js";
|
||||
import { LlDyn, recordDynKeepsIdentity, type DynHost } from "./dyn.js";
|
||||
import { LlDyn, recordDynKeepsIdentity, type DynHost, type JsEntryClosure } from "./dyn.js";
|
||||
import { VirtualBorrows } from "./virtual-borrows.js";
|
||||
import { planRecordViews, recordViewFieldPtr } from "./record-views.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
@@ -1485,6 +1487,10 @@ export class LlEmitter {
|
||||
private readonly rcHelpersNoinline: boolean;
|
||||
|
||||
readonly fnByName = new Map<string, IrFunction>();
|
||||
/** Whether some closure has a jsval body (IrFunction.jsEntry): the
|
||||
* dynamic tier's dynCall and dynInvoke sites then call a function value's
|
||||
* jsval entry when it has one. */
|
||||
jsEntries = false;
|
||||
/** Empty literals that construct with inline slots (appended-array-literals.ts). */
|
||||
appendedArrayLiterals: ReadonlySet<IrExpr> = new Set();
|
||||
/** Emitted LLVM text of every IR function body, by function name: the
|
||||
@@ -2180,7 +2186,10 @@ export class LlEmitter {
|
||||
this.ffiCallbackAdapters = allocateFfiCallbackAdapters(mod.ffiImports ?? []);
|
||||
this.ffiHasRetainedCallback = hasRetainedFfiCallback(mod.ffiImports ?? []);
|
||||
this.ffiHasForeignCallback = hasForeignFfiCallback(mod.ffiImports ?? []);
|
||||
for (const fn of mod.functions) this.fnByName.set(fn.name, fn);
|
||||
for (const fn of mod.functions) {
|
||||
this.fnByName.set(fn.name, fn);
|
||||
if (fn.jsEntry !== undefined) this.jsEntries = true;
|
||||
}
|
||||
this.appendedArrayLiterals = findAppendedArrayLiterals(mod, this.nodeIndex);
|
||||
for (const entry of mod.ffiImports ?? []) {
|
||||
this.ffiByName.set(entry.name, entry);
|
||||
@@ -2396,6 +2405,7 @@ export class LlEmitter {
|
||||
? undefined
|
||||
: (table, key, value) => this.journal?.entry(table, key, value),
|
||||
compactArrayElem: (elem) => this.compactArrayElem(elem),
|
||||
jsEntryClosures: () => this.jsEntryClosures(),
|
||||
compactRegion: () => this.compactRegion,
|
||||
sizeType: this.sizeType,
|
||||
cycleBufOffset: this.cycleBufOffset,
|
||||
@@ -5064,6 +5074,8 @@ export class LlEmitter {
|
||||
idx,
|
||||
(decl) => this.declare(decl),
|
||||
this.rcMode === undefined,
|
||||
(text) => this.cstr(text),
|
||||
this.shapeHost.recordViews?.partialTargets.has(shapeId) === true,
|
||||
);
|
||||
} else {
|
||||
p = this.B.tmp();
|
||||
@@ -5568,6 +5580,30 @@ export class LlEmitter {
|
||||
* the fiber eagerly to its first suspension and returns the promise);
|
||||
* generator bodies via theirs (which only ALLOCATES the suspended
|
||||
* fiber and returns the generator object). */
|
||||
private jsEntryClosureList: readonly JsEntryClosure[] | null = null;
|
||||
|
||||
/** The closures with a jsval body (IrFunction.jsEntry), on targets with
|
||||
* jsval words. */
|
||||
jsEntryClosures(): readonly JsEntryClosure[] {
|
||||
if (this.jsEntryClosureList !== null) return this.jsEntryClosureList;
|
||||
const list: JsEntryClosure[] = [];
|
||||
if (this.jsEntries && jsImmediates(this))
|
||||
for (const fn of this.fnByName.values()) {
|
||||
const body = fn.jsEntry === undefined ? undefined : this.fnByName.get(fn.jsEntry);
|
||||
if (body === undefined) continue;
|
||||
list.push({
|
||||
closure: this.callTarget(fn.name),
|
||||
params: fn.params.map((p) => p.type),
|
||||
ret: fn.returnType,
|
||||
body: this.callTarget(body.name),
|
||||
bodyParams: body.params.map((p) => p.type),
|
||||
bodyRet: body.returnType,
|
||||
});
|
||||
}
|
||||
this.jsEntryClosureList = list;
|
||||
return list;
|
||||
}
|
||||
|
||||
callTarget(fnName: string): string {
|
||||
const fn = this.fnByName.get(fnName);
|
||||
if (fn?.generator !== undefined) return mangleGenSpawn(fnName);
|
||||
@@ -8161,6 +8197,14 @@ export class LlEmitter {
|
||||
return fsRenameThunkFor(this, cbT);
|
||||
}
|
||||
|
||||
watchNameThunkFor(cbT: IrType & { kind: "func" }): string {
|
||||
return watchNameThunkFor(this, cbT);
|
||||
}
|
||||
|
||||
watchErrorThunkFor(cbT: IrType & { kind: "func" }): string {
|
||||
return watchErrorThunkFor(this, cbT);
|
||||
}
|
||||
|
||||
cryptoBytesThunkFor(
|
||||
cbT: IrType & { kind: "func" },
|
||||
arrayBuffer = false,
|
||||
@@ -8553,6 +8597,7 @@ for (const op of [
|
||||
"dyn.promiseDynAdapterHelper",
|
||||
"dyn.dynFuncThunkHelper",
|
||||
"dyn.dynFuncBoxHelper",
|
||||
"dyn.jsEntryThunk",
|
||||
"dyn.dynFuncAdapterHelper",
|
||||
"dyn.dynToStrHelper",
|
||||
"dyn.dynIterNHelper",
|
||||
|
||||
@@ -1003,6 +1003,16 @@ export function emitCallExpr(
|
||||
} else if (needsReceiver) B.line("call void @scr_dyn_this_pop()");
|
||||
return result;
|
||||
};
|
||||
// The direct entry of a known target brackets the call when the target
|
||||
// may observe the receiver. A signature adapter that only forwards the
|
||||
// receiver to its original carries the original's RECEIVER_FREE bit
|
||||
// (set where the adapter closure is built), so its entry tests the bit,
|
||||
// like the entry through the closure's own pointer.
|
||||
const invokeKnown = (target: string): string => {
|
||||
const readers = host.receiverReaders;
|
||||
if (readers.ignores(known!)) return invoke(target, false);
|
||||
return invoke(target, true, args, readers.forwardsOnly(known!));
|
||||
};
|
||||
const lending =
|
||||
lent && !dynamic
|
||||
? known!.captures !== undefined
|
||||
@@ -1025,7 +1035,7 @@ export function emitCallExpr(
|
||||
(e.callee.kind === "closure" ||
|
||||
(e.callee.kind === "varRef" && host.knownCallbacks.has(e.callee.localId)))
|
||||
) {
|
||||
t = invoke(lending ?? direct, !host.receiverReaders.ignores(known!));
|
||||
t = invokeKnown(lending ?? direct);
|
||||
if (quiet) pending = false;
|
||||
} else {
|
||||
const fnp = B.tmp(),
|
||||
@@ -1072,7 +1082,7 @@ export function emitCallExpr(
|
||||
B.line(`${matches} = icmp eq ptr ${fn}, ${direct}`);
|
||||
B.condBr(matches, fast, fallback);
|
||||
B.startBlock(fast);
|
||||
const specialized = invoke(lending ?? direct, !host.receiverReaders.ignores(known!));
|
||||
const specialized = invokeKnown(lending ?? direct);
|
||||
// `fast` may have split for exception checks; join from the end.
|
||||
const fastEnd = B.currentLabel();
|
||||
B.br(done);
|
||||
|
||||
@@ -27,11 +27,13 @@ import {
|
||||
} from "../../ir/ir.js";
|
||||
import { BYTES_ELEM_NUM, closureIdentityEqual } from "./common.js";
|
||||
import { DYN_KIND } from "./dyn.js";
|
||||
import { elemAccess, vAdapters } from "./shapes.js";
|
||||
import { elemAccess, releaseSym, vAdapters } from "./shapes.js";
|
||||
import { JS_BITS } from "./jsval-encoding.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
import { emitUnionWiden } from "./expr-records.js";
|
||||
import { emitJsIcLiteralMember, emitJsIcMethod, jsIcMethods } from "./js-ic.js";
|
||||
import { jsImmediates } from "./jsval.js";
|
||||
import { jsIcLiteralKey } from "../../ir/js-ic.js";
|
||||
import { DYN_BUFFER_METHODS } from "../../ir/runtime-features.js";
|
||||
|
||||
@@ -189,6 +191,30 @@ export function emitDynamicExpr(
|
||||
return out;
|
||||
}
|
||||
const args = e.args.map((a) => emitBorrowedInput(host, a));
|
||||
host.declare(`declare ptr @scr_dyn_call(ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
const fast = jsEntrySites(host);
|
||||
if (fast || args.some((a) => a.type.kind === "jsval")) {
|
||||
const name = calleeName();
|
||||
const t = emitJsvalArgumentsCall(
|
||||
host,
|
||||
callee.name,
|
||||
receiver?.name ?? "null",
|
||||
args,
|
||||
fast,
|
||||
(argsPtr) => {
|
||||
const r = B.tmp();
|
||||
B.line(`call void @scr_dyn_this_push_dyn(ptr ${receiver?.name ?? "null"})`);
|
||||
B.line(
|
||||
`${r} = call ptr @scr_dyn_call(ptr ${callee.name}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`,
|
||||
);
|
||||
B.line("call void @scr_dyn_this_pop()");
|
||||
return r;
|
||||
},
|
||||
);
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
}
|
||||
let argsPtr = "null";
|
||||
if (args.length > 0) {
|
||||
const arr = B.slot();
|
||||
@@ -202,7 +228,6 @@ export function emitDynamicExpr(
|
||||
});
|
||||
argsPtr = arr;
|
||||
}
|
||||
host.declare(`declare ptr @scr_dyn_call(ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
const t = B.tmp();
|
||||
const name = calleeName();
|
||||
B.line(`call void @scr_dyn_this_push_dyn(ptr ${receiver?.name ?? "null"})`);
|
||||
@@ -246,6 +271,39 @@ export function emitDynamicExpr(
|
||||
);
|
||||
}
|
||||
}
|
||||
// Buffer.prototype method names take their scr_bytes_dyn.c entry, which
|
||||
// runs them on Buffer receivers and dispatches any other receiver.
|
||||
// Each call passes its method name's id slot, so only the first call
|
||||
// looks the name up.
|
||||
const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared_id";
|
||||
const discardedPush =
|
||||
discarded && e.method === "push" && invoke === "scr_dyn_invoke_prepared_id";
|
||||
const fast = !discardedPush && invoke === "scr_dyn_invoke_prepared_id" && jsEntrySites(host);
|
||||
if (fast || args.some((a) => a.type.kind === "jsval")) {
|
||||
const name = discardedPush ? host.cstr(e.calleeName) : dynInvokeCalleeName(host, e);
|
||||
if (discardedPush)
|
||||
host.declare(
|
||||
`declare void @scr_dyn_invoke_discard_id(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`,
|
||||
);
|
||||
else host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
const t = emitJsvalArgumentsCall(host, prepared, recv.name, args, fast, (argsPtr) => {
|
||||
const site = `ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${host.methodIdSlot(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name}`;
|
||||
if (discardedPush) {
|
||||
B.line(`call void @scr_dyn_invoke_discard_id(${site})`);
|
||||
return "null";
|
||||
}
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = call ptr @${invoke}(${site})`);
|
||||
return r;
|
||||
});
|
||||
if (discardedPush) {
|
||||
host.emitPendingCheck();
|
||||
return { name: "", type: VOID };
|
||||
}
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
}
|
||||
let argsPtr = "null";
|
||||
if (args.length > 0) {
|
||||
const arr = B.slot();
|
||||
@@ -259,11 +317,6 @@ export function emitDynamicExpr(
|
||||
});
|
||||
argsPtr = arr;
|
||||
}
|
||||
// Buffer.prototype method names take their scr_bytes_dyn.c entry, which
|
||||
// runs them on Buffer receivers and dispatches any other receiver.
|
||||
// Each call passes its method name's id slot, so only the first call
|
||||
// looks the name up.
|
||||
const invoke = DYN_BUFFER_METHODS.get(e.method) ?? "scr_dyn_invoke_prepared_id";
|
||||
// A statement-position push drops the new length, so its entry
|
||||
// need not box it.
|
||||
if (discarded && e.method === "push" && invoke === "scr_dyn_invoke_prepared_id") {
|
||||
@@ -277,12 +330,7 @@ export function emitDynamicExpr(
|
||||
return { name: "", type: VOID };
|
||||
}
|
||||
host.declare(`declare ptr @${invoke}(ptr, ptr, ptr, ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
let name = host.cstr(e.calleeName);
|
||||
if (e.calleeNameValue !== undefined) {
|
||||
const value = emitBorrowedInput(host, e.calleeNameValue);
|
||||
name = B.tmp();
|
||||
B.line(`${name} = getelementptr inbounds %ScrStr, ptr ${value.name}, i32 1`);
|
||||
}
|
||||
const name = dynInvokeCalleeName(host, e);
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call ptr @${invoke}(ptr ${recv.name}, ptr ${prepared}, ptr ${host.cstr(e.method)}, ptr ${host.methodIdSlot(e.method)}, ptr ${argsPtr}, ${host.sizeType} ${args.length}, ptr ${name})`,
|
||||
@@ -547,6 +595,19 @@ export function emitDynamicExpr(
|
||||
B.br(join);
|
||||
return;
|
||||
}
|
||||
// A declared-only arm without the literal key: the object has no
|
||||
// such own property, so the read answers undefined.
|
||||
if (literal !== null && !shape.indexValue) {
|
||||
const tag = resultDef?.arms.findIndex((a) => a.kind === "undefinedT") ?? -1;
|
||||
if (tag < 0 || e.type.kind !== "union") {
|
||||
throw new InternalCompilerError(
|
||||
"llvm emitter bug: unionKeyGet missing key without an undefined result arm",
|
||||
);
|
||||
}
|
||||
B.line(`store ptr ${host.unitInstanceRef(e.type.unionId, tag)}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
return;
|
||||
}
|
||||
// Index-signature arm (or declared-only shape under a runtime
|
||||
// key): the shared keyed-read chain — a literal key naming no
|
||||
// declared field touches only the overflow map.
|
||||
@@ -1389,3 +1450,135 @@ function messageListenerArg(e: ExprOf<"dynInvoke">): (IrType & { kind: "func" })
|
||||
if (listener?.kind !== "dynFrom" || listener.value.type.kind !== "func") return null;
|
||||
return listener.value.type;
|
||||
}
|
||||
|
||||
/** Whether a dynCall or dynInvoke site of the current function tests its
|
||||
* callee for a jsval entry (emitJsvalArgumentsCall): dynamic-tier code of
|
||||
* a module with closure jsval bodies, on targets with jsval words. */
|
||||
function jsEntrySites(host: LlvmEmitterContext): boolean {
|
||||
return (
|
||||
host.jsEntries &&
|
||||
jsImmediates(host) &&
|
||||
host.fnByName.get(host.currentFunctionName)?.tier === "dynamic"
|
||||
);
|
||||
}
|
||||
|
||||
/** A call of the function value `callee` (null for a site without one)
|
||||
* with `args`, which may include jsval words (ir/jsval.ts). With `fast`,
|
||||
* a callee boxed from a closure with a jsval body (ScrDyn.v.fn.js, +72)
|
||||
* takes the arguments as words, a dyn argument under the cell tag, with
|
||||
* `receiver` bound as `this` around the call. Every other call goes
|
||||
* through `slow`, which takes an array of dyn arguments: the words boxed
|
||||
* for it and released after it. Returns the owned dyn result (null on
|
||||
* the slow path with an exception pending). */
|
||||
function emitJsvalArgumentsCall(
|
||||
host: LlvmEmitterContext,
|
||||
callee: string | null,
|
||||
receiver: string,
|
||||
args: readonly LlValue[],
|
||||
fast: boolean,
|
||||
slow: (argsPtr: string) => string,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const n = args.length;
|
||||
const result = B.slot();
|
||||
B.entryAllocas.push(`${result} = alloca ptr`);
|
||||
const lSlow = B.newLabel("jse.slow");
|
||||
const lJoin = B.newLabel("jse.join");
|
||||
if (fast && callee !== null) {
|
||||
let words = "null";
|
||||
if (n > 0) {
|
||||
words = B.slot();
|
||||
B.entryAllocas.push(`${words} = alloca [${n} x ptr]`);
|
||||
args.forEach((a, i) => {
|
||||
let word = a.name;
|
||||
if (a.type.kind !== "jsval") {
|
||||
const bits = B.tmp();
|
||||
const tagged = B.tmp();
|
||||
word = B.tmp();
|
||||
B.line(`${bits} = ptrtoint ptr ${a.name} to i64`);
|
||||
B.line(`${tagged} = or i64 ${bits}, ${JS_BITS.cellTag}`);
|
||||
B.line(`${word} = inttoptr i64 ${tagged} to ptr`);
|
||||
}
|
||||
const p = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds [${n} x ptr], ptr ${words}, i64 0, i64 ${i}`);
|
||||
B.line(`store ptr ${word}, ptr ${p}`);
|
||||
});
|
||||
}
|
||||
const lKind = B.newLabel("jse.kind");
|
||||
const lFn = B.newLabel("jse.fn");
|
||||
const lEntry = B.newLabel("jse.entry");
|
||||
const present = B.tmp();
|
||||
B.line(`${present} = icmp ne ptr ${callee}, null`);
|
||||
B.condBr(present, lKind, lSlow);
|
||||
B.startBlock(lKind);
|
||||
const kindPtr = B.tmp();
|
||||
const kind = B.tmp();
|
||||
const isFn = B.tmp();
|
||||
B.line(`${kindPtr} = getelementptr inbounds i8, ptr ${callee}, i64 8`);
|
||||
B.line(`${kind} = load i32, ptr ${kindPtr}`);
|
||||
B.line(`${isFn} = icmp eq i32 ${kind}, ${DYN_KIND.FUNC}`);
|
||||
B.condBr(isFn, lFn, lSlow);
|
||||
B.startBlock(lFn);
|
||||
const entryPtr = B.tmp();
|
||||
const entry = B.tmp();
|
||||
const hasEntry = B.tmp();
|
||||
B.line(`${entryPtr} = getelementptr inbounds i8, ptr ${callee}, i64 72 ; ->v.fn.js`);
|
||||
B.line(`${entry} = load ptr, ptr ${entryPtr}`);
|
||||
B.line(`${hasEntry} = icmp ne ptr ${entry}, null`);
|
||||
B.condBr(hasEntry, lEntry, lSlow);
|
||||
B.startBlock(lEntry);
|
||||
const cloPtr = B.tmp();
|
||||
const clo = B.tmp();
|
||||
const word = B.tmp();
|
||||
const value = B.tmp();
|
||||
host.declare("declare void @scr_dyn_this_push_dyn(ptr)");
|
||||
host.declare("declare void @scr_dyn_this_pop()");
|
||||
host.declare("declare ptr @scr_js_to_dyn(ptr)");
|
||||
B.line(`${cloPtr} = getelementptr inbounds i8, ptr ${callee}, i64 16 ; ->v.fn.clo`);
|
||||
B.line(`${clo} = load ptr, ptr ${cloPtr}`);
|
||||
B.line(`call void @scr_dyn_this_push_dyn(ptr ${receiver})`);
|
||||
B.line(`${word} = call ptr ${entry}(ptr ${clo}, ptr ${words}, ${host.sizeType} ${n})`);
|
||||
B.line("call void @scr_dyn_this_pop()");
|
||||
B.line(`${value} = call ptr @scr_js_to_dyn(ptr ${word})`);
|
||||
B.line(`store ptr ${value}, ptr ${result}`);
|
||||
B.br(lJoin);
|
||||
} else {
|
||||
B.br(lSlow);
|
||||
}
|
||||
B.startBlock(lSlow);
|
||||
let cells = "null";
|
||||
const boxed: string[] = [];
|
||||
if (n > 0) {
|
||||
cells = B.slot();
|
||||
B.entryAllocas.push(`${cells} = alloca [${n} x ptr]`);
|
||||
args.forEach((a, i) => {
|
||||
let cell = a.name;
|
||||
if (a.type.kind === "jsval") {
|
||||
host.declare("declare ptr @scr_js_to_dyn_borrow(ptr)");
|
||||
cell = B.tmp();
|
||||
B.line(`${cell} = call ptr @scr_js_to_dyn_borrow(ptr ${a.name})`);
|
||||
boxed.push(cell);
|
||||
}
|
||||
const p = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds [${n} x ptr], ptr ${cells}, i64 0, i64 ${i}`);
|
||||
B.line(`store ptr ${cell}, ptr ${p}`);
|
||||
});
|
||||
}
|
||||
const r = slow(cells);
|
||||
for (const cell of boxed) B.line(`call void ${releaseSym(host.shapeHost, DYN)}(ptr ${cell})`);
|
||||
B.line(`store ptr ${r}, ptr ${result}`);
|
||||
B.br(lJoin);
|
||||
B.startBlock(lJoin);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = load ptr, ptr ${result}`);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A dynInvoke's callee spelling for the runtime's error texts. */
|
||||
function dynInvokeCalleeName(host: LlvmEmitterContext, e: ExprOf<"dynInvoke">): string {
|
||||
if (e.calleeNameValue === undefined) return host.cstr(e.calleeName);
|
||||
const value = emitBorrowedInput(host, e.calleeNameValue);
|
||||
const name = host.B.tmp();
|
||||
host.B.line(`${name} = getelementptr inbounds %ScrStr, ptr ${value.name}, i32 1`);
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { isRefCounted, type IrExpr, type IrStrCmpOp, type IrType } from "../../ir/ir.js";
|
||||
import { mangleRecordClone, mangleRecordNew } from "../mangle.js";
|
||||
import { arrNewCall, elemAccess } from "./shapes.js";
|
||||
import { recordViewTestLines } from "./record-views.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import { offsetIndex } from "./expr-bytes.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
@@ -1473,16 +1474,70 @@ export function emitRecordExpr(
|
||||
return host.emitRecordKeyGet(e);
|
||||
case "recordHas": {
|
||||
// Own-property presence: only an undefined-armed union slot can
|
||||
// lack its property, by holding the ABSENT state.
|
||||
// lack its property, by holding the ABSENT state, and a PARTIAL
|
||||
// record view (record-views.ts) can lack a field its viewed record
|
||||
// does not declare.
|
||||
const fieldType = host.recordsById
|
||||
.get(e.shapeId)
|
||||
?.fields.find((f) => f.name === e.field)?.type;
|
||||
const obj = host.emitReadReceiver(e.obj);
|
||||
if (host.shapeHost.recordViews?.partialMissing(e.shapeId, e.field) !== true) {
|
||||
const { ptr, type, inline } = host.recordFieldPtr(obj.name, e.shapeId, e.field);
|
||||
if (type.kind !== "union" || undefinedArmTag(type, host.unionsById) < 0)
|
||||
return { name: "true", type: e.type };
|
||||
const v = host.loadField(ptr, type, inline);
|
||||
const absent = host.fieldAbsentTest(v, type.unionId);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = xor i1 ${absent}, true`);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
// A field some partial view of this shape lacks: ask the view first.
|
||||
// Only a present field's slot may be looked up — a partial view's
|
||||
// absent field answers no slot at all (scr_record_view_slot).
|
||||
const position = host.recordsById.get(e.shapeId)!.fields.findIndex((f) => f.name === e.field);
|
||||
host.declare(`declare ptr @scr_record_view_slot(ptr, ${host.sizeType})`);
|
||||
const isView = B.tmp();
|
||||
const rhFrom = B.currentLabel();
|
||||
const view = B.newLabel("rh.view");
|
||||
const done = B.newLabel("rh.done");
|
||||
for (const line of recordViewTestLines(host.sizeType, obj.name, isView))
|
||||
B.line(line.trimStart());
|
||||
// Not a view: every field of the target has its slot.
|
||||
B.condBr(isView, view, done, `, !prof !{!"branch_weights", i32 1, i32 1000}`);
|
||||
B.startBlock(view);
|
||||
const slot = B.tmp();
|
||||
B.line(
|
||||
`${slot} = call ptr @scr_record_view_slot(ptr ${obj.name}, ${host.sizeType} ${position})`,
|
||||
);
|
||||
const hasSlot = B.tmp();
|
||||
B.line(`${hasSlot} = icmp ne ptr ${slot}, null`);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const hasView = B.tmp();
|
||||
B.line(`${hasView} = phi i1 [ true, %${rhFrom} ], [ ${hasSlot}, %${view} ]`);
|
||||
if (fieldType === undefined || fieldType.kind !== "union")
|
||||
return { name: hasView, type: e.type };
|
||||
const unionId = fieldType.unionId;
|
||||
if (undefinedArmTag(fieldType, host.unionsById) < 0) return { name: hasView, type: e.type };
|
||||
// The field is also undefined-armed: a present slot still answers by
|
||||
// its ABSENT state, a field the view lacks by the view.
|
||||
const check = B.newLabel("rh.present");
|
||||
const join = B.newLabel("rh.join");
|
||||
const from = B.currentLabel();
|
||||
B.condBr(hasView, check, join);
|
||||
B.startBlock(check);
|
||||
const { ptr, type, inline } = host.recordFieldPtr(obj.name, e.shapeId, e.field);
|
||||
if (type.kind !== "union" || undefinedArmTag(type, host.unionsById) < 0)
|
||||
return { name: "true", type: e.type };
|
||||
const v = host.loadField(ptr, type, inline);
|
||||
const absent = host.fieldAbsentTest(v, type.unionId);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = xor i1 ${absent}, true`);
|
||||
return { name: t, type: e.type };
|
||||
const absent = host.fieldAbsentTest(v, unionId);
|
||||
const present = B.tmp();
|
||||
B.line(`${present} = xor i1 ${absent}, true`);
|
||||
// The field load can end in a later block (a view's slot read).
|
||||
const checked = B.currentLabel();
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const answer = B.tmp();
|
||||
B.line(`${answer} = phi i1 [ false, %${from} ], [ ${present}, %${checked} ]`);
|
||||
return { name: answer, type: e.type };
|
||||
}
|
||||
case "fieldAbsent":
|
||||
return { name: host.absentInstanceRef(e.unionId), type: e.type };
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
llFieldType,
|
||||
loadStructField,
|
||||
releaseSym,
|
||||
traceAdapter,
|
||||
traceArg,
|
||||
vAdapters,
|
||||
type ShapeHost,
|
||||
@@ -754,6 +755,50 @@ function symbolKeyLines(
|
||||
};
|
||||
}
|
||||
|
||||
/** The ScrStreamTypedDesc constant (scr_stream.h) of a record type an
|
||||
* object-mode stream holds as the record itself, or null for a type whose
|
||||
* chunks stay boxed: records without declared fields, with an index
|
||||
* signature or accessor slots, tuples, the text codec records (they box as
|
||||
* handles), and records the collector traces (a stream does not trace the
|
||||
* records it holds). The constant's `box` converts the record as a
|
||||
* live-reference conversion to dyn does (streamTypedRefBoxValue). */
|
||||
export function streamTypedDesc(host: LlvmEmitterContext, t: IrType): string | null {
|
||||
if (t.kind !== "record") return null;
|
||||
const shape = host.recordsById.get(t.shapeId);
|
||||
if (
|
||||
!shape ||
|
||||
shape.fields.length === 0 ||
|
||||
shape.indexValue !== undefined ||
|
||||
shape.tuple ||
|
||||
shapeHasAccessorSlots(shape) ||
|
||||
shape.fields.some((field) => field.name === "handleEvent" && field.type.kind === "func") ||
|
||||
textCodecRecordClass(t, (id) => host.recordsById.get(id)) !== null ||
|
||||
traceAdapter(host.shapeHost, t) !== null
|
||||
)
|
||||
return null;
|
||||
const key = typeKey(t);
|
||||
const cacheKey = `std:${key}`;
|
||||
const existing = host.resolveThunks.get(cacheKey);
|
||||
if (existing) return `@${existing}`;
|
||||
const sym = host.contentSymbol("sc_std_", key);
|
||||
host.resolveThunks.set(cacheKey, sym);
|
||||
host.journal?.allocated(sym, "streamTypedDesc", [t]);
|
||||
const B = new BlockBuilder();
|
||||
const boxed = streamTypedRefBoxValue(host, B, t, "%p");
|
||||
B.terminate(`ret ptr ${boxed}`);
|
||||
const rc = vAdapters(host.shapeHost, t);
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal ptr @${sym}_box(ptr %p) ${FN_ATTRS} { ; box a typed stream chunk ${key}`,
|
||||
B.render(),
|
||||
`}`,
|
||||
``,
|
||||
`@${sym} = internal constant { ptr, ${host.sizeType}, ptr, ptr, ptr } { ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr ${rc.retain}, ptr ${rc.release}, ptr @${sym}_box }`,
|
||||
``,
|
||||
);
|
||||
host.journal?.built();
|
||||
return `@${sym}`;
|
||||
}
|
||||
|
||||
export function streamTypedRefBoxValue(
|
||||
host: LlvmEmitterContext,
|
||||
B: BlockBuilder,
|
||||
@@ -2018,6 +2063,9 @@ export function streamFromArrayAdapter(
|
||||
}
|
||||
|
||||
registerReplay("dynPromiseAdapter", (host, args) => dynPromiseAdapter(host, args[0] as IrType));
|
||||
registerReplay("streamTypedDesc", (host, args) =>
|
||||
(streamTypedDesc(host, args[0] as IrType) ?? "").slice(1),
|
||||
);
|
||||
registerReplay("liveDynUnionRefAdapter", (host, args) =>
|
||||
liveDynUnionRefAdapter(host, args[0] as IrType & { kind: "union" }),
|
||||
);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { type IrType, isRefCounted, typeKey } from "../../ir/ir.js";
|
||||
import { FN_ATTRS, releaseSym, retainSym, traceArg } from "./shapes.js";
|
||||
import { type IrType, isRefCounted, STRING, typeKey } from "../../ir/ir.js";
|
||||
import { streamTypedRefEligible } from "../../ir/analysis.js";
|
||||
import { FN_ATTRS, releaseSym, retainSym, traceArg, vAdapters } from "./shapes.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { unionPeekLines, unionTagLines } from "./union-repr.js";
|
||||
import { registerReplay } from "./emit-journal.js";
|
||||
import { streamTypedDesc } from "./expr-stream-bridges.js";
|
||||
|
||||
export function streamDataAdapter(
|
||||
host: LlvmEmitterContext,
|
||||
@@ -259,6 +261,31 @@ export function streamDoneFnFor(
|
||||
strTag = def!.arms.findIndex((a) => a.kind === "string");
|
||||
}
|
||||
}
|
||||
// data of one record type or nothing (a typed completion,
|
||||
// pinTypedDataCompletion): a record with a typed-chunk descriptor
|
||||
// completes as a typed chunk, and another record boxes by reference
|
||||
// (the conversion a dyn `data` parameter receives).
|
||||
let typedTag = -1;
|
||||
let typedDesc: string | null = null;
|
||||
let liveData = false;
|
||||
if (valueData && dataT?.kind === "union") {
|
||||
const arms = host.unionsById.get(dataT.unionId)?.arms ?? [];
|
||||
const record = arms.findIndex((a) => a.kind === "record");
|
||||
if (arms.length === 2 && record >= 0 && arms.some((a) => a.kind === "undefinedT")) {
|
||||
typedDesc = streamTypedDesc(host, arms[record]!);
|
||||
if (typedDesc !== null) typedTag = record;
|
||||
else {
|
||||
// the records a conversion to dyn keeps by reference (Lowerer's
|
||||
// liveReference)
|
||||
const armT = arms[record]!;
|
||||
const shape = armT.kind === "record" ? host.recordsById.get(armT.shapeId) : undefined;
|
||||
liveData =
|
||||
streamTypedRefEligible(armT) &&
|
||||
shape !== undefined &&
|
||||
(shape.fields.length > 0 || shape.indexValue !== undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
const entry =
|
||||
kind === "c"
|
||||
? "scr_stream_construct_done"
|
||||
@@ -269,18 +296,20 @@ export function streamDoneFnFor(
|
||||
: kind === "d"
|
||||
? "scr_stream_destroy_done"
|
||||
: kind === "t"
|
||||
? valueData
|
||||
? "scr_stream_transform_done_value"
|
||||
: "scr_stream_transform_done"
|
||||
: valueData
|
||||
? "scr_stream_flush_done_value"
|
||||
: "scr_stream_flush_done";
|
||||
? typedDesc !== null
|
||||
? "scr_stream_transform_done_typed"
|
||||
: valueData
|
||||
? "scr_stream_transform_done_value"
|
||||
: "scr_stream_transform_done"
|
||||
: typedDesc !== null
|
||||
? "scr_stream_flush_done_typed"
|
||||
: valueData
|
||||
? "scr_stream_flush_done_value"
|
||||
: "scr_stream_flush_done";
|
||||
const twoSlot = (kind === "t" || kind === "l") && !valueData;
|
||||
host.declare(
|
||||
`declare void @${entry}(ptr, ptr${twoSlot ? ", ptr, ptr" : valueData ? ", ptr" : ""})`,
|
||||
`declare void @${entry}(ptr, ptr${typedDesc !== null ? ", ptr, ptr" : twoSlot ? ", ptr, ptr" : valueData ? ", ptr" : ""})`,
|
||||
);
|
||||
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
|
||||
host.declare(`declare void @scr_stream_release_v(ptr)`);
|
||||
host.declare(`declare ptr @scr_error_retain_v(ptr)`);
|
||||
host.declare(`declare void @scr_union_release(ptr)`);
|
||||
const params = [
|
||||
@@ -293,15 +322,21 @@ export function streamDoneFnFor(
|
||||
`entry:`,
|
||||
` %eslot = alloca ptr`,
|
||||
...(twoSlot ? [` %bslot = alloca ptr`, ` %sslot = alloca ptr`] : []),
|
||||
...(typedDesc !== null ? [` %tslot = alloca ptr`] : []),
|
||||
` %capsp = getelementptr inbounds %ScrClosure, ptr %self, i64 1`,
|
||||
` %bx = load ptr, ptr %capsp`,
|
||||
` %s = call ptr @scr_box_get_ref(ptr %bx) ; the stream, +1`,
|
||||
// the stream, borrowed: the box holds it, and the closure (alive for
|
||||
// the call) holds the box
|
||||
` %sp = getelementptr inbounds %ScrBox, ptr %bx, i64 0, i32 5`,
|
||||
` %s = load ptr, ptr %sp`,
|
||||
` store ptr null, ptr %eslot`,
|
||||
...(twoSlot ? [` store ptr null, ptr %bslot`, ` store ptr null, ptr %sslot`] : []),
|
||||
...(typedDesc !== null ? [` store ptr null, ptr %tslot`] : []),
|
||||
];
|
||||
// A tag-guarded retained peek out of a union argument into a slot.
|
||||
// A tag-guarded peek out of a union argument into a slot, retained
|
||||
// unless retain is null.
|
||||
let arm = 0;
|
||||
const unwrap = (u: string, tag: number, retain: string, slot: string): void => {
|
||||
const unwrap = (u: string, tag: number, retain: string | null, slot: string): void => {
|
||||
const a = arm++;
|
||||
const union = u === "%e" ? errT : dataT;
|
||||
const nullable = union?.kind === "union" ? host.nullableUnions.get(union.unionId) : null;
|
||||
@@ -314,8 +349,9 @@ export function streamDoneFnFor(
|
||||
` br i1 %hit${a}, label %yes${a}, label %done${a}`,
|
||||
`yes${a}:`,
|
||||
...unionPeekLines(nullable, u, `%pv${a}`),
|
||||
` %rt${a} = call ptr ${retain}(ptr %pv${a})`,
|
||||
` store ptr %rt${a}, ptr ${slot}`,
|
||||
...(retain !== null
|
||||
? [` %rt${a} = call ptr ${retain}(ptr %pv${a})`, ` store ptr %rt${a}, ptr ${slot}`]
|
||||
: [` store ptr %pv${a}, ptr ${slot}`]),
|
||||
` br label %done${a}`,
|
||||
`done${a}:`,
|
||||
);
|
||||
@@ -329,8 +365,21 @@ export function streamDoneFnFor(
|
||||
host.declare(`declare ptr @scr_str_retain_v(ptr)`);
|
||||
unwrap("%d", strTag, "@scr_str_retain_v", "%sslot");
|
||||
}
|
||||
if (typedDesc !== null) unwrap("%d", typedTag, null, "%tslot");
|
||||
d.push(` %ev = load ptr, ptr %eslot`);
|
||||
if (valueData) {
|
||||
if (typedDesc !== null) {
|
||||
d.push(
|
||||
` %tv = load ptr, ptr %tslot`,
|
||||
` call void @${entry}(ptr %s, ptr %ev, ptr %tv, ptr ${typedDesc}) ; moves err; borrows s and the record`,
|
||||
);
|
||||
} else if (liveData) {
|
||||
host.declare(`declare void @scr_dyn_release(ptr)`);
|
||||
d.push(
|
||||
` %dv = call ptr @${host.liveDynUnionRefAdapter(dataT as IrType & { kind: "union" })}(ptr %d)`,
|
||||
` call void @${entry}(ptr %s, ptr %ev, ptr %dv) ; moves err; borrows s and the value`,
|
||||
` call void @scr_dyn_release(ptr %dv)`,
|
||||
);
|
||||
} else if (valueData) {
|
||||
const conv = host.dyn.toDynHelper(dataT!);
|
||||
host.declare(`declare void @scr_dyn_release(ptr)`);
|
||||
d.push(
|
||||
@@ -347,7 +396,6 @@ export function streamDoneFnFor(
|
||||
} else {
|
||||
d.push(` call void @${entry}(ptr %s, ptr %ev) ; moves err; borrows s`);
|
||||
}
|
||||
d.push(` call void @scr_stream_release_v(ptr %s)`);
|
||||
if (errT !== undefined) d.push(` call void ${releaseSym(host.shapeHost, errT)}(ptr %e)`);
|
||||
if (dataT !== undefined && isRefCounted(dataT))
|
||||
d.push(` call void ${releaseSym(host.shapeHost, dataT)}(ptr %d)`);
|
||||
@@ -840,18 +888,42 @@ export function streamCbThunkFor(
|
||||
host.declare(`declare void @scr_dyn_release(ptr)`);
|
||||
host.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`);
|
||||
// A typed chunk of the declared record type (scr_stream_push_typed)
|
||||
// is the record itself; any other chunk converts.
|
||||
const desc = streamTypedDesc(host, p);
|
||||
if (desc !== null) {
|
||||
host.declare(`declare ptr @scr_stream_chunk_typed(ptr, ptr, ptr)`);
|
||||
d.push(
|
||||
` br label %ctchk${i}`,
|
||||
`ctchk${i}:`,
|
||||
` %ctt${i} = call ptr @scr_stream_chunk_typed(ptr %s, ptr %chunk, ptr ${desc})`,
|
||||
` %cthit${i} = icmp ne ptr %ctt${i}, null`,
|
||||
` br i1 %cthit${i}, label %ctok${i}, label %ctconv${i}`,
|
||||
`ctconv${i}:`,
|
||||
);
|
||||
}
|
||||
d.push(
|
||||
` %cv${i} = call ptr @scr_stream_chunk_value(ptr %s, ptr %chunk)`,
|
||||
` %ct${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %cv${i}, ptr null)`,
|
||||
` call void @scr_dyn_release(ptr %cv${i})`,
|
||||
` %ctbad${i} = call zeroext i1 @scr_exc_pending()`,
|
||||
` br i1 %ctbad${i}, label %ctfail${i}, label %ctok${i}`,
|
||||
` br i1 %ctbad${i}, label %ctfail${i}, label %${desc !== null ? "ctconvok" : "ctok"}${i}`,
|
||||
`ctfail${i}:`,
|
||||
...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []),
|
||||
` ret void`,
|
||||
`ctok${i}:`,
|
||||
);
|
||||
passed.push(`${ty} %ct${i}`);
|
||||
if (desc !== null) {
|
||||
d.push(
|
||||
`ctconvok${i}:`,
|
||||
` br label %ctok${i}`,
|
||||
`ctok${i}:`,
|
||||
` %ctv${i} = phi ptr [ %ctt${i}, %ctchk${i} ], [ %ct${i}, %ctconvok${i} ]`,
|
||||
);
|
||||
passed.push(`ptr %ctv${i}`);
|
||||
} else {
|
||||
d.push(`ctok${i}:`);
|
||||
passed.push(`${ty} %ct${i}`);
|
||||
}
|
||||
} else if (isEncPos && p.kind === "string") {
|
||||
// Node's encoding for decoded (Buffer) chunks is 'buffer'; an
|
||||
// object-mode side passes the write's encoding argument ("" for
|
||||
@@ -868,18 +940,46 @@ export function streamCbThunkFor(
|
||||
host.declare(`declare void @scr_dyn_release(ptr)`);
|
||||
host.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
if (hasThis) host.declare(`declare void @scr_stream_release_v(ptr)`);
|
||||
// An object-mode write without an encoding argument passes
|
||||
// undefined: a pointer union whose unit arm is undefined holds it as
|
||||
// the null pointer, so that case skips the conversion.
|
||||
const pointer = p.kind === "union" ? host.nullableUnions.get(p.unionId) : null;
|
||||
const undefinedIsNull =
|
||||
pointer !== null &&
|
||||
p.kind === "union" &&
|
||||
host.unionsById.get(p.unionId)?.arms[pointer.unitTag]?.kind === "undefinedT";
|
||||
if (undefinedIsNull) {
|
||||
host.declare(`declare zeroext i1 @scr_stream_chunk_encoding_undefined(ptr)`);
|
||||
d.push(
|
||||
` br label %etchk${i}`,
|
||||
`etchk${i}:`,
|
||||
` %eu${i} = call zeroext i1 @scr_stream_chunk_encoding_undefined(ptr %s)`,
|
||||
` br i1 %eu${i}, label %etok${i}, label %etconv${i}`,
|
||||
`etconv${i}:`,
|
||||
);
|
||||
}
|
||||
d.push(
|
||||
` %ev${i} = call ptr @scr_stream_chunk_encoding_value(ptr %s)`,
|
||||
` %et${i} = call ${ty === "i1" ? "zeroext i1" : ty} @${helper}(ptr %ev${i}, ptr null)`,
|
||||
` call void @scr_dyn_release(ptr %ev${i})`,
|
||||
` %etbad${i} = call zeroext i1 @scr_exc_pending()`,
|
||||
` br i1 %etbad${i}, label %etfail${i}, label %etok${i}`,
|
||||
` br i1 %etbad${i}, label %etfail${i}, label %${undefinedIsNull ? "etconvok" : "etok"}${i}`,
|
||||
`etfail${i}:`,
|
||||
...(hasThis ? [` call void @scr_stream_release_v(ptr %sr)`] : []),
|
||||
` ret void`,
|
||||
`etok${i}:`,
|
||||
);
|
||||
passed.push(`${ty} %et${i}`);
|
||||
if (undefinedIsNull) {
|
||||
d.push(
|
||||
`etconvok${i}:`,
|
||||
` br label %etok${i}`,
|
||||
`etok${i}:`,
|
||||
` %etv${i} = phi ptr [ null, %etchk${i} ], [ %et${i}, %etconvok${i} ]`,
|
||||
);
|
||||
passed.push(`${ty} %etv${i}`);
|
||||
} else {
|
||||
d.push(`etok${i}:`);
|
||||
passed.push(`${ty} %et${i}`);
|
||||
}
|
||||
} else if (isErrPos) {
|
||||
// destroy's error argument: `Error | null` — wrap type-directedly.
|
||||
// The finished/pipeline callback ("e") may declare `Error | null |
|
||||
@@ -966,6 +1066,161 @@ registerReplay("streamDoneFnFor", (host, args) =>
|
||||
registerReplay("fsRenameThunkFor", (host, args) =>
|
||||
fsRenameThunkFor(host, args[0] as IrType & { kind: "func" }),
|
||||
);
|
||||
/** The fs.watch listener adapter for an (eventType, filename) listener, or
|
||||
* for an (eventType) listener whose parameter is dynamic: the runtime hands
|
||||
* the borrowed C strings, the thunk builds what the listener declared. The
|
||||
* listener owns its +1 arguments (the universal convention
|
||||
* scr_watch_thunk_event follows), so a string the thunk boxes into a dynamic
|
||||
* value is released once the box holds it. */
|
||||
export function watchNameThunkFor(
|
||||
host: LlvmEmitterContext,
|
||||
cbT: IrType & { kind: "func" },
|
||||
): string {
|
||||
const key = `watchname:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = host.contentSymbol("sc_watchname_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "watchNameThunkFor", [cbT]);
|
||||
const S = host.sizeType;
|
||||
host.declare(`declare ${S} @strlen(ptr)`);
|
||||
host.declare(`declare ptr @scr_str_new(ptr, ${S})`);
|
||||
host.declare(`declare void @scr_str_release(ptr)`);
|
||||
const d: string[] = [
|
||||
`define internal void @${sym}(ptr %cb, ptr %event, ptr %name) ${FN_ATTRS} { ; fs.watch listener ${typeKey(cbT)}`,
|
||||
`entry:`,
|
||||
` %evlen = call ${S} @strlen(ptr %event)`,
|
||||
` %evstr = call ptr @scr_str_new(ptr %event, ${S} %evlen)`,
|
||||
];
|
||||
const named = cbT.params.length === 2;
|
||||
if (named) {
|
||||
d.push(
|
||||
` %slot = alloca ptr`,
|
||||
` %has = icmp ne ptr %name, null`,
|
||||
` br i1 %has, label %yes, label %no`,
|
||||
`yes:`,
|
||||
);
|
||||
const nameParam = cbT.params[1]!;
|
||||
if (nameParam.kind === "dyn") {
|
||||
host.declare(`declare ptr @scr_dyn_new_null()`);
|
||||
host.declare(`declare ptr @scr_dyn_new_str(ptr)`);
|
||||
d.push(
|
||||
` %nlen = call ${S} @strlen(ptr %name)`,
|
||||
` %nstr = call ptr @scr_str_new(ptr %name, ${S} %nlen)`,
|
||||
` %ndyn = call ptr @scr_dyn_new_str(ptr %nstr)`,
|
||||
` call void @scr_str_release(ptr %nstr)`,
|
||||
` store ptr %ndyn, ptr %slot`,
|
||||
` br label %go`,
|
||||
`no:`,
|
||||
` %dn = call ptr @scr_dyn_new_null()`,
|
||||
` store ptr %dn, ptr %slot`,
|
||||
);
|
||||
} else {
|
||||
if (nameParam.kind !== "union")
|
||||
throw new InternalCompilerError("llvm emitter bug: fs.watch filename param is not a union");
|
||||
const def = host.unionsById.get(nameParam.unionId);
|
||||
const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
|
||||
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
|
||||
if (strTag < 0 || nullTag < 0)
|
||||
throw new InternalCompilerError("llvm emitter bug: fs.watch filename union lacks its arms");
|
||||
if (host.nullableUnions.has(nameParam.unionId)) {
|
||||
// `string | null` erases: the owned payload IS the union's value.
|
||||
d.push(
|
||||
` %nlen = call ${S} @strlen(ptr %name)`,
|
||||
` %nstr = call ptr @scr_str_new(ptr %name, ${S} %nlen)`,
|
||||
` store ptr %nstr, ptr %slot`,
|
||||
` br label %go`,
|
||||
`no:`,
|
||||
` store ptr null, ptr %slot`,
|
||||
);
|
||||
} else {
|
||||
const rc = vAdapters(host.shapeHost, STRING);
|
||||
host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
|
||||
d.push(
|
||||
` %nlen = call ${S} @strlen(ptr %name)`,
|
||||
` %nstr = call ptr @scr_str_new(ptr %name, ${S} %nlen)`,
|
||||
` %nu = call ptr @scr_union_new_ref(i32 ${strTag}, ptr %nstr, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, STRING)})`,
|
||||
` store ptr %nu, ptr %slot`,
|
||||
` br label %go`,
|
||||
`no:`,
|
||||
` store ptr ${host.unitInstanceRef(nameParam.unionId, nullTag)}, ptr %slot`,
|
||||
);
|
||||
}
|
||||
}
|
||||
d.push(` br label %go`, `go:`);
|
||||
}
|
||||
let ev: string;
|
||||
if (cbT.params[0]?.kind === "dyn") {
|
||||
// The box retains the string; the thunk's own reference goes.
|
||||
host.declare(`declare ptr @scr_dyn_new_str(ptr)`);
|
||||
d.push(
|
||||
` %evd = call ptr @scr_dyn_new_str(ptr %evstr)`,
|
||||
` call void @scr_str_release(ptr %evstr)`,
|
||||
);
|
||||
ev = "%evd";
|
||||
} else {
|
||||
ev = "%evstr";
|
||||
}
|
||||
d.push(
|
||||
` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`,
|
||||
` %fn = load ptr, ptr %fnp`,
|
||||
);
|
||||
if (named)
|
||||
d.push(` %arg = load ptr, ptr %slot`, ` call void %fn(ptr %cb, ptr ${ev}, ptr %arg)`);
|
||||
else d.push(` call void %fn(ptr %cb, ptr ${ev})`);
|
||||
d.push(` ret void`, `}`, ``);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
/** The FSWatcher 'error' listener adapter: the runtime hands a borrowed
|
||||
* ScrError, the listener receives the declared dyn value or an Error
|
||||
* object it owns. A zero-parameter listener uses the runtime's
|
||||
* scr_watch_err_thunk0 and never reaches here. */
|
||||
export function watchErrorThunkFor(
|
||||
host: LlvmEmitterContext,
|
||||
cbT: IrType & { kind: "func" },
|
||||
): string {
|
||||
const key = `watcherr:${typeKey(cbT)}`;
|
||||
let sym = host.resolveThunks.get(key);
|
||||
if (sym) return sym;
|
||||
sym = host.contentSymbol("sc_watcherr_", key);
|
||||
host.resolveThunks.set(key, sym);
|
||||
host.journal?.allocated(sym, "watchErrorThunkFor", [cbT]);
|
||||
const param = cbT.params[0]!;
|
||||
const d: string[] = [
|
||||
`define internal void @${sym}(ptr %cb, ptr %err) ${FN_ATTRS} { ; FSWatcher error listener ${typeKey(cbT)}`,
|
||||
`entry:`,
|
||||
];
|
||||
if (param.kind === "dyn") {
|
||||
host.declare(`declare ptr @scr_dyn_from_error(ptr)`);
|
||||
d.push(` %de = call ptr @scr_dyn_from_error(ptr %err)`);
|
||||
} else if (param.kind === "object" && param.className === "%Error") {
|
||||
host.declare(`declare ptr @scr_error_retain(ptr)`);
|
||||
d.push(` %de = call ptr @scr_error_retain(ptr %err)`);
|
||||
} else {
|
||||
throw new InternalCompilerError("llvm emitter bug: FSWatcher error param is not an Error");
|
||||
}
|
||||
d.push(
|
||||
` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`,
|
||||
` %fn = load ptr, ptr %fnp`,
|
||||
` call void %fn(ptr %cb, ptr %de)`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
host.journal?.built();
|
||||
return sym;
|
||||
}
|
||||
|
||||
registerReplay("watchNameThunkFor", (host, args) =>
|
||||
watchNameThunkFor(host, args[0] as IrType & { kind: "func" }),
|
||||
);
|
||||
registerReplay("watchErrorThunkFor", (host, args) =>
|
||||
watchErrorThunkFor(host, args[0] as IrType & { kind: "func" }),
|
||||
);
|
||||
registerReplay("cryptoIntThunkFor", (host, args) =>
|
||||
cryptoIntThunkFor(host, args[0] as IrType & { kind: "func" }),
|
||||
);
|
||||
|
||||
@@ -57,7 +57,7 @@ export function jsIcMethods(host: Host): boolean {
|
||||
return (
|
||||
jsImmediates(host) &&
|
||||
host.currentTier === "dynamic" &&
|
||||
jsIcMethodSites(host.currentFunctionName)
|
||||
jsIcMethodSites(host.currentFunctionName, host.loopDepth > 0)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -214,6 +214,9 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue
|
||||
|
||||
export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
// The generic emitter registrations queue the maxListeners leak warning
|
||||
// for the next tick like emitter.on (lib-process.ts).
|
||||
if (e.fn === "emitter.onFlex" || e.fn === "emitter.onAny") host.usesTimers = true;
|
||||
if (host.coroAwait && e.fn === "async.hop") {
|
||||
host.emitCoroAwait(null);
|
||||
return { name: "", type: e.type };
|
||||
@@ -1185,6 +1188,10 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
case "str":
|
||||
case "regexp":
|
||||
case "intl":
|
||||
// ICU-backed Intl (scr_intl.c): ordinary libCalls whose C symbols the
|
||||
// runtime provides — the same generic emission the engine-free intl
|
||||
// forms use.
|
||||
case "icu":
|
||||
case "sym":
|
||||
case "perf":
|
||||
case "number":
|
||||
@@ -1272,6 +1279,9 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
return host.emitGenericLibCall(e);
|
||||
case "nodeIsland":
|
||||
return host.emitGenericLibCall(e);
|
||||
// Primitive wrapper objects (scr_js_wrapper.c).
|
||||
case "wrapper":
|
||||
return host.emitGenericLibCall(e);
|
||||
case "webstream":
|
||||
// A stream's start, pull and cancel steps run as promise jobs, so
|
||||
// a program with the classes runs its event loop.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Focused LLVM library-call emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { DYN, F64, STRING, type IrExpr } from "../../ir/ir.js";
|
||||
import { DYN, F64, STRING, type IrExpr, type IrType } from "../../ir/ir.js";
|
||||
import { mangleRecordNew } from "../mangle.js";
|
||||
import { arrNewCall, traceArg, vAdapters } from "./shapes.js";
|
||||
import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js";
|
||||
@@ -173,6 +173,22 @@ function emitObjEntryPairs(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The adapter of an fs.watch 'change' listener, by its declared shape: the
|
||||
* runtime's for no parameters and for a string eventType, and one this
|
||||
* module emits for a dynamic eventType or an (eventType, filename) listener.
|
||||
* Only the runtime's adapters need a declaration. */
|
||||
function watchChangeAdapter(host: LlvmEmitterContext, cbT: IrType & { kind: "func" }): string {
|
||||
const runtime =
|
||||
cbT.params.length === 0
|
||||
? "scr_watch_thunk0"
|
||||
: cbT.params.length === 1 && cbT.params[0]!.kind !== "dyn"
|
||||
? "scr_watch_thunk_event"
|
||||
: null;
|
||||
if (runtime === null) return host.watchNameThunkFor(cbT);
|
||||
host.declare(`declare void @${runtime}(ptr, ptr, ptr)`);
|
||||
return runtime;
|
||||
}
|
||||
|
||||
export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
const alwaysThrowSym = FS_ALWAYS_THROW_SYMS[e.fn];
|
||||
@@ -448,17 +464,19 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
// loop (usesTimers).
|
||||
host.usesTimers = true;
|
||||
const path = host.emitExpr(e.args[0]!);
|
||||
host.declare(`declare ptr @scr_fs_watch(ptr, ptr, ptr)`);
|
||||
host.declare(`declare ptr @scr_fs_watch(ptr, ptr, ptr, i1 zeroext)`);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call ptr @scr_fs_watch(ptr ${path.name}, ptr null, ptr null)`);
|
||||
B.line(`${t} = call ptr @scr_fs_watch(ptr ${path.name}, ptr null, ptr null, i1 false)`);
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
}
|
||||
if (e.fn === "fs.watchCb") {
|
||||
// The callback MOVES into the watcher's registry; the adapter is
|
||||
// runtime-provided per listener shape (zero-param, or the eventType
|
||||
// string). May throw (ENOENT) — the standard pending check.
|
||||
// runtime-provided per listener shape (zero-param, the eventType
|
||||
// string, or the compiler's (eventType, filename) adapter). May
|
||||
// throw (ENOENT, or the macOS directory-names refusal) — the
|
||||
// standard pending check.
|
||||
host.usesTimers = true;
|
||||
const cbT = e.args[1]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
@@ -466,15 +484,64 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
const path = host.emitExpr(e.args[0]!);
|
||||
const cb = host.emitExpr(e.args[1]!);
|
||||
host.toHeap(cb);
|
||||
const adapter = cbT.params.length === 0 ? "scr_watch_thunk0" : "scr_watch_thunk_event";
|
||||
host.declare(`declare void @${adapter}(ptr, ptr)`);
|
||||
host.declare(`declare ptr @scr_fs_watch(ptr, ptr, ptr)`);
|
||||
const adapter = watchChangeAdapter(host, cbT);
|
||||
host.declare(`declare ptr @scr_fs_watch(ptr, ptr, ptr, i1 zeroext)`);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = call ptr @scr_fs_watch(ptr ${path.name}, ptr ${cb.name}, ptr @${adapter})`);
|
||||
B.line(
|
||||
`${t} = call ptr @scr_fs_watch(ptr ${path.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${cbT.params.length === 2 ? "true" : "false"})`,
|
||||
);
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
}
|
||||
if (e.fn === "watcher.on") {
|
||||
// (recv, event, flags, listener) — flags: 1 once, 2 prepend. The
|
||||
// listener MOVES in; the adapter follows the event (0 change, 1
|
||||
// error, 2 close — CLOSE takes no adapter, a plain zero-arg call).
|
||||
// Registration can throw (the macOS directory-names refusal).
|
||||
const flagsArg = e.args[2]!;
|
||||
if (flagsArg.kind !== "numLit")
|
||||
throw new InternalCompilerError("llvm emitter bug: watcher.on flags not a literal");
|
||||
const flags = flagsArg.value;
|
||||
const codeArg = e.args[1]!;
|
||||
if (codeArg.kind !== "numLit")
|
||||
throw new InternalCompilerError("llvm emitter bug: watcher.on event not a literal");
|
||||
const code = codeArg.value;
|
||||
const cbT = e.args[3]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: watcher.on listener not a func");
|
||||
const w = host.emitExpr(e.args[0]!);
|
||||
const event = host.emitExpr(e.args[1]!);
|
||||
const cb = host.emitExpr(e.args[3]!);
|
||||
host.toHeap(cb);
|
||||
let adapter = "null";
|
||||
let wantsName = false;
|
||||
if (code === 0) {
|
||||
adapter = watchChangeAdapter(host, cbT);
|
||||
wantsName = cbT.params.length === 2;
|
||||
} else if (code === 1) {
|
||||
adapter = cbT.params.length === 0 ? "scr_watch_err_thunk0" : host.watchErrorThunkFor(cbT);
|
||||
if (cbT.params.length === 0) host.declare(`declare void @scr_watch_err_thunk0(ptr, ptr)`);
|
||||
}
|
||||
host.declare(
|
||||
`declare void @scr_watcher_on(ptr, double, ptr, ptr, i1 zeroext, i1 zeroext, i1 zeroext)`,
|
||||
);
|
||||
B.line(
|
||||
`call void @scr_watcher_on(ptr ${w.name}, double ${event.name}, ptr ${cb.name}, ptr ${adapter === "null" ? "null" : `@${adapter}`}, i1 ${flags & 1 ? "true" : "false"}, i1 ${flags & 2 ? "true" : "false"}, i1 ${wantsName ? "true" : "false"})`,
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "watcher.off") {
|
||||
// Removal is by pointer identity: the listener stays borrowed
|
||||
// (never heap-moved), exactly like process.offExit's.
|
||||
const w = host.emitExpr(e.args[0]!);
|
||||
const event = host.emitExpr(e.args[1]!);
|
||||
const cb = host.emitExpr(e.args[2]!);
|
||||
host.declare(`declare void @scr_watcher_off(ptr, double, ptr)`);
|
||||
B.line(`call void @scr_watcher_off(ptr ${w.name}, double ${event.name}, ptr ${cb.name})`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "watcher.close") {
|
||||
// Idempotent; never throws.
|
||||
const w = host.emitExpr(e.args[0]!);
|
||||
@@ -482,6 +549,45 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
B.line(`call void @scr_watcher_close(ptr ${w.name})`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "fs.watchFile") {
|
||||
// (path, interval, persistent, listener) — the listener MOVES into
|
||||
// the poller's registry and the adapter is runtime-provided per
|
||||
// listener shape (zero, one or two Stats parameters). The poller
|
||||
// re-arms a timer every interval, so this is a timer program. May
|
||||
// throw (a null byte in the path, or an interval that is not a
|
||||
// uint32) — the standard pending check.
|
||||
host.usesTimers = true;
|
||||
const cbT = e.args[3]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: fs.watchFile listener not a func");
|
||||
const path = host.emitExpr(e.args[0]!);
|
||||
const interval = host.emitExpr(e.args[1]!);
|
||||
const persistent = host.emitExpr(e.args[2]!);
|
||||
const cb = host.emitExpr(e.args[3]!);
|
||||
host.toHeap(cb);
|
||||
const adapter = `scr_watch_file_thunk${cbT.params.length}`;
|
||||
host.declare(`declare void @${adapter}(ptr, ptr, ptr)`);
|
||||
host.declare(`declare void @scr_fs_watch_file(ptr, double, i1 zeroext, ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_fs_watch_file(ptr ${path.name}, double ${interval.name}, i1 ${persistent.name}, ptr ${cb.name}, ptr @${adapter})`,
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "fs.unwatchFile") {
|
||||
// Removal is by closure identity: the listener stays borrowed
|
||||
// (never heap-moved), exactly like watcher.off's; an absent one is
|
||||
// the NULL that removes every listener on the path. A null byte in
|
||||
// the path throws.
|
||||
const path = host.emitExpr(e.args[0]!);
|
||||
const cb = e.args[1] === undefined ? null : host.emitExpr(e.args[1]!);
|
||||
host.declare(`declare void @scr_fs_unwatch_file(ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_fs_unwatch_file(ptr ${path.name}, ptr ${cb === null ? "null" : cb.name})`,
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "fs.globTypesSync" || e.fn === "fsp.globTypes") {
|
||||
// Dirent rows from one glob snapshot (scr_glob.c): mode 1 is the
|
||||
// globSync form, 3 the fs/promises form with resolved parent paths.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Focused LLVM library-call emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { F64, type IrType, RUNTIME_ERROR_CLASSES, STRING, typeKey } from "../../ir/ir.js";
|
||||
import { BOOL, F64, type IrType, RUNTIME_ERROR_CLASSES, STRING, typeKey } from "../../ir/ir.js";
|
||||
import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { mockTimerSym } from "./lib-shared.js";
|
||||
@@ -679,32 +679,46 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
host.emitPendingCheck();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "process.onExit") {
|
||||
if (e.fn === "process.onExit" || e.fn === "process.prependExit") {
|
||||
// Runtime adapters cover both shapes (the code is a plain double);
|
||||
// the registry owns the callback.
|
||||
const cbT = e.args[0]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: process.onExit callback not a func");
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} callback not a func`);
|
||||
const cb = host.emitExpr(e.args[0]!);
|
||||
const once = host.emitExpr(e.args[1]!);
|
||||
host.toHeap(cb);
|
||||
const adapter = cbT.params.length === 0 ? "scr_exit_thunk0" : "scr_exit_thunk_code";
|
||||
host.declare(`declare void @${adapter}(ptr, double)`);
|
||||
if (e.fn === "process.prependExit") {
|
||||
host.declare(`declare void @scr_process_prepend_exit(ptr, ptr)`);
|
||||
B.line(`call void @scr_process_prepend_exit(ptr ${cb.name}, ptr @${adapter})`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
const once = host.emitExpr(e.args[1]!);
|
||||
host.declare(`declare void @scr_process_on_exit(ptr, ptr, i1 zeroext)`);
|
||||
B.line(`call void @scr_process_on_exit(ptr ${cb.name}, ptr @${adapter}, i1 ${once.name})`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "process.envGet" || e.fn === "process.columns" || e.fn === "process.rows") {
|
||||
// getenv(3) / ioctl(TIOCGWINSZ): the runtime answers a +1 string or
|
||||
// NULL (a width or a negative sentinel); the union construction is
|
||||
// type-directed HERE — present wraps the value arm, absent yields
|
||||
if (
|
||||
e.fn === "process.envGet" ||
|
||||
e.fn === "process.columns" ||
|
||||
e.fn === "process.rows" ||
|
||||
e.fn === "process.stdinIsRaw"
|
||||
) {
|
||||
// getenv(3) / ioctl(TIOCGWINSZ) / termios: the runtime answers a +1
|
||||
// string or NULL, or a number with a negative sentinel (the raw-mode
|
||||
// flag rides the same double channel, 1/0/-1); the union construction
|
||||
// is type-directed HERE — present wraps the value arm, absent yields
|
||||
// the interned immortal undefined-arm instance.
|
||||
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 undefTag = undefinedArmTag(e.type, host.unionsById);
|
||||
const isEnv = e.fn === "process.envGet";
|
||||
const valTag = def ? def.arms.findIndex((a) => a.kind === (isEnv ? "string" : "f64")) : -1;
|
||||
const isRaw = e.fn === "process.stdinIsRaw";
|
||||
const valTag = def
|
||||
? def.arms.findIndex((a) => a.kind === (isEnv ? "string" : isRaw ? "bool" : "f64"))
|
||||
: -1;
|
||||
if (valTag < 0 || undefTag < 0)
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} union lacks its arms`);
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
@@ -719,6 +733,10 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
host.declare(`declare ptr @scr_env_get(ptr)`);
|
||||
B.line(`${raw} = call ptr @scr_env_get(ptr ${args[0]!.name})`);
|
||||
B.line(`${present} = icmp ne ptr ${raw}, null`);
|
||||
} else if (isRaw) {
|
||||
host.declare(`declare double @scr_process_stdin_is_raw()`);
|
||||
B.line(`${raw} = call double @scr_process_stdin_is_raw()`);
|
||||
B.line(`${present} = fcmp oge double ${raw}, ${f64Lit(0)}`);
|
||||
} else {
|
||||
const runtimeFn = e.fn === "process.rows" ? "scr_process_rows" : "scr_process_columns";
|
||||
host.declare(`declare double @${runtimeFn}(double)`);
|
||||
@@ -727,9 +745,18 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
}
|
||||
B.condBr(present, lp, la);
|
||||
B.startBlock(lp);
|
||||
B.line(
|
||||
`store ptr ${host.unionNewOwned(e.type.unionId, valTag, { name: raw, type: isEnv ? STRING : F64 })}, ptr ${slot}`,
|
||||
);
|
||||
if (isRaw) {
|
||||
// The raw flag: 1/0 becomes the bool arm's i1.
|
||||
const flag = B.tmp();
|
||||
B.line(`${flag} = fcmp one double ${raw}, ${f64Lit(0)}`);
|
||||
B.line(
|
||||
`store ptr ${host.unionNewOwned(e.type.unionId, valTag, { name: flag, type: BOOL })}, ptr ${slot}`,
|
||||
);
|
||||
} else {
|
||||
B.line(
|
||||
`store ptr ${host.unionNewOwned(e.type.unionId, valTag, { name: raw, type: isEnv ? STRING : F64 })}, ptr ${slot}`,
|
||||
);
|
||||
}
|
||||
B.br(lj);
|
||||
B.startBlock(la);
|
||||
B.line(`store ptr ${host.unitInstanceRef(e.type.unionId, undefTag)}, ptr ${slot}`);
|
||||
@@ -811,10 +838,19 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
const kind = host.emitExpr(e.args[0]!);
|
||||
const code = host.emitExpr(e.args[1]!);
|
||||
const msg = host.emitExpr(e.args[2]!);
|
||||
host.declare(`declare void @scr_throw_node_coded(double, ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_throw_node_coded(double ${kind.name}, ptr ${code.name}, ptr ${msg.name})`,
|
||||
);
|
||||
if (e.args.length > 3) {
|
||||
// The fourth argument: the error's own `url` property.
|
||||
const url = host.emitExpr(e.args[3]!);
|
||||
host.declare(`declare void @scr_throw_node_coded_url(double, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_throw_node_coded_url(double ${kind.name}, ptr ${code.name}, ptr ${msg.name}, ptr ${url.name})`,
|
||||
);
|
||||
} else {
|
||||
host.declare(`declare void @scr_throw_node_coded(double, ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_throw_node_coded(double ${kind.name}, ptr ${code.name}, ptr ${msg.name})`,
|
||||
);
|
||||
}
|
||||
const ty = host.llType(e.type);
|
||||
if (ty === "void") {
|
||||
host.emitAlwaysPendingUnwind();
|
||||
@@ -867,6 +903,10 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
const cbT = e.args[2]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: emitter.on listener not a func");
|
||||
// A registration past the emitter's maxListeners queues the leak
|
||||
// warning for the next tick (scr_events_emitter.c), which the loop
|
||||
// delivers before the process exits.
|
||||
host.usesTimers = true;
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
const { fn: adapterFn, shim } = host.emitterFixedAdapter(cbT);
|
||||
// The wrapper's capture box owns its OWN +1 of the listener; the
|
||||
@@ -897,6 +937,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
const adT = e.args[3]!.type;
|
||||
if (adT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: emitter.onDyn adapter not a func");
|
||||
host.usesTimers = true; // the leak warning's tick (emitter.on)
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
const { fn: adapterFn, shim } = host.emitterFixedAdapter(adT);
|
||||
host.toHeap(args[3]!); // the frame's +1 moves into the wrapper's box
|
||||
@@ -946,6 +987,34 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
host.emitPendingCheck();
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "emitter.emitErrorValue") {
|
||||
// (recv, ...payload as dyn): emit('error') with a non-Error payload.
|
||||
// The unhandled case renders the payload with util.inspect
|
||||
// (scr_insp_dyn), which this call links.
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
const n = args.length - 1;
|
||||
let payload = "null";
|
||||
if (n > 0) {
|
||||
const arr = B.slot();
|
||||
B.entryAllocas.push(`${arr} = alloca [${n} x ptr]`);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const slot = B.tmp();
|
||||
B.line(`${slot} = getelementptr inbounds [${n} x ptr], ptr ${arr}, i64 0, i64 ${i}`);
|
||||
B.line(`store ptr ${args[i + 1]!.name}, ptr ${slot}`);
|
||||
}
|
||||
payload = arr;
|
||||
}
|
||||
host.declare(`declare ptr @scr_insp_dyn(ptr, double, double)`);
|
||||
host.declare(
|
||||
`declare zeroext i1 @scr_emitter_emit_error_value(ptr, ptr, ${host.sizeType}, ptr)`,
|
||||
);
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call i1 @scr_emitter_emit_error_value(ptr ${args[0]!.name}, ptr ${payload}, ${host.sizeType} ${n}, ptr @scr_insp_dyn)`,
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "emitter.emitFlex" || e.fn === "emitter.emitAny") {
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
const n = args.length - 2;
|
||||
@@ -1046,6 +1115,7 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
// emitter.on/onDyn, but the DATA adapter (the three-slot payload ABI
|
||||
// — scr_stream_emit_data) behind the arity-3 fixed shim.
|
||||
const isDyn = e.fn === "emitter.onDataDyn";
|
||||
host.usesTimers = true; // the leak warning's tick (emitter.on)
|
||||
const cbT = e.args[isDyn ? 3 : 2]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} listener not a func`);
|
||||
|
||||
@@ -67,6 +67,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"http.reqH2StreamOrThrow": "scr_http_req_h2_stream_or_throw",
|
||||
"intl.segmenterNew": "scr_intl_segmenter_new",
|
||||
"intl.segmenterRefusing": "scr_intl_segmenter_refusing",
|
||||
"icu.segmenterWord": "scr_intl_segmenter_word_new",
|
||||
"icu.numFormatDefault": "scr_intl_num_format_default",
|
||||
"icu.i18nSupport": "scr_intl_i18n_support",
|
||||
"icu.dateLocaleString": "scr_intl_date_locale_string",
|
||||
"icu.dateLocaleTimeString": "scr_intl_date_locale_time_string",
|
||||
"weakMap.is": "scr_weak_map_is",
|
||||
"weakSet.is": "scr_weak_set_is",
|
||||
"weakMap.new": "scr_weak_map_new",
|
||||
@@ -261,6 +266,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"os.userName": "scr_os_user_name",
|
||||
"os.userShell": "scr_os_user_shell",
|
||||
"os.userHomedir": "scr_os_user_homedir",
|
||||
"os.userUid": "scr_os_user_uid",
|
||||
"os.userGid": "scr_os_user_gid",
|
||||
"os.tmpdir": "scr_os_tmpdir",
|
||||
"process.argv": "scr_process_argv",
|
||||
"process.execArgv": "scr_process_exec_argv",
|
||||
@@ -304,9 +311,19 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"navigator.languages": "scr_navigator_languages",
|
||||
"navigator.platform": "scr_navigator_platform",
|
||||
"process.builtinModule": "scr_process_builtin_module",
|
||||
"process.builtinObject": "scr_process_builtin_object",
|
||||
"process.builtinUnsupported": "scr_process_builtin_unsupported",
|
||||
"module.namespaceValue": "scr_module_namespace_value",
|
||||
"module.namespaceDyn": "scr_module_namespace_dyn_value",
|
||||
"wrapper.new": "scr_js_wrapper_new",
|
||||
"wrapper.toObject": "scr_js_wrapper_to_object",
|
||||
"wrapper.instanceOf": "scr_js_wrapper_instance_of",
|
||||
"wrapper.objectTag": "scr_js_wrapper_object_tag",
|
||||
"wrapper.typeIs": "scr_js_wrapper_type_is",
|
||||
"wrapper.guard": "scr_js_wrapper_guard",
|
||||
"wrapper.guardList": "scr_js_wrapper_guard_list",
|
||||
"wrapper.isStringMethod": "scr_js_wrapper_is_string_method",
|
||||
"wrapper.thisString": "scr_js_wrapper_this_string",
|
||||
"fs.callbackValue": "scr_fs_callback_value",
|
||||
"fs.callbackCall": "scr_fs_callback_call",
|
||||
"process.hrtimeValue": "scr_process_hrtime_value",
|
||||
@@ -379,9 +396,14 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"process.envPairs": "scr_env_pairs",
|
||||
"process.stdoutWrite": "scr_process_stdout_write",
|
||||
"process.stdio": "scr_process_stdio",
|
||||
"process.stdioSlot": "scr_process_stdio",
|
||||
"process.stdioWriteDyn": "scr_process_stdio_write_dyn",
|
||||
"process.stderrWrite": "scr_process_stderr_write",
|
||||
"process.isTTY": "scr_process_is_tty",
|
||||
"process.isTTYValue": "scr_process_is_tty_value",
|
||||
// A promise that never settles (events.once of an event a synchronous
|
||||
// stream never emits); a fresh pending promise is its owned result.
|
||||
"events.pendingForever": "scr_promise_new",
|
||||
"date.now": "scr_date_now",
|
||||
"date.newNow": "scr_date_now",
|
||||
"date.newMs": "scr_date_new_ms",
|
||||
@@ -441,10 +463,17 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"fsp.globError": "scr_fsp_glob_error",
|
||||
"fs.realpathSync": "scr_fs_realpath",
|
||||
"fs.realpathValue": "scr_fs_realpath_value",
|
||||
"dyn.validateString": "scr_value_string",
|
||||
"dyn.validateStrings": "scr_value_strings",
|
||||
"path.resolveValue": "scr_path_resolve_value",
|
||||
"path.parseValue": "scr_path_parse_value",
|
||||
"path.formatValue": "scr_path_format_value",
|
||||
"path.toNamespacedValue": "scr_path_to_namespaced_value",
|
||||
"fs.realpathNativeSync": "scr_fs_realpath_promise",
|
||||
"fs.unlinkSync": "scr_fs_unlink",
|
||||
"fs.chmodSync": "scr_fs_chmod",
|
||||
"fs.copyFileSync": "scr_fs_copyfile",
|
||||
"fs.copyFileFlagsSync": "scr_fs_copyfile_flags",
|
||||
"fs.renameSync": "scr_fs_rename",
|
||||
"fs.accessSync": "scr_fs_access",
|
||||
// node:assert (scr_assert.c): all args borrowed; failures throw the
|
||||
@@ -593,6 +622,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
// string_decoder (scr_bytes.c): stateless helpers over (enc, pending,
|
||||
// chunk) — never throw.
|
||||
"strdec.write": "scr_strdec_write",
|
||||
"strdec.writeDyn": "scr_strdec_write_dyn",
|
||||
"strdec.nextDyn": "scr_strdec_next_dyn",
|
||||
"strdec.next": "scr_strdec_next",
|
||||
"strdec.end": "scr_strdec_end",
|
||||
"strdec.encoding": "scr_strdec_encoding",
|
||||
@@ -868,6 +899,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
// identity); registration is special-cased above (ownership moves).
|
||||
"process.offSignal": "scr_signal_off",
|
||||
"process.offExit": "scr_process_off_exit",
|
||||
// Front-of-list registrations (process.prependListener): the signal and
|
||||
// uncaught-exception registries. prependExit is special-cased below with
|
||||
// the exit adapters, like onExit.
|
||||
"process.prependSignal": "scr_signal_prepend",
|
||||
"process.prependUncaughtException": "scr_process_prepend_uncaught_exception",
|
||||
// child_process (scr_child.c): spawnSync/exec forms are synchronous
|
||||
// (execSync/execCapture throw — the may-throw check); handle getters
|
||||
// are pure reads. Registration forms are special-cased (moves + per-
|
||||
@@ -908,6 +944,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"fsp.readFileBytes": "scr_fsp_read_file_bytes",
|
||||
"fsp.writeFile": "scr_fsp_write_file",
|
||||
"fsp.writeFileMode": "scr_fsp_write_file_mode",
|
||||
"fsp.writeFileFlags": "scr_fsp_write_file_flags",
|
||||
"fsp.copyFileFlags": "scr_fsp_copyfile_flags",
|
||||
"fsp.mkdir": "scr_fsp_mkdir",
|
||||
"fsp.mkdirMode": "scr_fsp_mkdir_mode",
|
||||
"fsp.mkdirRecursive": "scr_fsp_mkdir_recursive",
|
||||
@@ -1113,6 +1151,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.keyDelete": "scr_dyn_key_delete",
|
||||
"dyn.keyDeleteComputed": "scr_dyn_key_delete_computed",
|
||||
"dyn.reflectDeleteProperty": "scr_dyn_reflect_delete_property",
|
||||
"dyn.reflectHasTarget": "scr_dyn_reflect_has_target",
|
||||
"dyn.hasKeyComputed": "scr_dyn_has_key_computed",
|
||||
"dyn.hasOwnComputed": "scr_dyn_has_own_computed",
|
||||
"dyn.propertyIsEnumerableComputed": "scr_dyn_property_is_enumerable_computed",
|
||||
@@ -1262,6 +1301,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"crypto.x509Verify": "scr_crypto_x509_verify",
|
||||
"crypto.x509CheckPrivateKey": "scr_crypto_x509_check_private_key",
|
||||
"fs.writeFileModeSync": "scr_fs_write_file_mode",
|
||||
"fs.writeFileFlagsSync": "scr_fs_write_file_flags",
|
||||
"fs.chownSync": "scr_fs_chown",
|
||||
"fs.lchownSync": "scr_fs_lchown",
|
||||
"fs.fchownSync": "scr_fs_fchown",
|
||||
@@ -1450,6 +1490,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"net.serverListening": "scr_net_server_listening",
|
||||
"net.sockSetRef": "scr_net_sock_set_ref",
|
||||
"net.serverSetRef": "scr_net_server_set_ref",
|
||||
"net.serverOff": "scr_net_server_off",
|
||||
"net.sockWrite": "scr_net_sock_write_str",
|
||||
"net.sockWriteBytes": "scr_net_sock_write_bytes",
|
||||
"net.sockEnd": "scr_net_sock_end",
|
||||
@@ -1896,6 +1937,11 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"worker.receiveMessageOnPort",
|
||||
"worker.broadcastChannel",
|
||||
"process.stdio",
|
||||
// The typed write slot and the run-time-typed chunk write keep the loop
|
||||
// live for the write callbacks they queue: both reach the same stream
|
||||
// object as process.stdio and call back from a nextTick.
|
||||
"process.stdioSlot",
|
||||
"process.stdioWriteDyn",
|
||||
"fetch.start",
|
||||
"fetch.input",
|
||||
"fetch.function",
|
||||
@@ -1981,6 +2027,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"writable.writeU",
|
||||
"writable.writeDyn",
|
||||
"writable.writeEnc",
|
||||
"writable.writeEcb",
|
||||
"writable.end",
|
||||
"writable.uncork",
|
||||
"stream.destroy",
|
||||
|
||||
@@ -3,6 +3,8 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { pipelinePumpedRuns } from "../../ir/builtin-signatures.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { streamTypedDesc } from "./expr-stream-bridges.js";
|
||||
|
||||
const STREAM_CANONICAL_CBS: Record<string, ("r" | "w" | "f" | "d" | "t" | "l")[]> = {
|
||||
readable: ["r", "d"],
|
||||
@@ -14,6 +16,10 @@ const STREAM_CANONICAL_CBS: Record<string, ("r" | "w" | "f" | "d" | "t" | "l")[]
|
||||
|
||||
export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
if (e.fn === "readable.pushDyn" || e.fn === "writable.writeDyn") {
|
||||
const typed = emitTypedChunkCall(host, e);
|
||||
if (typed !== null) return typed;
|
||||
}
|
||||
if (e.fn === "stream.addAbortSignal" || e.fn === "stream.addAbortSignalFn") {
|
||||
// stream.addAbortSignal: [stream, signal] — the constructors' signal
|
||||
// option chained over the fresh stream (a falsy signal is ignored);
|
||||
@@ -455,6 +461,44 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
host.emitPendingCheck();
|
||||
return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "writable.writeEcb") {
|
||||
// (recv, mode, chunk, encoding, cb): mode 0 bytes, 1 string, 2 dyn. The
|
||||
// callback MOVES into an error-first wrapper (scr_stream_ecb_wrap); the
|
||||
// adapter turns the runtime's ScrError (NULL = success) into the
|
||||
// callback's Error | null argument, as fs.rename callbacks do.
|
||||
const modeArg = e.args[1]!;
|
||||
if (modeArg.kind !== "numLit")
|
||||
throw new InternalCompilerError("llvm emitter bug: writable.writeEcb mode not a literal");
|
||||
const cbT = e.args[4]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: writable.writeEcb callback not a func");
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
host.toHeap(args[4]!);
|
||||
const adapter = host.fsRenameThunkFor(cbT);
|
||||
host.declare(`declare ptr @scr_stream_ecb_wrap(ptr, ptr)`);
|
||||
const wrapped = B.tmp();
|
||||
B.line(`${wrapped} = call ptr @scr_stream_ecb_wrap(ptr ${args[4]!.name}, ptr @${adapter})`);
|
||||
const entry =
|
||||
modeArg.value === 0
|
||||
? "scr_stream_write"
|
||||
: modeArg.value === 1
|
||||
? "scr_stream_write_str"
|
||||
: null;
|
||||
const t = B.tmp();
|
||||
if (entry !== null) {
|
||||
host.declare(`declare zeroext i1 @${entry}(ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`${t} = call zeroext i1 @${entry}(ptr ${args[0]!.name}, ptr ${args[2]!.name}, ptr ${wrapped})`,
|
||||
);
|
||||
} else {
|
||||
host.declare(`declare zeroext i1 @scr_stream_write_enc(ptr, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`${t} = call zeroext i1 @scr_stream_write_enc(ptr ${args[0]!.name}, ptr ${args[2]!.name}, ptr ${args[3]!.name}, ptr ${wrapped})`,
|
||||
);
|
||||
}
|
||||
host.emitPendingCheck();
|
||||
return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "writable.end") {
|
||||
// (recv, flags[, chunk][, encoding][, cb]) — flags: 1 bytes chunk, 2
|
||||
// string chunk, 8 dyn chunk (write first, Node's end(chunk)
|
||||
@@ -834,6 +878,34 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
return host.emitGenericLibCall(e);
|
||||
}
|
||||
|
||||
/** push(value) or write(value) of a record that converts to dyn by
|
||||
* reference: the stream receives the record as a typed chunk
|
||||
* (scr_stream_push_typed / scr_stream_write_typed), boxed only for a
|
||||
* consumer that reads it as a dyn value. Null for another chunk. */
|
||||
function emitTypedChunkCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue | null {
|
||||
const chunk = e.args[1];
|
||||
if (chunk === undefined || chunk.kind !== "dynFrom" || !chunk.liveRef) return null;
|
||||
const desc = streamTypedDesc(host, chunk.value.type);
|
||||
if (desc === null) return null;
|
||||
const B = host.B;
|
||||
const recv = host.emitExpr(e.args[0]!);
|
||||
const rec = emitBorrowedInput(host, chunk.value);
|
||||
const t = B.tmp();
|
||||
if (e.fn === "readable.pushDyn") {
|
||||
host.declare(`declare zeroext i1 @scr_stream_push_typed(ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`${t} = call zeroext i1 @scr_stream_push_typed(ptr ${recv.name}, ptr ${rec.name}, ptr ${desc})`,
|
||||
);
|
||||
} else {
|
||||
host.declare(`declare zeroext i1 @scr_stream_write_typed(ptr, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`${t} = call zeroext i1 @scr_stream_write_typed(ptr ${recv.name}, ptr ${rec.name}, ptr ${desc}, ptr null)`,
|
||||
);
|
||||
}
|
||||
host.emitPendingCheck();
|
||||
return e.type.kind === "void" ? { name: "", type: e.type } : { name: t, type: e.type };
|
||||
}
|
||||
|
||||
/** The encoding argument a write passes when the call has none: undefined
|
||||
* (a null pointer), or the null value when a callback took its place. */
|
||||
function writeEncNull(host: LlvmEmitterContext, which: "null" | "undef"): string {
|
||||
|
||||
@@ -3,11 +3,19 @@
|
||||
* local arrays they rely on. */
|
||||
import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js";
|
||||
import type { IrFunction, IrStmt } from "../../ir/ir.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import { findInitializedByteLoopBindings } from "./byte-windows.js";
|
||||
import type { DenseArrayLoop } from "./dense-array-loops.js";
|
||||
import type { LlEmitter } from "./emitter.js";
|
||||
import { defineModuleState } from "./emitter-state.js";
|
||||
|
||||
/** The most IR nodes a function may have for its in-loop number-key
|
||||
* lookups to inline the hashed probe (emitNumberMapEntry). Each inlined
|
||||
* probe adds code to its function, and LLVM's passes over a large function
|
||||
* take longer for it; a small function's loop of lookups, such as a k-mer
|
||||
* count, gains the most from it. */
|
||||
const MAX_INLINED_LOOKUP_NODES = 200;
|
||||
|
||||
/** Sets the loop state for `fn` at the start of its emission; `numericFn`
|
||||
* is `fn` with its initializer bindings (emitFunction). */
|
||||
export function beginFunctionLoops(
|
||||
@@ -19,10 +27,11 @@ export function beginFunctionLoops(
|
||||
loops.unaliasedArrays = findUnaliasedLocalArrays(fn);
|
||||
loops.countedLoopsEnabled = emitter.debug === null && !fn.async && !fn.generator;
|
||||
loops.byteWindowsEnabled = loops.countedLoopsEnabled;
|
||||
loops.bytesLoopVersionsEnabled = loops.countedLoopsEnabled;
|
||||
loops.plainLoops = fn.plainLoops === true;
|
||||
loops.bytesLoopVersionsEnabled = loops.countedLoopsEnabled && !loops.plainLoops;
|
||||
loops.bytesLoopEntries = null;
|
||||
loops.bytesLoopFunction = fn;
|
||||
loops.denseLoopsEnabled = loops.countedLoopsEnabled;
|
||||
loops.denseLoopsEnabled = loops.countedLoopsEnabled && !loops.plainLoops;
|
||||
loops.denseLoop = null;
|
||||
loops.denseLoopEntries =
|
||||
loops.denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === "array")
|
||||
@@ -32,6 +41,15 @@ export function beginFunctionLoops(
|
||||
loops.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes")
|
||||
? findInitializedByteLoopBindings(fn)
|
||||
: new Map();
|
||||
loops.inlinedLookups = fewNodes(fn.body, MAX_INLINED_LOOKUP_NODES);
|
||||
}
|
||||
|
||||
/** Whether `body` has at most `max` statements and expressions. */
|
||||
function fewNodes(body: IrStmt[], max: number): boolean {
|
||||
let count = 0;
|
||||
const visit = (): boolean => ++count <= max;
|
||||
everyStmtList(body, { expr: visit, stmt: visit });
|
||||
return count <= max;
|
||||
}
|
||||
|
||||
/** The state LOOPS holds on each emitter. */
|
||||
@@ -48,8 +66,11 @@ export interface LoopState {
|
||||
byteWindowsEnabled: boolean;
|
||||
byteWindowEntries: ReturnType<typeof findInitializedByteLoopBindings>;
|
||||
/** Typed-array loop versioning (bytes-loop-versions.ts) for the current
|
||||
* function: off in debug and suspending functions and inside a copy. */
|
||||
* function: off in debug and suspending functions, in a function that
|
||||
* emits each loop once and inside a copy. */
|
||||
bytesLoopVersionsEnabled: boolean;
|
||||
/** The current function emits each loop once (IrFunction.plainLoops). */
|
||||
plainLoops: boolean;
|
||||
/** Loops being versioned around the statement being emitted: a loop is
|
||||
* never versioned inside its own copies, and versions nest boundedly. */
|
||||
readonly bytesLoopVersioning: Set<IrStmt>;
|
||||
@@ -58,6 +79,9 @@ export interface LoopState {
|
||||
/** Counted loops whose integer-form safety test an enclosing version
|
||||
* guard already passed. */
|
||||
readonly provenCountedGuards: Set<IrStmt>;
|
||||
/** Number-key lookups in this function's loops inline the hashed probe:
|
||||
* the function has at most MAX_INLINED_LOOKUP_NODES nodes. */
|
||||
inlinedLookups: boolean;
|
||||
}
|
||||
|
||||
export const LOOPS = defineModuleState<LoopState>(() => ({
|
||||
@@ -69,8 +93,10 @@ export const LOOPS = defineModuleState<LoopState>(() => ({
|
||||
byteWindowsEnabled: true,
|
||||
byteWindowEntries: new Map(),
|
||||
bytesLoopVersionsEnabled: false,
|
||||
plainLoops: false,
|
||||
bytesLoopVersioning: new Set<IrStmt>(),
|
||||
bytesLoopEntries: null,
|
||||
bytesLoopFunction: null,
|
||||
provenCountedGuards: new Set<IrStmt>(),
|
||||
inlinedLookups: false,
|
||||
}));
|
||||
|
||||
@@ -117,8 +117,9 @@ test("string keys and reference values are retained inline, boolean slots compar
|
||||
expect(text).toContain("call ptr @scr_map_iter_val_ref(");
|
||||
});
|
||||
|
||||
/** work(map, k): map.has(k) on a number-keyed map. */
|
||||
function lookupFixture(key: IrExpr = ref("k", F64)): IrModule {
|
||||
/** work(map, k): map.has(k) on a number-keyed map, or with `loop`, the
|
||||
* condition of `while (map.has(k)) {}`, after `filler` statements `k;`. */
|
||||
function lookupFixture(key: IrExpr = ref("k", F64), loop = false, filler = 0): IrModule {
|
||||
const map: IrType = { kind: "map", key: F64, value: F64 };
|
||||
const params = [
|
||||
{ localId: "map", name: "map", type: map },
|
||||
@@ -138,7 +139,17 @@ function lookupFixture(key: IrExpr = ref("k", F64)): IrModule {
|
||||
returnType: BOOL,
|
||||
loc,
|
||||
locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: true })),
|
||||
body: [{ kind: "return", value: has, loc }],
|
||||
body: loop
|
||||
? [
|
||||
...Array.from({ length: filler }, (): IrStmt => ({
|
||||
kind: "exprStmt",
|
||||
expr: ref("k", F64),
|
||||
loc,
|
||||
})),
|
||||
{ kind: "while", cond: has, body: [], loc },
|
||||
{ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc },
|
||||
]
|
||||
: [{ kind: "return", value: has, loc }],
|
||||
};
|
||||
return {
|
||||
irVersion: 17,
|
||||
@@ -159,6 +170,10 @@ test("hashed number-key lookups probe inline and call the runtime only for wide
|
||||
module,
|
||||
)![0];
|
||||
expect(helper).toContain("call ptr @sc_map_entry_f64_hashed(ptr %m, double %k)");
|
||||
// A key outside the direct index goes to the probe, whose small-map
|
||||
// branch tells a map with the index (field 16) from one without it.
|
||||
expect(helper).toMatch(/br i1 %ix\d+, label %slot, label %slow\n/);
|
||||
expect(helper).not.toContain("dense.any");
|
||||
const hashed =
|
||||
/^define internal ptr @sc_map_entry_f64_hashed\(ptr %m, double %k\) noinline [^]*?^}/m.exec(
|
||||
module,
|
||||
@@ -171,7 +186,10 @@ test("hashed number-key lookups probe inline and call the runtime only for wide
|
||||
// Filter (field 10) before the small-map scan of the entry count (field
|
||||
// 8) only, and compact buckets (fields 13 and 14) tagged with the hash's
|
||||
// top byte.
|
||||
expect(hashed).toContain("br i1 %linear, label %filter, label %buckets");
|
||||
expect(hashed).toContain("br i1 %linear, label %unbucketed, label %buckets");
|
||||
expect(hashed).toMatch(
|
||||
/^unbucketed:\n[^]*i32 0, i32 16\n[^]*br i1 %indexed, label %miss, label %filter\n/m,
|
||||
);
|
||||
expect(hashed).toContain("getelementptr inbounds %ScrMapIx, ptr %m, i32 0, i32 10");
|
||||
expect(hashed).toContain(`load i32, ptr %nep`);
|
||||
expect(hashed).toContain(`load i32, ptr %nbp`);
|
||||
@@ -194,22 +212,27 @@ test("a key with an integer view tests the direct index on the integer", () => {
|
||||
integral.module,
|
||||
)![0];
|
||||
// An unsigned compare also rejects negative keys; the hashed probe takes
|
||||
// the integer.
|
||||
// the integer and its double.
|
||||
expect(helper).toContain("icmp ult i64 %ki, %nd");
|
||||
expect(helper).toContain(`getelementptr inbounds i32, ptr %dense, ${sz} %i`);
|
||||
expect(helper).toContain("call ptr @sc_map_entry_f64_hashed_int(ptr %m, i64 %ki)");
|
||||
expect(helper).toContain("call ptr @sc_map_entry_f64_hashed_int(ptr %m, i64 %ki, double %k)");
|
||||
expect(helper).toContain("br i1 %inside, label %slot, label %slow");
|
||||
expect(helper).not.toContain("fptosi");
|
||||
// Its key word is the integer's double, which needs no NaN or -0
|
||||
// normalization; the runtime lookup gets that double.
|
||||
// Its key word is the bits of the integer's double, which needs no NaN
|
||||
// normalization and is +0 for the integer 0; the runtime lookup gets
|
||||
// that double. The probe converts nothing itself (cvtsi2sd would wait
|
||||
// for its register's last value).
|
||||
const hashed =
|
||||
/^define internal ptr @sc_map_entry_f64_hashed_int\(ptr %m, i64 %ki\) noinline [^]*?^}/m.exec(
|
||||
/^define internal ptr @sc_map_entry_f64_hashed_int\(ptr %m, i64 %ki, double %k\) noinline [^]*?^}/m.exec(
|
||||
integral.module,
|
||||
)![0];
|
||||
expect(hashed).toContain("sitofp i64 %ki to double");
|
||||
expect(hashed).toContain("bitcast double %k to i64");
|
||||
expect(hashed).toContain("select i1 %kzero, i64 0, i64 %kbits");
|
||||
expect(hashed).not.toContain("sitofp");
|
||||
expect(hashed).not.toContain("fcmp");
|
||||
expect(hashed).toContain("mul i64 %x1, -4658895280553007687");
|
||||
expect(hashed).toContain(
|
||||
"call ptr @scr_map_entry_f64(ptr %m, double %kf) memory(read) nounwind willreturn",
|
||||
"call ptr @scr_map_entry_f64(ptr %m, double %k) memory(read) nounwind willreturn",
|
||||
);
|
||||
// A fractional key and a key without a proof keep the double lookup.
|
||||
for (const key of [{ kind: "numLit", value: 0.5, type: F64, loc } as IrExpr, ref("k", F64)]) {
|
||||
@@ -220,6 +243,36 @@ test("a key with an integer view tests the direct index on the integer", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("a lookup inside a loop calls the flavor that inlines the hashed probe", () => {
|
||||
for (const key of [ref("k", F64), { kind: "numLit", value: 12, type: F64, loc } as IrExpr]) {
|
||||
const { work, module } = emitted(lookupFixture(key, true), 64);
|
||||
const name = key.kind === "numLit" ? "sc_map_entry_f64_int" : "sc_map_entry_f64";
|
||||
expect(work).toContain(`call ptr @${name}.loop(`);
|
||||
expect(work).not.toContain(`call ptr @${name}(`);
|
||||
// They only read memory and always return, so a has() and get() of one
|
||||
// key merge before they are inlined.
|
||||
const helper = new RegExp(
|
||||
`^define internal ptr @${name}\\.loop\\([^)]*\\) alwaysinline memory\\(read\\) nounwind willreturn [^]*?^}`,
|
||||
"m",
|
||||
).exec(module)![0];
|
||||
const hashed =
|
||||
key.kind === "numLit" ? "sc_map_entry_f64_hashed_int" : "sc_map_entry_f64_hashed";
|
||||
expect(helper).toContain(`call ptr @${hashed}.loop(`);
|
||||
expect(module).toMatch(
|
||||
new RegExp(
|
||||
`^define internal ptr @${hashed}\\.loop\\([^)]*\\) alwaysinline memory\\(read\\) nounwind willreturn `,
|
||||
"m",
|
||||
),
|
||||
);
|
||||
// A lookup outside a loop keeps the out-of-line probe, and so does a
|
||||
// lookup in a loop of a function of more than 200 nodes.
|
||||
expect(emitted(lookupFixture(key), 64).work).toContain(`call ptr @${name}(`);
|
||||
const large = emitted(lookupFixture(key, true, 100), 64).work;
|
||||
expect(large).toContain(`call ptr @${name}(`);
|
||||
expect(large).not.toContain(".loop(");
|
||||
}
|
||||
});
|
||||
|
||||
test("a key view converted in a branch is not reused by a later lookup of the same constant", () => {
|
||||
// work(map, k, taken): if (taken) return map.has((k = 2 ** 40)); return map.has(2 ** 40).
|
||||
const wide = 2 ** 40;
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
import type { IrExpr, IrType } from "../../ir/ir.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { LOOPS } from "./loop-state.js";
|
||||
import { mapKeyAccess } from "./shapes.js";
|
||||
|
||||
export const NUMBER_MAP_ENTRY_DECL = "declare ptr @scr_map_entry_f64(ptr, double)";
|
||||
@@ -55,19 +56,22 @@ export function noteNumberMapKey(host: LlvmEmitterContext, key: LlValue, expr: I
|
||||
|
||||
/** The entry for `key`, or null when absent. Valid until the next mutation.
|
||||
* A key with an integer view (noteNumberMapKey) tests the direct index on
|
||||
* that integer instead of converting the double back. */
|
||||
* that integer instead of converting the double back. A lookup inside a
|
||||
* loop of a small function (LOOPS.inlinedLookups) calls the helpers' `.loop`
|
||||
* flavor, which inlines the hashed probe. */
|
||||
export function emitNumberMapEntry(host: LlvmEmitterContext, map: string, key: string): string {
|
||||
host.declare(NUMBER_MAP_ENTRY_DECL);
|
||||
const entry = host.B.tmp();
|
||||
const flavor = host.loopDepth > 0 && LOOPS.of(host).inlinedLookups ? ".loop" : "";
|
||||
const integer = host.numberMapKeyIntegers.get(key);
|
||||
if (integer) {
|
||||
const wide = widenInteger(host, integer);
|
||||
host.B.line(
|
||||
`${entry} = call ptr @sc_map_entry_f64_int(ptr ${map}, i64 ${wide}, double ${key})`,
|
||||
`${entry} = call ptr @sc_map_entry_f64_int${flavor}(ptr ${map}, i64 ${wide}, double ${key})`,
|
||||
);
|
||||
return entry;
|
||||
}
|
||||
host.B.line(`${entry} = call ptr @sc_map_entry_f64(ptr ${map}, double ${key})`);
|
||||
host.B.line(`${entry} = call ptr @sc_map_entry_f64${flavor}(ptr ${map}, double ${key})`);
|
||||
return entry;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../../index.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
|
||||
async function llvmFor(source: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-receiver-brackets-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts"),
|
||||
output = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
|
||||
return emitLlvmModule(deserializeModule(await readFile(output, "utf8")));
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function body(ir: string, name: string): string {
|
||||
const match = new RegExp(`^define [^\\n]*@${name}\\(.*?^}`, "ms").exec(ir);
|
||||
if (!match) throw new Error(`no function ${name}`);
|
||||
return match[0];
|
||||
}
|
||||
|
||||
test("a known signature adapter's direct entry brackets the receiver only when its original reads it", async () => {
|
||||
const ir = await llvmFor(`
|
||||
class Node {
|
||||
kids: Node[] = [];
|
||||
constructor(readonly kind: number) {}
|
||||
}
|
||||
function walk(root: Node, cb: (node: Node, depth: number) => void | "skip" | "stop"): void {
|
||||
const stack: Node[] = [root];
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
const verdict = cb(node, stack.length);
|
||||
if (verdict === "stop") return;
|
||||
if (verdict === "skip") continue;
|
||||
for (const kid of node.kids) stack.push(kid);
|
||||
}
|
||||
}
|
||||
class Counter {
|
||||
count = 0;
|
||||
run(root: Node): number {
|
||||
walk(root, (node) => {
|
||||
if (node.kind === 3) this.count++;
|
||||
});
|
||||
return this.count;
|
||||
}
|
||||
}
|
||||
const root = new Node(0);
|
||||
root.kids.push(new Node(3));
|
||||
console.log(new Counter().run(root));
|
||||
`);
|
||||
const walk = body(ir, "sc_bf_walk");
|
||||
const blocks = walk.split(/^(?=[\w.]+:$)/m);
|
||||
const direct = blocks.find((block) => block.startsWith("callback.direct"));
|
||||
// The adapter forwards the receiver to the arrow function, which never
|
||||
// reads it: the direct entry tests the closure's RECEIVER_FREE bit, and
|
||||
// every receiver push is behind such a test.
|
||||
expect(direct).toMatch(/ = and i32 %\S+, 16\n/);
|
||||
const pushes = blocks.filter((block) => block.includes("@scr_dyn_this_push_dyn"));
|
||||
expect(pushes.length).toBeGreaterThan(0);
|
||||
for (const block of pushes) expect(block).toMatch(/^receiver\.push\d+:/);
|
||||
});
|
||||
@@ -3,8 +3,11 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../../index.js";
|
||||
import type { IrModule } from "../../ir/ir.js";
|
||||
import { DYN } from "../../ir/ir.js";
|
||||
import type { IrExpr, IrModule } from "../../ir/ir.js";
|
||||
import { computeTraced } from "../cycle-analysis.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
import { planRecordViews } from "./record-views.js";
|
||||
|
||||
/** The views planned for `source`'s module, as target field names → source
|
||||
@@ -128,3 +131,98 @@ ${conversions}
|
||||
Object.freeze(wide);`);
|
||||
expect(views).toEqual(new Map());
|
||||
});
|
||||
|
||||
/** `source`'s module shaped the way the frontend lowers a partial
|
||||
* assertion (C2): the checked cast of the source record literal carries
|
||||
* `partialView` over a `dynFrom{liveRef}` of that literal. */
|
||||
async function partialModule(
|
||||
source: string,
|
||||
sourceFields: string,
|
||||
targetFields: string,
|
||||
): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-record-views-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts"),
|
||||
outPath = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
|
||||
const mod: IrModule = deserializeModule(await readFile(outPath, "utf8"));
|
||||
const idOf = (fields: string): string =>
|
||||
(mod.records ?? []).find((shape) => shape.fields.map((f) => f.name).join(",") === fields)!.id;
|
||||
const exprs: IrExpr[] = [];
|
||||
for (const fn of mod.functions)
|
||||
everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (e) => {
|
||||
exprs.push(e);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const lit = exprs.find(
|
||||
(e) =>
|
||||
e.kind === "recordLit" && e.type.kind === "record" && e.type.shapeId === idOf(sourceFields),
|
||||
);
|
||||
const check = exprs.find(
|
||||
(e) =>
|
||||
e.kind === "dynCheck" && e.type.kind === "record" && e.type.shapeId === idOf(targetFields),
|
||||
);
|
||||
if (!lit || !check || check.kind !== "dynCheck") throw new Error("shapes not found");
|
||||
check.partialView = true;
|
||||
check.value = { kind: "dynFrom", liveRef: true, value: lit, type: DYN, loc: check.loc };
|
||||
return mod;
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test("a partial assertion plans a partial view with an absent-field sentinel", async () => {
|
||||
const mod = await partialModule(
|
||||
`
|
||||
type Wide = { a: number; b: string };
|
||||
type Narrow = { a: number; z: boolean };
|
||||
const wide: Wide = { a: 1, b: "x" };
|
||||
const o: unknown = wide;
|
||||
const v = o as Narrow;
|
||||
console.log(v.a);
|
||||
`,
|
||||
"a,b",
|
||||
"a,z",
|
||||
);
|
||||
const idOf = (fields: string): string =>
|
||||
(mod.records ?? []).find((shape) => shape.fields.map((f) => f.name).join(",") === fields)!.id;
|
||||
const target = idOf("a,z");
|
||||
const source = idOf("a,b");
|
||||
const views = planRecordViews(mod, { traced: () => false, excluded: () => false });
|
||||
expect(views.targets.has(target)).toBe(true);
|
||||
expect(views.sources(target)).toEqual([source]);
|
||||
expect(views.partial(target, source)).toBe(true);
|
||||
expect(views.partialTargets.has(target)).toBe(true);
|
||||
expect(views.partialMissing(target, "z")).toBe(true);
|
||||
expect(views.partialMissing(target, "a")).toBe(false);
|
||||
// The offset table marks the target field the source does not declare
|
||||
// with the all-ones sentinel.
|
||||
const llvm = emitLlvmModule(mod);
|
||||
expect(llvm).toMatch(/@sc_rv_\d+_0 = internal constant \[2 x i64\] \[i64 ptrtoint .*, i64 -1\]/);
|
||||
});
|
||||
|
||||
test("a full source keeps an ordinary view, not a partial one", async () => {
|
||||
const mod = await partialModule(
|
||||
`
|
||||
type Wide = { a: number; b: string };
|
||||
type Narrow = { a: number };
|
||||
const wide: Wide = { a: 1, b: "x" };
|
||||
const o: unknown = wide;
|
||||
const v = o as Narrow;
|
||||
console.log(v.a);
|
||||
`,
|
||||
"a,b",
|
||||
"a",
|
||||
);
|
||||
const idOf = (fields: string): string =>
|
||||
(mod.records ?? []).find((shape) => shape.fields.map((f) => f.name).join(",") === fields)!.id;
|
||||
const views = planRecordViews(mod, { traced: () => false, excluded: () => false });
|
||||
expect(views.sources(idOf("a"))).toEqual([idOf("a,b")]);
|
||||
expect(views.partial(idOf("a"), idOf("a,b"))).toBe(false);
|
||||
expect(views.partialTargets.size).toBe(0);
|
||||
});
|
||||
|
||||
@@ -15,7 +15,15 @@
|
||||
* checked conversion can produce (the closure of dynCheck targets), and
|
||||
* some other record type whose values reach untyped code as live capsules
|
||||
* (the closure of live dynFrom types) has all of B's fields. A program
|
||||
* without checked conversions has none, so its code is unchanged. */
|
||||
* without checked conversions has none, so its code is unchanged.
|
||||
*
|
||||
* A PARTIAL view (`dynCheck.partialView`) pairs a record literal's shape A
|
||||
* with the wider record B it was asserted to, though A lacks some of B's
|
||||
* fields. The view is a B value whose record-source entry names A, with
|
||||
* the sentinel SIZE_MAX for every B field A does not declare: a slot lookup
|
||||
* answers NULL there (scr_record_view_slot), so a presence test answers false
|
||||
* and an address lookup stops the program. The frontend's checked reads of
|
||||
* those fields throw their TypeError first. */
|
||||
import {
|
||||
isDirentShape,
|
||||
KEY_ORDER_FIELD,
|
||||
@@ -34,9 +42,28 @@ export interface RecordViews {
|
||||
readonly targets: ReadonlySet<string>;
|
||||
/** The record shapes whose capsules convert to a view of `target`. */
|
||||
sources(target: string): readonly string[];
|
||||
/** Whether `target` may be a PARTIAL view of `source`: some of its
|
||||
* fields are absent, because `source` does not declare them (a partial
|
||||
* record literal asserted to a wider type). */
|
||||
partial(target: string, source: string): boolean;
|
||||
/** Shape ids a partial record assertion may produce a view of. */
|
||||
readonly partialTargets: ReadonlySet<string>;
|
||||
/** Whether some partial source of view target `target` lacks `field`. */
|
||||
partialMissing(target: string, field: string): boolean;
|
||||
}
|
||||
|
||||
export const NO_RECORD_VIEWS: RecordViews = { targets: new Set(), sources: () => [] };
|
||||
/** The trap of an internal reader of a field a partial view's viewed record
|
||||
* lacks (a width copy, say): a source-level read of the field threw first. */
|
||||
export const PARTIAL_VIEW_ABSENT =
|
||||
"scriptc: a value asserted as a wider record has no property at this field (copy it explicitly)\n";
|
||||
|
||||
export const NO_RECORD_VIEWS: RecordViews = {
|
||||
targets: new Set(),
|
||||
sources: () => [],
|
||||
partial: () => false,
|
||||
partialTargets: new Set(),
|
||||
partialMissing: () => false,
|
||||
};
|
||||
|
||||
interface ViewOptions {
|
||||
readonly traced: (shapeId: string) => boolean;
|
||||
@@ -156,16 +183,41 @@ export function planRecordViews(mod: IrModule, options: ViewOptions): RecordView
|
||||
};
|
||||
const checked: IrType[] = [];
|
||||
const boxed: IrType[] = [];
|
||||
// (target B, source A) of a partial record assertion's checked cast.
|
||||
const partialPairs: { target: string; source: string }[] = [];
|
||||
for (const fn of mod.functions)
|
||||
everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (e) => {
|
||||
if (e.kind === "dynCheck") checked.push(e.type);
|
||||
else if (e.kind === "dynFrom" && e.liveRef === true) boxed.push(e.value.type);
|
||||
if (e.kind === "dynCheck") {
|
||||
checked.push(e.type);
|
||||
if (
|
||||
e.partialView === true &&
|
||||
e.type.kind === "record" &&
|
||||
e.value.kind === "dynFrom" &&
|
||||
e.value.value.type.kind === "record"
|
||||
)
|
||||
partialPairs.push({
|
||||
target: e.type.shapeId,
|
||||
source: e.value.value.type.shapeId,
|
||||
});
|
||||
} else if (e.kind === "dynFrom" && e.liveRef === true) boxed.push(e.value.type);
|
||||
else if (e.kind === "libCall" && /^(transform|passthrough)\.(new|init)$/.test(e.fn)) {
|
||||
// A transform or flush callback's typed completion data
|
||||
// (pinTypedDataCompletion) reaches untyped consumers as a live
|
||||
// capsule: scr_stream_push_typed boxes it for them.
|
||||
for (const arg of e.args) {
|
||||
if (arg.type.kind !== "func") continue;
|
||||
for (const param of arg.type.params)
|
||||
if (param.kind === "func" && param.params[1] !== undefined)
|
||||
boxed.push(param.params[1]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (checked.length === 0 || boxed.length === 0) return NO_RECORD_VIEWS;
|
||||
if ((checked.length === 0 || boxed.length === 0) && partialPairs.length === 0)
|
||||
return NO_RECORD_VIEWS;
|
||||
const reachable = closure(checked);
|
||||
const live = closure(boxed);
|
||||
if (reachable.size === 0) return NO_RECORD_VIEWS;
|
||||
@@ -183,6 +235,24 @@ export function planRecordViews(mod: IrModule, options: ViewOptions): RecordView
|
||||
}
|
||||
if (found.length > 0) sources.set(id, found);
|
||||
}
|
||||
// Partial pairs join the plan before the cycle analysis, which owns their
|
||||
// view's capsule reference like any other. A pair whose source has every
|
||||
// target field is an ordinary full view and is already planned above.
|
||||
const partials = new Map<string, Set<string>>();
|
||||
for (const pair of partialPairs) {
|
||||
const target = shapes.get(pair.target);
|
||||
const source = shapes.get(pair.source);
|
||||
if (!target || !source || !plainShape(target) || !plainShape(source)) continue;
|
||||
if (target.freezable === true || source.freezable === true) continue;
|
||||
if (options.traced(pair.target) || options.excluded(pair.target)) continue;
|
||||
if (covers(source, target)) continue;
|
||||
const list = sources.get(pair.target) ?? [];
|
||||
if (!list.includes(pair.source)) list.push(pair.source);
|
||||
sources.set(pair.target, list);
|
||||
const set = partials.get(pair.target);
|
||||
if (set) set.add(pair.source);
|
||||
else partials.set(pair.target, new Set([pair.source]));
|
||||
}
|
||||
// A view's record-source entry owns the source's capsule, an edge the
|
||||
// cycle analysis does not see. A view stays off every cycle only when its
|
||||
// source cannot hold a value of the target type, such as the view itself,
|
||||
@@ -200,11 +270,34 @@ export function planRecordViews(mod: IrModule, options: ViewOptions): RecordView
|
||||
changed = true;
|
||||
if (kept.length > 0) sources.set(id, kept);
|
||||
else sources.delete(id);
|
||||
const partial = partials.get(id);
|
||||
if (!partial) continue;
|
||||
for (const source of [...partial]) if (!kept.includes(source)) partial.delete(source);
|
||||
if (partial.size === 0) partials.delete(id);
|
||||
}
|
||||
}
|
||||
if (sources.size === 0) return NO_RECORD_VIEWS;
|
||||
const targets = new Set(sources.keys());
|
||||
return { targets, sources: (target) => sources.get(target) ?? [] };
|
||||
// The target fields some partial source lacks: their presence test is a
|
||||
// runtime view probe, and their slot lookup traps.
|
||||
const missing = new Map<string, Set<string>>();
|
||||
for (const [id, list] of partials) {
|
||||
const absent = new Set<string>();
|
||||
const target = shapes.get(id);
|
||||
for (const source of list) {
|
||||
const declared = new Set((shapes.get(source)?.fields ?? []).map((field) => field.name));
|
||||
for (const field of target?.fields ?? [])
|
||||
if (!declared.has(field.name)) absent.add(field.name);
|
||||
}
|
||||
missing.set(id, absent);
|
||||
}
|
||||
return {
|
||||
targets,
|
||||
sources: (target) => sources.get(target) ?? [],
|
||||
partial: (target, source) => partials.get(target)?.has(source) === true,
|
||||
partialTargets: new Set(partials.keys()),
|
||||
partialMissing: (target, field) => missing.get(target)?.has(field) === true,
|
||||
};
|
||||
}
|
||||
|
||||
/** Lines setting `dest` (i1) to whether the record `obj` of a view target
|
||||
@@ -229,7 +322,12 @@ export function recordViewTestLines(sizeType: "i32" | "i64", obj: string, dest:
|
||||
* `obj` of view-target shape `struct`: the slot itself, or the viewed
|
||||
* record's slot when `obj` is a view. Optimized builds test the mark
|
||||
* inline and look the slot up out of line; unoptimized builds, which give
|
||||
* every value a frame slot, make the address one call. */
|
||||
* every value a frame slot, make the address one call.
|
||||
*
|
||||
* `partial` names a shape a PARTIAL view may present: its viewed record can
|
||||
* lack the field, and the slot lookup answers NULL there (scr_record_view_slot),
|
||||
* so the address stops with `scr_trap` and `cstr(PARTIAL_VIEW_ABSENT)`. The
|
||||
* message is the program's own constant; the runtime carries none. */
|
||||
export function recordViewFieldPtr(
|
||||
B: BlockBuilder,
|
||||
sizeType: "i32" | "i64",
|
||||
@@ -239,13 +337,25 @@ export function recordViewFieldPtr(
|
||||
position: number,
|
||||
declare: (decl: string) => void,
|
||||
outOfLine: boolean,
|
||||
cstr: (text: string) => string,
|
||||
partial = false,
|
||||
): string {
|
||||
if (outOfLine) {
|
||||
declare(`declare ptr @scr_record_view_field(ptr, ${sizeType}, ${sizeType})`);
|
||||
const field = "scr_record_view_field";
|
||||
declare(`declare ptr @${field}(ptr, ${sizeType}, ${sizeType})`);
|
||||
const ptr = B.tmp();
|
||||
B.line(
|
||||
`${ptr} = call ptr @scr_record_view_field(ptr ${obj}, ${sizeType} ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 0, i32 ${slotIndex}) to ${sizeType}), ${sizeType} ${position})`,
|
||||
`${ptr} = call ptr @${field}(ptr ${obj}, ${sizeType} ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 0, i32 ${slotIndex}) to ${sizeType}), ${sizeType} ${position})`,
|
||||
);
|
||||
if (!partial) return ptr;
|
||||
const absent = B.tmp();
|
||||
B.line(`${absent} = icmp eq ptr ${ptr}, null`);
|
||||
const trap = B.newLabel("rv.absent");
|
||||
const join = B.newLabel("rv.present");
|
||||
B.condBr(absent, trap, join, `, !prof !{!"branch_weights", i32 1, i32 1000}`);
|
||||
B.startBlock(trap);
|
||||
absentTrapLines(B, cstr, declare);
|
||||
B.startBlock(join);
|
||||
return ptr;
|
||||
}
|
||||
const own = B.tmp();
|
||||
@@ -261,9 +371,34 @@ export function recordViewFieldPtr(
|
||||
declare(`declare ptr @scr_record_view_slot(ptr, ${sizeType})`);
|
||||
const viewed = B.tmp();
|
||||
B.line(`${viewed} = call ptr @scr_record_view_slot(ptr ${obj}, ${sizeType} ${position})`);
|
||||
let viewFrom = view;
|
||||
if (partial) {
|
||||
const absent = B.tmp();
|
||||
B.line(`${absent} = icmp eq ptr ${viewed}, null`);
|
||||
const trap = B.newLabel("rv.absent");
|
||||
const present = B.newLabel("rv.present");
|
||||
B.condBr(absent, trap, present);
|
||||
B.startBlock(trap);
|
||||
absentTrapLines(B, cstr, declare);
|
||||
viewFrom = present;
|
||||
B.startBlock(present);
|
||||
}
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const ptr = B.tmp();
|
||||
B.line(`${ptr} = phi ptr [ ${own}, %${from} ], [ ${viewed}, %${view} ]`);
|
||||
B.line(`${ptr} = phi ptr [ ${own}, %${from} ], [ ${viewed}, %${viewFrom} ]`);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
/** The block of an address lookup of a field a partial view lacks: the
|
||||
* program stops with its own message (PARTIAL_VIEW_ABSENT) and does not
|
||||
* return. */
|
||||
export function absentTrapLines(
|
||||
B: BlockBuilder,
|
||||
cstr: (text: string) => string,
|
||||
declare: (decl: string) => void,
|
||||
): void {
|
||||
declare(`declare void @scr_trap(ptr)`);
|
||||
B.line(`call void @scr_trap(ptr ${cstr(PARTIAL_VIEW_ABSENT)})`);
|
||||
B.terminate("unreachable");
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ const INERT_LIB_CALLS = new Set([
|
||||
"json.parse",
|
||||
"process.stderrWrite",
|
||||
"process.stdoutWrite",
|
||||
"process.stdioWriteDyn",
|
||||
"process.envGet",
|
||||
"process.envPairs",
|
||||
"regex.groupEquals",
|
||||
|
||||
@@ -28,6 +28,10 @@ function declarationHost(
|
||||
declare(decl) {
|
||||
declarations.push(decl);
|
||||
},
|
||||
cstr(text) {
|
||||
declarations.push(`@cstr ${text}`);
|
||||
return "@sc_cstr";
|
||||
},
|
||||
needOom() {},
|
||||
mapOps: (operands) => (operands.every((op) => op === "null") ? "null" : "@sc_map_ops"),
|
||||
sizeType: pointerBits === 32 ? "i32" : "i64",
|
||||
|
||||
@@ -73,7 +73,7 @@ import {
|
||||
disposeFieldLines,
|
||||
plainRefType,
|
||||
} from "./arenas.js";
|
||||
import { recordViewTestLines, type RecordViews } from "./record-views.js";
|
||||
import { PARTIAL_VIEW_ABSENT, recordViewTestLines, type RecordViews } from "./record-views.js";
|
||||
import { recordMayKeepSource } from "./record-sources.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
|
||||
@@ -109,6 +109,9 @@ export interface ShapeHost {
|
||||
/** Byte offset from an object pointer back to ScrCycHdr.buf. */
|
||||
readonly cycleBufOffset: number;
|
||||
declare(decl: string): void;
|
||||
/** The module constant for a string (LlEmitter.cstr), for a trap message
|
||||
* an emitted accessor passes to the runtime. */
|
||||
cstr(text: string): string;
|
||||
/** Inline the small-object allocator's fast paths (llvm/alloc.ts). */
|
||||
readonly inlineAlloc?: boolean;
|
||||
/** Record and class fields that store a scalar union's tag and bits
|
||||
@@ -2451,15 +2454,29 @@ export function emitRecordShapes(
|
||||
const inline = host.inlineUnions.record(field.type);
|
||||
const fieldTy = llFieldType(field.type, inline);
|
||||
if (host.recordViews?.targets.has(shape.id) === true) {
|
||||
// A view's fields live in the viewed record (record-views.ts).
|
||||
// A view's fields live in the viewed record (record-views.ts); a
|
||||
// shape a partial view may present answers NULL for a field the
|
||||
// viewed record lacks, so the copy stops with the program's own
|
||||
// message (a source-level read threw first).
|
||||
const partial = host.recordViews.partialTargets.has(shape.id);
|
||||
host.declare(`declare ptr @scr_record_view_slot(ptr, ${host.sizeType})`);
|
||||
const fromView = partial
|
||||
? [
|
||||
` %sn${i} = icmp eq ptr %sw${i}, null`,
|
||||
` br i1 %sn${i}, label %sv${i}.absent, label %sv${i}.join`,
|
||||
`sv${i}.absent:`,
|
||||
` call void @scr_trap(ptr ${host.cstr(PARTIAL_VIEW_ABSENT)})`,
|
||||
` unreachable`,
|
||||
]
|
||||
: [` br label %sv${i}.join`];
|
||||
if (partial) host.declare(`declare void @scr_trap(ptr)`);
|
||||
clone.push(
|
||||
` %so${i} = getelementptr inbounds %${struct}, ptr %src, i64 0, i32 ${index}`,
|
||||
...recordViewTestLines(host.sizeType, "%src", `%sr${i}v`),
|
||||
` br i1 %sr${i}v, label %sv${i}.view, label %sv${i}.join`,
|
||||
`sv${i}.view:`,
|
||||
` %sw${i} = call ptr @scr_record_view_slot(ptr %src, ${host.sizeType} ${i})`,
|
||||
` br label %sv${i}.join`,
|
||||
...fromView,
|
||||
`sv${i}.join:`,
|
||||
` %sp${i} = phi ptr [ %so${i}, %${i === 0 ? "entry" : `sv${i - 1}.join`} ], [ %sw${i}, %sv${i}.view ]`,
|
||||
` %sv${i} = load ${fieldTy}, ptr %sp${i}`,
|
||||
|
||||
@@ -234,6 +234,17 @@ function powerOfTwo(e: IrExpr): boolean {
|
||||
/** Typed-array element kinds whose elements are integers. */
|
||||
const INTEGER_ELEMENTS = new Set(["u8", "u8c", "i8", "u16", "i16", "u32", "i32"]);
|
||||
|
||||
/** The values of each integer element kind. */
|
||||
const ELEMENT_RANGES: Record<string, { min: number; max: number }> = {
|
||||
u8: { min: 0, max: 255 },
|
||||
u8c: { min: 0, max: 255 },
|
||||
i8: { min: -128, max: 127 },
|
||||
u16: { min: 0, max: 65535 },
|
||||
i16: { min: -32768, max: 32767 },
|
||||
u32: { min: 0, max: 2 ** 32 - 1 },
|
||||
i32: { min: -(2 ** 31), max: 2 ** 31 - 1 },
|
||||
};
|
||||
|
||||
/** A typed-array element read of an integer kind with no effect: an
|
||||
* out-of-range read gives NaN, which fails its check. Float elements would
|
||||
* only bail. */
|
||||
@@ -904,6 +915,17 @@ export function emitSafeIntegerCheck(host: LlvmEmitterContext, value: string): [
|
||||
return [integer, ok];
|
||||
}
|
||||
|
||||
/** The condition that a bounded value's exponent is above `limit`. A
|
||||
* value test it replaces can fail only past that limit; testing the
|
||||
* exponent alone, which is usually the same in every iteration of a loop,
|
||||
* keeps the value out of the test. Past the limit the write bails to the
|
||||
* double copy, also for a value the replaced test would have passed. */
|
||||
function exponentAbove(host: LlvmEmitterContext, bound: MaskBound, limit: number): string {
|
||||
const above = host.B.tmp();
|
||||
host.B.line(`${above} = icmp sgt i64 ${bound.exponent}, ${limit}`);
|
||||
return above;
|
||||
}
|
||||
|
||||
/** A condition that is true when an i64 lies outside the safe range. */
|
||||
function unsafeInteger(host: LlvmEmitterContext, value: string): string {
|
||||
const B = host.B;
|
||||
@@ -932,6 +954,29 @@ function outsideRange(host: LlvmEmitterContext, value: string, low: number, high
|
||||
return outside;
|
||||
}
|
||||
|
||||
/** The range of an operand once its speculative evaluation continued: a
|
||||
* literal's value, the range analysis' proof (well inside the safe
|
||||
* integers), or for an integer element read, which bails on the NaN of an
|
||||
* out-of-range index, the element kind's values. */
|
||||
function operandRange(
|
||||
host: LlvmEmitterContext,
|
||||
e: IrExpr,
|
||||
): { min: number; max: number } | undefined {
|
||||
const range = staticRange(host, e);
|
||||
if (range || !isSpeculativeByteRead(e) || e.kind !== "bytesIntrinsic") return range;
|
||||
return e.receiver.type.kind === "bytes" ? ELEMENT_RANGES[e.receiver.type.elem] : undefined;
|
||||
}
|
||||
|
||||
/** An i64 the speculative evaluation has bounded to [0, 2^(exponent +
|
||||
* shift)): the floor-mod of a value by 2^exponent, shifted left by `shift`.
|
||||
* `exponent` is an i64 already checked to be in [0, 52]; in a loop it is
|
||||
* usually the same value on every iteration, so a test on it alone is
|
||||
* loop-invariant. */
|
||||
interface MaskBound {
|
||||
exponent: string;
|
||||
shift: number;
|
||||
}
|
||||
|
||||
/** A literal's value or the range analysis' proof for an expression whose
|
||||
* range lies well inside the safe integers. */
|
||||
function staticRange(
|
||||
@@ -962,6 +1007,7 @@ export function emitSpeculativeInteger(
|
||||
bail: (condition: string) => void,
|
||||
reads: ReadonlyMap<IrExpr, LlValue> = new Map(),
|
||||
integral = false,
|
||||
bounds = new Map<string, MaskBound>(),
|
||||
): string {
|
||||
const B = host.B;
|
||||
// A value the range analysis already proves takes the ordinary integer
|
||||
@@ -1000,12 +1046,13 @@ export function emitSpeculativeInteger(
|
||||
// bits, also for a negative value: in [0, 2^n), and never -0.
|
||||
const mod = floorModPowerOfTwo(e);
|
||||
if (mod) {
|
||||
const value = emitSpeculativeInteger(host, mod.value, bail, reads, integral);
|
||||
const value = emitSpeculativeInteger(host, mod.value, bail, reads, integral, bounds);
|
||||
const n = exponentOf(host, mod.power, bail, reads, integral);
|
||||
const mask = B.tmp(),
|
||||
low = B.tmp();
|
||||
B.line(`${mask} = sub i64 ${shiftOne(host, n)}, 1`);
|
||||
B.line(`${low} = and i64 ${value}, ${mask}`);
|
||||
bounds.set(low, { exponent: n, shift: 0 });
|
||||
return low;
|
||||
}
|
||||
// A product with 2^n shifts: it is safe iff the other factor's
|
||||
@@ -1013,8 +1060,19 @@ export function emitSpeculativeInteger(
|
||||
// integer and a positive power is +0.
|
||||
if (e.op === "*" && (powerOfTwo(e.right) || powerOfTwo(e.left))) {
|
||||
const [factor, power] = powerOfTwo(e.right) ? [e.left, e.right] : [e.right, e.left];
|
||||
const value = emitSpeculativeInteger(host, factor, bail, reads, integral);
|
||||
const value = emitSpeculativeInteger(host, factor, bail, reads, integral, bounds);
|
||||
const n = exponentOf(host, power, bail, reads, integral);
|
||||
// A factor below 2^(m + s) times 2^k is below 2^(m + s + k), so
|
||||
// safe when m + s + k <= 53.
|
||||
const bound = bounds.get(value);
|
||||
const k = power.kind === "numLit" ? exactPowerOfTwo(power.value) : null;
|
||||
if (bound && k !== null) {
|
||||
bail(exponentAbove(host, bound, 53 - bound.shift - k));
|
||||
const product = B.tmp();
|
||||
B.line(`${product} = shl nuw nsw i64 ${value}, ${n}`);
|
||||
bounds.set(product, { exponent: bound.exponent, shift: bound.shift + k });
|
||||
return product;
|
||||
}
|
||||
const limit = B.tmp(),
|
||||
magnitude = B.tmp(),
|
||||
outside = B.tmp(),
|
||||
@@ -1027,16 +1085,24 @@ export function emitSpeculativeInteger(
|
||||
B.line(`${product} = shl nsw i64 ${value}, ${n}`);
|
||||
return product;
|
||||
}
|
||||
const a = emitSpeculativeInteger(host, e.left, bail, reads, integral);
|
||||
const b = emitSpeculativeInteger(host, e.right, bail, reads, integral);
|
||||
const a = emitSpeculativeInteger(host, e.left, bail, reads, integral, bounds);
|
||||
const b = emitSpeculativeInteger(host, e.right, bail, reads, integral, bounds);
|
||||
const result = B.tmp();
|
||||
if (e.op === "+" || e.op === "-") {
|
||||
// With one operand's range known, the other is checked against the
|
||||
// narrowed bounds before the operation: the check then depends on
|
||||
// no new value, and LLVM keeps the operation itself simple.
|
||||
const right = staticRange(host, e.right);
|
||||
const left = e.op === "+" ? staticRange(host, e.left) : undefined;
|
||||
if (right) {
|
||||
const right = operandRange(host, e.right);
|
||||
const left = e.op === "+" ? operandRange(host, e.left) : undefined;
|
||||
// A bounded operand, in [0, 2^(m + s)), plus one in [lo, hi] with
|
||||
// |lo|, hi <= 2^52 is safe when m + s <= 52.
|
||||
const bound = (value: string): MaskBound | undefined =>
|
||||
e.op === "+" ? bounds.get(value) : undefined;
|
||||
const aBound = right ? bound(a) : undefined;
|
||||
const bBound = !right && left ? bound(b) : undefined;
|
||||
if (aBound) bail(exponentAbove(host, aBound, 52 - aBound.shift));
|
||||
else if (bBound) bail(exponentAbove(host, bBound, 52 - bBound.shift));
|
||||
else if (right) {
|
||||
// a + b with b in [lo, hi] is safe iff a in [-SAFE - lo, SAFE - hi];
|
||||
// a - b iff a in [-SAFE + hi, SAFE + lo].
|
||||
const low = e.op === "+" ? -SAFE - right.min : -SAFE + right.max;
|
||||
|
||||
@@ -72,9 +72,9 @@ function recursiveFunctions(edges: ReadonlyMap<string, readonly string[]>): Set<
|
||||
* `call` from emitted code that can lower it as an `invoke`: it is not
|
||||
* async or a generator, not a constructor, not a method a virtual call can
|
||||
* reach or one a vtable holds (an overridden method), not a closure
|
||||
* target, not the entry or a library export, not named by any node other
|
||||
* than a `call`'s callee, and not called from an async or generator body
|
||||
* (coroutine bodies keep plain calls).
|
||||
* target or a closure's jsval body, not the entry or a library export,
|
||||
* not named by any node other than a `call`'s callee, and not called from
|
||||
* an async or generator body (coroutine bodies keep plain calls).
|
||||
*
|
||||
* A native unwind costs about 0.05 µs a raise and 10 to 25 ns a frame on
|
||||
* x86-64 (scr_unwind.c's cached plans) and about 2 µs a raise and 0.1 µs a
|
||||
@@ -103,6 +103,9 @@ export function unwindAbiFunctions(
|
||||
blocked.add("%error.name.read");
|
||||
blocked.add("%error.message.read");
|
||||
for (const entry of mod.lib?.exports ?? []) blocked.add(entry.fnName);
|
||||
// A closure's jsval body is called through the function value's jsval
|
||||
// entry (IrFunction.jsEntry), which runtime calls reach.
|
||||
for (const fn of mod.functions) if (fn.jsEntry !== undefined) blocked.add(fn.jsEntry);
|
||||
// Methods a virtual call can reach, and the overridden methods of class
|
||||
// hierarchies (whose vtables list them), are called through pointers; the
|
||||
// others are called directly, like functions. A virtual call reaches its listed
|
||||
|
||||
@@ -293,6 +293,8 @@ export class LlWalkers {
|
||||
index - base,
|
||||
(decl) => this.host.declare(decl),
|
||||
this.host.rcMode === undefined,
|
||||
(text) => this.host.cstr(text),
|
||||
this.host.recordViews?.partialTargets.has(shapeId) === true,
|
||||
);
|
||||
} else {
|
||||
p = B.tmp();
|
||||
|
||||
@@ -563,6 +563,9 @@ function helperProfileMatch(match: NativeProfileMatch): Record<string, number> {
|
||||
missing: match.missing,
|
||||
recovered: match.recovered,
|
||||
unknown_partitions: match.unknownPartitions,
|
||||
...(match.annotatedCounts === undefined || match.mismatchedCounts === undefined
|
||||
? {}
|
||||
: { annotated_counts: match.annotatedCounts, mismatched_counts: match.mismatchedCounts }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -31,9 +31,28 @@ export interface NativeLinkFeatures {
|
||||
/** The program assigns process.emitWarning: links the
|
||||
* SCR_WARNING_REPLACE variant of the dyn-async unit. */
|
||||
warningReplace?: boolean;
|
||||
/** The program asserts a record literal to a wider record type (a partial
|
||||
* record view): links the SCR_RECORD_VIEW_PARTIAL variant of scr_json.c.
|
||||
* Absent for every program without one, so their objects stay identical. */
|
||||
recordViewPartial?: boolean;
|
||||
/** The program uses process.stdin.setEncoding, string 'data' listeners
|
||||
* or stdio 'resize' events: links the SCR_STDIO_EXTENDED events unit. */
|
||||
stdioExtended?: boolean;
|
||||
/** writeFile's `flag` disposition or copyFile's mode argument:
|
||||
* scr_fs_flags.c and the SCR_FS_FLAGS build of scr_fs_cp.c. */
|
||||
fsFlags?: boolean;
|
||||
/** string_decoder's checked-dynamic chunk forms: scr_bytes_strdec_dyn.c. */
|
||||
strdecDyn?: boolean;
|
||||
/** process.prependListener, stdin.isRaw or server.off: the
|
||||
* SCR_PROCESS_EXTRAS builds of scr_events.c and scr_async_dyn.c and the
|
||||
* SCR_NET_SERVER_OFF build of scr_net.c. */
|
||||
processExtras?: boolean;
|
||||
/** A write completion callback takes the error argument: links the
|
||||
* SCR_STREAM_WRITE_ERRORS build of the stream unit. */
|
||||
streamWriteErrors?: boolean;
|
||||
/** The program reaches the process output streams' write slot: links the
|
||||
* SCR_STDIO_WRITE_SLOT build of the events unit. */
|
||||
stdioWriteSlot?: boolean;
|
||||
/** Calls into Node.js (@scriptc/node): scr_node_island.c and the
|
||||
* SCR_NODE_ISLANDS variant of the event loop unit. */
|
||||
nodeIsland?: boolean;
|
||||
@@ -43,6 +62,9 @@ export interface NativeLinkFeatures {
|
||||
/** A module namespace boxed for typed code links
|
||||
* scr_module_namespace_dyn.c; absent (false) otherwise. */
|
||||
moduleNamespaceDyn?: boolean;
|
||||
/** Primitive wrapper objects link scr_js_wrapper.c; absent (false)
|
||||
* otherwise. */
|
||||
primitiveWrapper?: boolean;
|
||||
zlib: boolean;
|
||||
/** node:zlib's Brotli forms: scr_brotli.c and the vendored brotli archive. */
|
||||
brotli?: boolean;
|
||||
@@ -120,6 +142,8 @@ export interface NativeLinkFeatures {
|
||||
* scr_text_codec_dyn.c. */
|
||||
textCodecDyn: boolean;
|
||||
watch: boolean;
|
||||
/** fs.watchFile/fs.unwatchFile: scr_watch_file.c. */
|
||||
watchFile: boolean;
|
||||
foreignFfi: boolean;
|
||||
nodeTest: boolean;
|
||||
tls: boolean;
|
||||
@@ -144,6 +168,10 @@ export interface NativeLinkFeatures {
|
||||
x509: boolean;
|
||||
/** perf_hooks event-loop metrics: scr_perf_loop.c. */
|
||||
perfLoop: boolean;
|
||||
/** Intl over ICU (scr_intl.c plus the ICU archive and its data
|
||||
* file): the `icu.*` libCalls of the first slice. Absent (false) for
|
||||
* programs without them, so their link features stay unchanged. */
|
||||
intl?: boolean;
|
||||
tlsCa: boolean;
|
||||
/** crypto.subtle importKey/sign/verify/digest and CryptoKey values:
|
||||
* scr_webcrypto.c. */
|
||||
|
||||
@@ -34,6 +34,7 @@ export async function createNativeLinkInfo(options: {
|
||||
features: pack.features,
|
||||
runtime: pack.selectedRuntimeArtifacts,
|
||||
archives: pack.selectedArchiveArtifacts,
|
||||
data: pack.selectedDataArtifacts,
|
||||
systemLibraries: pack.systemLibraries,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -145,6 +145,20 @@ test("the helper's profile match is read from its output", () => {
|
||||
'{"ok":true,"profile":{"annotated":1,"mismatched":0,"missing":0,"recovered":-1,"unknown_partitions":0}}\n',
|
||||
),
|
||||
).toBeNull();
|
||||
// With the profile's counters on the annotated and the mismatched functions.
|
||||
expect(
|
||||
parseNativeProfileMatch(
|
||||
'{"ok":true,"profile":{"annotated":120,"mismatched":7,"missing":40,"recovered":0,"unknown_partitions":0,"annotated_counts":9000,"mismatched_counts":1000}}\n',
|
||||
),
|
||||
).toEqual({
|
||||
annotated: 120,
|
||||
mismatched: 7,
|
||||
missing: 40,
|
||||
recovered: 0,
|
||||
unknownPartitions: 0,
|
||||
annotatedCounts: 9000,
|
||||
mismatchedCounts: 1000,
|
||||
});
|
||||
// A helper that predates the report prints nothing.
|
||||
expect(parseNativeProfileMatch("")).toBeNull();
|
||||
expect(parseNativeProfileMatch('{"ok":true}\n')).toBeNull();
|
||||
|
||||
@@ -111,13 +111,16 @@ export function nativeProfileHelperArgs(profile: NativeProfileOptions | undefine
|
||||
* profile has no record of (new, or never run while it was collected),
|
||||
* mismatched functions that the block-count file annotated (never more
|
||||
* than the mismatched ones), and partitions reused from the cache without
|
||||
* their counts. */
|
||||
* their counts. Helpers that weigh the match also report the profile's
|
||||
* counters on the annotated and on the mismatched functions. */
|
||||
export interface NativeProfileMatch {
|
||||
annotated: number;
|
||||
mismatched: number;
|
||||
missing: number;
|
||||
recovered: number;
|
||||
unknownPartitions: number;
|
||||
annotatedCounts?: number;
|
||||
mismatchedCounts?: number;
|
||||
}
|
||||
|
||||
/** The profile match in a successful helper emit's output, or null when the
|
||||
@@ -150,7 +153,18 @@ export function parseNativeProfileMatch(stdout: string): NativeProfileMatch | nu
|
||||
unknownPartitions === null
|
||||
)
|
||||
continue;
|
||||
return { annotated, mismatched, missing, recovered, unknownPartitions };
|
||||
const annotatedCounts = count("annotated_counts");
|
||||
const mismatchedCounts = count("mismatched_counts");
|
||||
return {
|
||||
annotated,
|
||||
mismatched,
|
||||
missing,
|
||||
recovered,
|
||||
unknownPartitions,
|
||||
...(annotatedCounts === null || mismatchedCounts === null
|
||||
? {}
|
||||
: { annotatedCounts, mismatchedCounts }),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -83,6 +83,17 @@ export interface CcOptions {
|
||||
/** The program uses node:zlib's Brotli forms (with zlib): compiles
|
||||
* scr_brotli.c and links the vendored brotli archive. */
|
||||
brotli?: boolean;
|
||||
/** The program uses an Intl form that only ICU can answer (the `icu.*`
|
||||
* libCalls: default-locale number/date formats, word segmentation):
|
||||
* compiles scr_intl.c and links the ICU archive, and the
|
||||
* executable gets icudt78l.dat next to it. Every engine-free form keeps
|
||||
* linking none of it. */
|
||||
intl?: boolean;
|
||||
/** With `intl`: the installed runtime pack's ICU archive and the include
|
||||
* root of the ICU headers it ships. The C-source path then links that
|
||||
* archive and compiles scr_intl.c against those headers, instead of
|
||||
* building ICU from its fetched release with Zig's C++ driver. */
|
||||
icuPack?: { archive: string; include: string };
|
||||
/** The program uses node:assert (index.ts detects assert.* libCalls on
|
||||
* the IR): compiles scr_assert.c — the zlib gating precedent, so
|
||||
* assert-free binaries keep their exact size class. scr_regex.c calls
|
||||
@@ -159,8 +170,26 @@ export interface CcOptions {
|
||||
wasm?: boolean;
|
||||
/** SCR_WARNING_REPLACE: process.emitWarning replacement support. */
|
||||
warningReplace?: boolean;
|
||||
/** SCR_RECORD_VIEW_PARTIAL: the scr_json.c build whose view slot answers
|
||||
* NULL for a field a partial record view does not have. */
|
||||
recordViewPartial?: boolean;
|
||||
/** SCR_STDIO_EXTENDED: stdin encoding, string data listeners, resize. */
|
||||
stdioExtended?: boolean;
|
||||
/** SCR_FS_FLAGS: writeFile's `flag` option and copyFile's mode argument
|
||||
* (scr_fs_flags.c and that build of scr_fs_cp.c). */
|
||||
fsFlags?: boolean;
|
||||
/** string_decoder's checked-dynamic chunk forms: compiles
|
||||
* scr_bytes_strdec_dyn.c. */
|
||||
strdecDyn?: boolean;
|
||||
/** SCR_PROCESS_EXTRAS / SCR_NET_SERVER_OFF: process.prependListener,
|
||||
* stdin.isRaw and server.off. */
|
||||
processExtras?: boolean;
|
||||
/** SCR_STREAM_WRITE_ERRORS: the stream unit publishes the error each
|
||||
* write completion callback receives (error-taking write callbacks). */
|
||||
streamWriteErrors?: boolean;
|
||||
/** SCR_STDIO_WRITE_SLOT: the events unit carries the typed write slot
|
||||
* (the write method as a value, its replacement, run-time-typed chunks). */
|
||||
stdioWriteSlot?: boolean;
|
||||
/** Calls into Node.js (@scriptc/node): compiles scr_node_island.c and
|
||||
* the event loop with SCR_NODE_ISLANDS (its helper slot). */
|
||||
nodeIsland?: boolean;
|
||||
@@ -170,6 +199,8 @@ export interface CcOptions {
|
||||
/** A module namespace boxed for typed code: compiles
|
||||
* scr_module_namespace_dyn.c. */
|
||||
moduleNamespaceDyn?: boolean;
|
||||
/** Primitive wrapper objects: compiles scr_js_wrapper.c. */
|
||||
primitiveWrapper?: boolean;
|
||||
/** The program uses the process-events surface (signal/exit listeners,
|
||||
* stdin events, for-await over stdin — moduleUsesProcessEvents on the
|
||||
* IR): compiles scr_events.c into the binary. Event-free binaries keep
|
||||
@@ -258,6 +289,11 @@ export interface CcOptions {
|
||||
* scr_watch.c into the binary — the net gating precedent, so watch-free
|
||||
* binaries keep their exact link line. */
|
||||
watch?: boolean;
|
||||
/** The program uses fs.watchFile or fs.unwatchFile (moduleUsesFsWatchFile
|
||||
* on the IR): compiles scr_watch_file.c into the binary — its own gated
|
||||
* unit under the same precedent, so polling-free binaries keep their
|
||||
* exact link line. */
|
||||
watchFile?: boolean;
|
||||
/** The executable manifest has a format-5 foreign callback descriptor:
|
||||
* compiles the MPSC queue/self-pipe unit. Other FFI and non-FFI binaries
|
||||
* keep their existing runtime size class. */
|
||||
|
||||
@@ -84,6 +84,9 @@ import {
|
||||
vendorBuildCacheRoot,
|
||||
tlsArchivePath,
|
||||
brotliArchivePath,
|
||||
ensureIcuSources,
|
||||
icuArchivePath,
|
||||
ensureIcuArchive,
|
||||
lreObjectPaths,
|
||||
zlibObjectPaths,
|
||||
stageVendorInputs,
|
||||
@@ -124,6 +127,10 @@ interface RuntimeUnitContext {
|
||||
zlibObjects: readonly string[];
|
||||
tlsArchive: string | null;
|
||||
brotliArchive: string | null;
|
||||
icuArchive: string | null;
|
||||
/** The include roots of the ICU headers scr_intl.c compiles against: the
|
||||
* runtime pack's, or the fetched release's common/ and i18n/. */
|
||||
icuIncludeDirs: readonly string[];
|
||||
}
|
||||
|
||||
/** An optional runtime input: runtime sources compiled when `when` holds,
|
||||
@@ -153,6 +160,11 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
),
|
||||
unit((u) => u.opts.copying, "scr_copying.c"),
|
||||
unit((u) => u.opts.fileHandle, "scr_file_handle.c"),
|
||||
// writeFile's `flag` option and the fs/promises twins; the copyFile mode
|
||||
// argument is the SCR_FS_FLAGS build of scr_fs_cp.c.
|
||||
unit((u) => u.opts.fsFlags === true, "scr_fs_flags.c"),
|
||||
// string_decoder's checked-dynamic chunk forms, beside their typed twins.
|
||||
unit((u) => u.opts.strdecDyn === true, "scr_bytes_strdec_dyn.c"),
|
||||
// BigIntStats member reads (`statSync(p, { bigint: true })` integers and
|
||||
// `*Ns`/`*Ms` members). Gated on the reads themselves (the bigstats
|
||||
// feature), not on bigint: every bigint value or wasm conversion turns
|
||||
@@ -174,9 +186,10 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
// SCR_MUSL flag and this target-selected TU travel together.
|
||||
unit((u) => isMuslTarget(u.driver), "scr_musl.c"),
|
||||
// scr_glob.c (path.matchesGlob) compiles its segments through the
|
||||
// regex unit and scr_idna.c (node:url's IDNA) normalizes through it,
|
||||
// so both ride the same switch; section GC drops them from binaries
|
||||
// that never use them.
|
||||
// regex unit, scr_idna.c (node:url's IDNA) normalizes through it and
|
||||
// scr_path_win32_relative.c lowercases through its case mapping, so all
|
||||
// three ride the same switch; section GC drops them from binaries that
|
||||
// never use them.
|
||||
unitArgs((u) =>
|
||||
u.regex
|
||||
? [
|
||||
@@ -185,6 +198,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
u.rt(join(u.rtDir, "scr_regex.c")),
|
||||
u.rt(join(u.rtDir, "scr_glob.c")),
|
||||
u.rt(join(u.rtDir, "scr_idna.c")),
|
||||
u.rt(join(u.rtDir, "scr_path_win32_relative.c")),
|
||||
...u.lreObjects,
|
||||
]
|
||||
: [],
|
||||
@@ -212,6 +226,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unit((u) => u.opts.moduleNamespace === true, "scr_module_namespace.c"),
|
||||
// A module namespace as the frozen null-prototype object typed code sees.
|
||||
unit((u) => u.opts.moduleNamespaceDyn === true, "scr_module_namespace_dyn.c"),
|
||||
// Primitive wrapper objects (new String/Number/Boolean, Object(primitive)).
|
||||
unit((u) => u.opts.primitiveWrapper === true, "scr_js_wrapper.c"),
|
||||
unit(
|
||||
(u) =>
|
||||
u.opts.dynAsync ||
|
||||
@@ -253,6 +269,20 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
]
|
||||
: [],
|
||||
),
|
||||
// Intl over ICU (scr_intl.c): ICU's C API only, at the default locale,
|
||||
// reading its data file from beside the executable. The ICU headers come
|
||||
// from the installed runtime pack beside its archive, or, when ICU is
|
||||
// built from its fetched release, from that source tree (common/unicode
|
||||
// for the core API, i18n/unicode for udat.h and unumberformatter.h).
|
||||
unitArgs((u) =>
|
||||
u.icuIncludeDirs.length === 0 || u.icuArchive === null
|
||||
? []
|
||||
: [
|
||||
...u.icuIncludeDirs.flatMap((dir) => ["-I", dir]),
|
||||
u.rt(join(u.rtDir, "scr_intl.c")),
|
||||
u.icuArchive,
|
||||
],
|
||||
),
|
||||
unit((u) => u.opts.events, "scr_events.c", "scr_readline.c"),
|
||||
// readline's terminal mode: rl.terminal sets the regex unit it
|
||||
// measures with.
|
||||
@@ -344,6 +374,9 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unit((u) => u.opts.dgram && u.opts.dgramDyn, "scr_dgram_dyn.c"),
|
||||
unit((u) => u.opts.textCodecDyn, "scr_text_codec_dyn.c"),
|
||||
unit((u) => u.opts.watch, "scr_watch.c"),
|
||||
// fs.watchFile's stat poller: its own gated unit (no loop hook — the
|
||||
// re-armed timers are the liveness), linked only by programs that poll.
|
||||
unit((u) => u.opts.watchFile, "scr_watch_file.c"),
|
||||
unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"),
|
||||
unit((u) => u.opts.nodeTest, "scr_test.c"),
|
||||
unit((u) => u.opts.webcrypto, "scr_webcrypto.c"),
|
||||
@@ -389,6 +422,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
"scr_node_island_handle.c",
|
||||
"scr_node_island_export.c",
|
||||
"scr_node_island_boot.c",
|
||||
"scr_node_island_gc.c",
|
||||
),
|
||||
// Readable.fromWeb reads a web ReadableStream through its reader.
|
||||
unit((u) => u.nativeFetch && u.opts.stream, "scr_stream_from_web.c"),
|
||||
@@ -416,13 +450,30 @@ const RUNTIME_DEFINES: readonly {
|
||||
// programs with dynamic-tier code select it.
|
||||
{ define: "SCR_JS_TIER", when: (opts) => opts.dynTier === true },
|
||||
{ define: "SCR_WARNING_REPLACE", when: (opts) => opts.warningReplace },
|
||||
// scr_json.c's view slot answers NULL for a field a partial record view
|
||||
// does not have; only a program with a partial assertion selects it.
|
||||
{ define: "SCR_RECORD_VIEW_PARTIAL", when: (opts) => opts.recordViewPartial },
|
||||
{ define: "SCR_STDIO_EXTENDED", when: (opts) => opts.stdioExtended },
|
||||
// The wave's prepend registrations and stdin.isRaw (scr_events.c,
|
||||
// scr_async_dyn.c), server.off (scr_net.c) and copyFile's mode form
|
||||
// (scr_fs_cp.c). The source build compiles these units from source, so the
|
||||
// defines must name the same builds runtime-pack-matrix.mjs selects.
|
||||
{ define: "SCR_PROCESS_EXTRAS", when: (opts) => opts.processExtras },
|
||||
{ define: "SCR_NET_SERVER_OFF", when: (opts) => opts.processExtras },
|
||||
{ define: "SCR_FS_FLAGS", when: (opts) => opts.fsFlags },
|
||||
// scr_stream.c's write-callback error publication, which the compiler's
|
||||
// error-first write adapter reads.
|
||||
{ define: "SCR_STREAM_WRITE_ERRORS", when: (opts) => opts.streamWriteErrors },
|
||||
// scr_events.c's typed process output write slot.
|
||||
{ define: "SCR_STDIO_WRITE_SLOT", when: (opts) => opts.stdioWriteSlot },
|
||||
// scr_async.c's native promise reactions, which the web streams unit uses,
|
||||
// scr_json.c's and scr_dyn_builtin_class.c's instanceof over its classes,
|
||||
// and scr_fetch.c's bodies of its streams.
|
||||
{ define: "SCR_NATIVE_REACTIONS", when: (opts) => opts.webStreams },
|
||||
{ define: "SCR_HANDLE_CLASSES", when: (opts) => opts.webStreams },
|
||||
{ define: "SCR_YIN", when: (opts) => opts.youngNursery },
|
||||
// scr_lib.c's path stat and zero Stats for the fs.watchFile poll.
|
||||
{ define: "SCR_FS_WATCH_FILE", when: (opts) => opts.watchFile },
|
||||
{ define: "SCR_NODE_ISLANDS", when: (opts) => opts.nodeIsland },
|
||||
];
|
||||
|
||||
@@ -455,6 +506,7 @@ export async function compileCInternal(
|
||||
const pki = opts.pki ?? false;
|
||||
const mbedtls = tls || cipher || ecdh || dh || pki;
|
||||
const brotli = (opts.zlib ?? false) && opts.brotli === true;
|
||||
const icu = opts.intl ?? false;
|
||||
const driver = resolveCc();
|
||||
const darwinDebugSymbols = needsDarwinDebugSymbols(
|
||||
targetPlatform(driver),
|
||||
@@ -697,6 +749,24 @@ export async function compileCInternal(
|
||||
let brotliArchive = brotli
|
||||
? brotliArchivePath(sanitize, driver, vendorBuildIdentity, vendorCacheRoot)
|
||||
: null;
|
||||
// The installed runtime pack's ICU archive is the one this path would
|
||||
// build (uninstrumented either way), so a pack that ships it and its
|
||||
// headers spares the Zig C++ build and the source download.
|
||||
const icuPack = icu ? (opts.icuPack ?? null) : null;
|
||||
let icuArchive = icu
|
||||
? (icuPack?.archive ?? icuArchivePath(driver, vendorBuildIdentity, vendorCacheRoot))
|
||||
: null;
|
||||
// scr_intl.c's ICU headers. They are known before the runtime-object flags
|
||||
// are: those flags compile every runtime unit, so without them a cached
|
||||
// scr_intl.o would take whatever ICU headers the host has.
|
||||
const icuSource =
|
||||
icu && icuPack === null ? join(await ensureIcuSources(vendorCacheRoot), "source") : null;
|
||||
const icuIncludeDirs: readonly string[] =
|
||||
icuPack !== null
|
||||
? [icuPack.include]
|
||||
: icuSource !== null
|
||||
? [join(icuSource, "common"), join(icuSource, "i18n")]
|
||||
: [];
|
||||
|
||||
// ours would collide; see scr_regex.c) — the standalone objects are for
|
||||
// static builds only.
|
||||
@@ -751,6 +821,18 @@ export async function compileCInternal(
|
||||
: await stageVendorInputs(materialize, join(stageRoot, "brotli"));
|
||||
brotliArchive = paths[0]!;
|
||||
}
|
||||
if (icu && icuPack === null) {
|
||||
const cachedArchive = icuArchivePath(driver, vendorBuildIdentity, materializeCacheRoot);
|
||||
protectCachedArtifact(cacheWarmPaths, cachedArchive);
|
||||
const materialize = async (): Promise<string[]> => [
|
||||
await ensureIcuArchive(driver, vendorBuildIdentity, materializeCacheRoot),
|
||||
];
|
||||
const paths =
|
||||
stageRoot === undefined
|
||||
? await materialize()
|
||||
: await stageVendorInputs(materialize, join(stageRoot, "icu"));
|
||||
icuArchive = paths[0]!;
|
||||
}
|
||||
if (regex) {
|
||||
for (const object of lreObjectPaths(
|
||||
sanitize,
|
||||
@@ -838,6 +920,8 @@ export async function compileCInternal(
|
||||
zlibObjects,
|
||||
tlsArchive,
|
||||
brotliArchive,
|
||||
icuArchive,
|
||||
icuIncludeDirs,
|
||||
}),
|
||||
// A .ll program TU (the LLVM backend) deliberately carries no target
|
||||
// triple (byte-stable output; clang supplies the host/SCRIPTC_TARGET
|
||||
@@ -886,6 +970,7 @@ export async function compileCInternal(
|
||||
...(regex ? ["-I", vendorEngineDir()] : []),
|
||||
...(zlibObjects.length > 0 ? ["-I", vendorZlibDir()] : []),
|
||||
...(tlsArchive !== null ? ["-I", join(vendorTlsDir(), "include")] : []),
|
||||
...icuIncludeDirs.flatMap((dir) => ["-I", dir]),
|
||||
];
|
||||
// Keep sanitizer runtime objects optimized in both postures. Large dev
|
||||
// programs still compile at O0, avoiding an expensive LLVM optimization
|
||||
|
||||
@@ -104,7 +104,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise<void>
|
||||
// Zig's musl sysroot does not provide arc4random_buf. Keep the fallback
|
||||
// inside the archive so library embedders need no extra system library.
|
||||
...(isMuslTarget(driver) ? ["scr_musl.c"] : []),
|
||||
...(regex ? ["scr_regex.c", "scr_glob.c"] : []),
|
||||
...(regex ? ["scr_regex.c", "scr_glob.c", "scr_path_win32_relative.c"] : []),
|
||||
...(opts.assert || regex || opts.symbol ? ["scr_assert.c"] : []),
|
||||
...(opts.inspect ? ["scr_inspect.c", "scr_console_native.c"] : []),
|
||||
...(opts.symbol ? ["scr_symbol.c"] : []),
|
||||
|
||||
@@ -3,7 +3,13 @@ import { createRequire } from "node:module";
|
||||
import { readdir, readFile, realpath, stat } from "node:fs/promises";
|
||||
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { BROTLI_VERSION, MBEDTLS_VERSION, QJS_COMMIT, ZLIB_VERSION } from "../vendor-inputs.js";
|
||||
import {
|
||||
BROTLI_VERSION,
|
||||
ICU_VERSION,
|
||||
MBEDTLS_VERSION,
|
||||
QJS_COMMIT,
|
||||
ZLIB_VERSION,
|
||||
} from "../vendor-inputs.js";
|
||||
import { stableTestMemo } from "./session.js";
|
||||
|
||||
const implementationPath = fileURLToPath(import.meta.url);
|
||||
@@ -139,6 +145,7 @@ async function runtimeFingerprintFresh(rtDir: string): Promise<string> {
|
||||
.update(MBEDTLS_VERSION)
|
||||
.update(ZLIB_VERSION)
|
||||
.update(BROTLI_VERSION)
|
||||
.update(ICU_VERSION)
|
||||
.update("\0backend-recipe\0");
|
||||
for (const path of NATIVE_RECIPE_IMPLEMENTATION_PATHS) {
|
||||
h.update(basename(path))
|
||||
@@ -159,19 +166,29 @@ async function runtimeFingerprintFresh(rtDir: string): Promise<string> {
|
||||
return h.digest("hex");
|
||||
}
|
||||
|
||||
interface RuntimeFingerprintGroup {
|
||||
label: string;
|
||||
dir: string;
|
||||
}
|
||||
|
||||
async function runtimeFingerprintInputGroups(
|
||||
rtDir: string,
|
||||
): Promise<{ label: string; dir: string; names: string[] }[]> {
|
||||
return Promise.all(
|
||||
[
|
||||
{ label: "runtime", dir: rtDir },
|
||||
{ label: "ryu", dir: join(rtDir, "..", "vendor", "ryu") },
|
||||
{ label: "quickjs-ng", dir: join(rtDir, "..", "vendor", "quickjs-ng") },
|
||||
{ label: "mbedtls", dir: join(rtDir, "..", "vendor", "mbedtls") },
|
||||
{ label: "zlib", dir: join(rtDir, "..", "vendor", "zlib") },
|
||||
{ label: "brotli", dir: join(rtDir, "..", "vendor", "brotli", "c") },
|
||||
{ label: "curl", dir: join(rtDir, "..", "vendor", "curl") },
|
||||
].map(async (group) => {
|
||||
(
|
||||
[
|
||||
{ label: "runtime", dir: rtDir },
|
||||
{ label: "ryu", dir: join(rtDir, "..", "vendor", "ryu") },
|
||||
{ label: "quickjs-ng", dir: join(rtDir, "..", "vendor", "quickjs-ng") },
|
||||
{ label: "mbedtls", dir: join(rtDir, "..", "vendor", "mbedtls") },
|
||||
{ label: "zlib", dir: join(rtDir, "..", "vendor", "zlib") },
|
||||
{ label: "brotli", dir: join(rtDir, "..", "vendor", "brotli", "c") },
|
||||
// ICU is fetched, not vendored: its pinned version and sha256 key the
|
||||
// archive's cache path (vendor-archives.ts icuArchivePath), so it is
|
||||
// not part of the runtime tree's fingerprint.
|
||||
{ label: "curl", dir: join(rtDir, "..", "vendor", "curl") },
|
||||
] satisfies RuntimeFingerprintGroup[]
|
||||
).map(async (group) => {
|
||||
const names = (await nativeSourceFiles(group.dir, true))
|
||||
.map((path) => relative(group.dir, path))
|
||||
.sort();
|
||||
|
||||
@@ -9,6 +9,9 @@ export const {
|
||||
vendorBrotliDir,
|
||||
brotliArchivePath,
|
||||
ensureBrotliArchive,
|
||||
ensureIcuSources,
|
||||
icuArchivePath,
|
||||
ensureIcuArchive,
|
||||
vendorBuildCacheRoot,
|
||||
vendorCacheTargetFlavor,
|
||||
vendorCacheBuildIdentity,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* of the link contract: a program object keeps this symbol undefined and the
|
||||
* matching runtime defines it, so incompatible manual links fail before the
|
||||
* program can start. */
|
||||
export const RUNTIME_ABI_VERSION = 47 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v47" as const;
|
||||
export const RUNTIME_ABI_VERSION = 48 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v48" as const;
|
||||
|
||||
/** Object consumption is useful today, but the complete scr_* surface may
|
||||
* still change before 1.0. The versioned marker prevents accidental mixing;
|
||||
|
||||
@@ -34,7 +34,7 @@ interface RuntimePackUnit {
|
||||
}
|
||||
|
||||
interface RuntimePackArchive extends RuntimePackArtifact {
|
||||
id: "quickjs" | "libregexp" | "zlib" | "brotli" | "mbedtls";
|
||||
id: "quickjs" | "libregexp" | "zlib" | "brotli" | "mbedtls" | "icu";
|
||||
predicate: RuntimePredicate;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,15 @@ export interface RuntimePackManifest {
|
||||
};
|
||||
flavors: Partial<Record<RuntimePackFlavor, RuntimePackFlavorManifest>>;
|
||||
archives: RuntimePackArchive[];
|
||||
/** Files the linked executable needs at RUN time rather than at link time,
|
||||
* installed beside it on every build. ICU's data file (icudt78l.dat, 11.1
|
||||
* MiB) is the only one; Intl programs copy it, every other program links
|
||||
* neither ICU nor its data. */
|
||||
data?: RuntimePackArtifact[];
|
||||
/** C headers under one include root (`<root>/unicode/*.h`): the ICU C API
|
||||
* headers scr_intl.c includes, which a sanitized build compiles that unit
|
||||
* against (native/sanitizer.ts). Only a pack that ships ICU lists them. */
|
||||
headers?: RuntimePackArtifact[];
|
||||
system_libraries: { name: string; predicate: RuntimePredicate }[];
|
||||
licenses: { path: string; license: string }[];
|
||||
}
|
||||
@@ -100,11 +109,19 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
|
||||
builtinValues: boolean;
|
||||
computedRequire: boolean;
|
||||
warningReplace: boolean;
|
||||
recordViewPartial: boolean;
|
||||
stdioExtended: boolean;
|
||||
/** The program's write completion callbacks take the error argument:
|
||||
* the SCR_STREAM_WRITE_ERRORS build of the stream unit. */
|
||||
streamWriteErrors: boolean;
|
||||
/** The program reaches the process output streams' write slot: the
|
||||
* SCR_STDIO_WRITE_SLOT build of the events unit. */
|
||||
stdioWriteSlot: boolean;
|
||||
nodeIsland: boolean;
|
||||
brotli: boolean;
|
||||
moduleNamespace: boolean;
|
||||
moduleNamespaceDyn: boolean;
|
||||
primitiveWrapper: boolean;
|
||||
nativeFetch: boolean;
|
||||
abortSignalOnly: boolean;
|
||||
netEffective: boolean;
|
||||
@@ -245,6 +262,10 @@ export function parseRuntimePackManifest(value: unknown): RuntimePackManifest {
|
||||
validArtifact(raw) && typeof archive?.id === "string" && validPredicate(archive.predicate)
|
||||
);
|
||||
}) ||
|
||||
(manifest.data !== undefined &&
|
||||
(!Array.isArray(manifest.data) || !manifest.data.every(validArtifact))) ||
|
||||
(manifest.headers !== undefined &&
|
||||
(!Array.isArray(manifest.headers) || !manifest.headers.every(validArtifact))) ||
|
||||
!Array.isArray(manifest.system_libraries) ||
|
||||
!manifest.system_libraries.every((raw) => {
|
||||
const library = object(raw);
|
||||
@@ -311,11 +332,15 @@ export function effectiveRuntimeFeatures(
|
||||
builtinValues: features.builtinValues === true,
|
||||
computedRequire: features.computedRequire === true,
|
||||
warningReplace: features.warningReplace === true,
|
||||
recordViewPartial: features.recordViewPartial === true,
|
||||
stdioExtended: features.stdioExtended === true,
|
||||
streamWriteErrors: features.streamWriteErrors === true,
|
||||
stdioWriteSlot: features.stdioWriteSlot === true,
|
||||
nodeIsland: features.nodeIsland === true,
|
||||
brotli: features.brotli === true,
|
||||
moduleNamespace: features.moduleNamespace === true,
|
||||
moduleNamespaceDyn: features.moduleNamespaceDyn === true,
|
||||
primitiveWrapper: features.primitiveWrapper === true,
|
||||
nativeFetch,
|
||||
// The fetch unit has AbortController and AbortSignal; scr_abort.c is
|
||||
// the same implementation without the fetch transport.
|
||||
@@ -395,6 +420,8 @@ export interface RuntimePackArtifacts {
|
||||
/** Selected unit variants; each may carry its bitcode artifact. */
|
||||
runtime: (RuntimePackArtifact & { bitcode?: RuntimePackArtifact })[];
|
||||
archives: RuntimePackArtifact[];
|
||||
/** Files to install beside the executable (ICU's data file for Intl). */
|
||||
data: RuntimePackArtifact[];
|
||||
systemLibraries: string[];
|
||||
}
|
||||
|
||||
@@ -447,12 +474,26 @@ export function selectRuntimePackArtifacts(
|
||||
`runtime pack has no ${key} flavor; reinstall the matching runtime package`,
|
||||
"invalid",
|
||||
);
|
||||
// Intl needs the ICU objects only the host-flavored packs carry (ICU is
|
||||
// C++, so a pack builds it with Zig's C++ driver for its own glibc target);
|
||||
// say so instead of linking a program whose scr_intl.c references would go
|
||||
// undefined.
|
||||
if (
|
||||
features.intl === true &&
|
||||
!selectedFlavor.runtime_units.some((unit) => unit.source === "scr_intl.c")
|
||||
)
|
||||
throw new RuntimePackError(
|
||||
`the ${manifest.target.name} runtime pack has no ICU objects, which Intl needs; reinstall a runtime package that supports Intl`,
|
||||
"unsupported",
|
||||
);
|
||||
return {
|
||||
features,
|
||||
runtime: selectedRuntimeUnits(selectedFlavor, features).map(({ variant }) => variant),
|
||||
archives: manifest.archives.filter((archive) =>
|
||||
evaluateRuntimePredicate(archive.predicate, features),
|
||||
),
|
||||
// ICU's data file is 11.1 MiB: only a program that links ICU installs it.
|
||||
data: features.intl === true ? (manifest.data ?? []) : [],
|
||||
systemLibraries: manifest.system_libraries
|
||||
.filter((entry) => evaluateRuntimePredicate(entry.predicate, features))
|
||||
.map((entry) => entry.name),
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
chmodSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
symlinkSync,
|
||||
unlinkSync,
|
||||
@@ -148,10 +152,12 @@ async function packFixture() {
|
||||
store,
|
||||
);
|
||||
let stages = 0;
|
||||
const stage = (selection: NativeRuntimeSelection) => {
|
||||
const directory = join(root, `stage-${stages++}`);
|
||||
const stage = (selection: NativeRuntimeSelection, with_: RuntimePackStore = store) => {
|
||||
const directory = join(tmpdir(), `scriptc-runtime-pack-stage-${process.pid}-${stages++}`);
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
mkdirSync(directory);
|
||||
return stageNativeRuntimeSelection(selection, directory, store);
|
||||
dirs.push(directory);
|
||||
return stageNativeRuntimeSelection(selection, directory, with_);
|
||||
};
|
||||
return { root, object, manifestText, entries, select, stage };
|
||||
}
|
||||
@@ -241,3 +247,180 @@ test.skipIf(process.platform === "win32")(
|
||||
expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object");
|
||||
},
|
||||
);
|
||||
|
||||
/** A store that keeps nothing: a build with an empty cache. */
|
||||
function emptyStore(): RuntimePackStore {
|
||||
const entries = new Map<string, string>();
|
||||
return {
|
||||
read: (key) => entries.get(key) ?? null,
|
||||
write: (key, text) => void entries.set(key, text),
|
||||
};
|
||||
}
|
||||
|
||||
const packRecord = (root: string) => join(root, "runtime-pack.verified.json");
|
||||
const hashes = () => vi.mocked(createHash).mock.calls.length;
|
||||
|
||||
test("a build with an empty cache trusts the record beside the pack", async () => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
let start = hashes();
|
||||
f.stage(selection);
|
||||
const firstStage = hashes() - start;
|
||||
const record = JSON.parse(readFileSync(packRecord(f.root), "utf8"));
|
||||
expect(record.schema).toBe(1);
|
||||
expect(record.artifacts.map((artifact: { path: string }) => artifact.path)).toEqual([
|
||||
"artifacts/base.o",
|
||||
]);
|
||||
start = hashes();
|
||||
const staged = f.stage(selection, emptyStore());
|
||||
// Only the store key is hashed.
|
||||
expect(hashes() - start).toBe(firstStage - 1);
|
||||
expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object");
|
||||
});
|
||||
|
||||
test.each(["replaced", "rewritten in place", "resized"])(
|
||||
"the record beside the pack does not trust an artifact that was %s",
|
||||
async (change) => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
f.stage(selection);
|
||||
const before = statSync(f.object);
|
||||
const changed = change === "replaced" ? f.object + ".new" : f.object;
|
||||
writeFileSync(changed, "x".repeat(before.size + (change === "resized" ? 1 : 0)));
|
||||
utimesSync(changed, before.atime, before.mtime);
|
||||
if (changed !== f.object) renameSync(changed, f.object);
|
||||
expect(() => f.stage(selection, emptyStore())).toThrow(
|
||||
"runtime pack artifact hash mismatch: artifacts/base.o",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test("an artifact whose mtime changed is hashed again and recorded again", async () => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
f.stage(selection);
|
||||
const before = statSync(f.object);
|
||||
utimesSync(f.object, before.atime, new Date(before.mtimeMs + 5000));
|
||||
const start = hashes();
|
||||
f.stage(selection, emptyStore());
|
||||
// The store key and the artifact.
|
||||
expect(hashes() - start).toBe(2);
|
||||
const [entry] = JSON.parse(readFileSync(packRecord(f.root), "utf8")).artifacts;
|
||||
expect(entry.mtime).toBe(statSync(f.object).mtimeMs);
|
||||
const again = hashes();
|
||||
f.stage(selection, emptyStore());
|
||||
expect(hashes() - again).toBe(1);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["truncated", () => "{"],
|
||||
["null", () => "null"],
|
||||
["another schema", (text: string) => text.replace('"schema":1', '"schema":2')],
|
||||
[
|
||||
"an entry with a string ctime",
|
||||
(text: string) => text.replace(/"ctime":([\d.]+)/, '"ctime":"$1"'),
|
||||
],
|
||||
["an entry without an inode", (text: string) => text.replace(/"ino":[\d.]+,/, "")],
|
||||
[
|
||||
"another digest",
|
||||
(text: string) => text.replace(/"sha256":"[0-9a-f]+"/, `"sha256":"${"0".repeat(64)}"`),
|
||||
],
|
||||
])("a record beside the pack that is %s is not trusted", async (_name, rewrite) => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
f.stage(selection);
|
||||
const text = readFileSync(packRecord(f.root), "utf8");
|
||||
const rewritten = rewrite(text);
|
||||
expect(rewritten).not.toBe(text);
|
||||
writeFileSync(packRecord(f.root), rewritten);
|
||||
const start = hashes();
|
||||
const staged = f.stage(selection, emptyStore());
|
||||
expect(hashes() - start).toBe(2);
|
||||
expect(readFileSync(staged.runtimeObjects[0]!, "utf8")).toBe("base object");
|
||||
// The record is written again, whole.
|
||||
const [entry] = JSON.parse(readFileSync(packRecord(f.root), "utf8")).artifacts;
|
||||
expect(entry.ino).toBe(statSync(f.object).ino);
|
||||
});
|
||||
|
||||
test("an unreadable record beside the pack is not trusted, and staging still succeeds", async () => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
// A directory where the record would be: it can be neither read nor replaced.
|
||||
mkdirSync(packRecord(f.root));
|
||||
const start = hashes();
|
||||
f.stage(selection, emptyStore());
|
||||
f.stage(selection, emptyStore());
|
||||
expect(hashes() - start).toBe(4);
|
||||
expect(readdirSync(f.root).filter((name) => name.endsWith(".tmp"))).toEqual([]);
|
||||
});
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"a pack directory that cannot be written keeps the record in the cache only",
|
||||
async (context) => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
chmodSync(f.root, 0o555);
|
||||
try {
|
||||
// A process that may write any directory cannot run this case.
|
||||
try {
|
||||
writeFileSync(join(f.root, "probe"), "");
|
||||
rmSync(join(f.root, "probe"));
|
||||
context.skip();
|
||||
} catch {
|
||||
/* The directory is read-only for this process. */
|
||||
}
|
||||
const store = emptyStore();
|
||||
let start = hashes();
|
||||
f.stage(selection, store);
|
||||
expect(hashes() - start).toBe(2);
|
||||
expect(existsSync(packRecord(f.root))).toBe(false);
|
||||
expect(readdirSync(f.root).filter((name) => name.endsWith(".tmp"))).toEqual([]);
|
||||
// The cache's record still answers the next build with that cache.
|
||||
start = hashes();
|
||||
f.stage(selection, store);
|
||||
expect(hashes() - start).toBe(1);
|
||||
} finally {
|
||||
chmodSync(f.root, 0o755);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test("writing the record beside the pack keeps the entries another build wrote", async () => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
const other = {
|
||||
path: "artifacts/other.o",
|
||||
sha256: "1".repeat(64),
|
||||
dev: 1,
|
||||
ino: 2,
|
||||
size: 3,
|
||||
mtime: 4,
|
||||
ctime: 5,
|
||||
};
|
||||
writeFileSync(packRecord(f.root), JSON.stringify({ schema: 1, artifacts: [other] }));
|
||||
f.stage(selection, emptyStore());
|
||||
const record = JSON.parse(readFileSync(packRecord(f.root), "utf8"));
|
||||
expect(record.artifacts.map((artifact: { path: string }) => artifact.path).sort()).toEqual([
|
||||
"artifacts/base.o",
|
||||
"artifacts/other.o",
|
||||
]);
|
||||
expect(
|
||||
record.artifacts.find((artifact: { path: string }) => artifact.path === other.path),
|
||||
).toEqual(other);
|
||||
expect(readdirSync(f.root).filter((name) => name.endsWith(".tmp"))).toEqual([]);
|
||||
});
|
||||
|
||||
test("a cache that already trusts the artifacts writes the record beside the pack", async () => {
|
||||
const f = await packFixture();
|
||||
const selection = f.select();
|
||||
f.stage(selection);
|
||||
rmSync(packRecord(f.root));
|
||||
const start = hashes();
|
||||
f.stage(selection);
|
||||
// The cache's record answers; only the store key is hashed.
|
||||
expect(hashes() - start).toBe(1);
|
||||
expect(existsSync(packRecord(f.root))).toBe(true);
|
||||
const empty = hashes();
|
||||
f.stage(selection, emptyStore());
|
||||
expect(hashes() - empty).toBe(1);
|
||||
});
|
||||
|
||||
@@ -2,22 +2,27 @@
|
||||
* selected artifact is verified and copied from the same read into private
|
||||
* storage. With a store, a pack's selections and the identities of the
|
||||
* artifacts it verified are kept between builds: an artifact whose file
|
||||
* identity is unchanged since it was hashed is not hashed again. */
|
||||
* identity is unchanged since it was hashed is not hashed again. The
|
||||
* identities are also kept in a record beside the pack when its directory
|
||||
* can be written, which builds with an empty store read too. */
|
||||
import type { NativeOptimization } from "./optimization.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
||||
import {
|
||||
closeSync,
|
||||
constants,
|
||||
copyFileSync,
|
||||
existsSync,
|
||||
fstatSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeSync,
|
||||
type Stats,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import type { NativeLinkFeatures } from "./native-link-info.js";
|
||||
import type { NativeTargetSpec } from "./targets.js";
|
||||
import {
|
||||
@@ -94,7 +99,9 @@ function storeKey(text: string): string {
|
||||
/** A stored selection: what selecting from the pack's files, with these
|
||||
* identities, returned. */
|
||||
interface StoredSelection {
|
||||
schema: 1;
|
||||
/** 2 adds the selection's data artifacts: a stored selection from before
|
||||
* them would make an Intl build stage none of them, so it is discarded. */
|
||||
schema: 2;
|
||||
licenses: RuntimePackManifest["licenses"];
|
||||
selected: RuntimePackArtifacts;
|
||||
}
|
||||
@@ -172,7 +179,7 @@ export function selectNativeRuntimePack(
|
||||
sameIdentity(fileIdentity(packagePath), identities[0]!) &&
|
||||
sameIdentity(fileIdentity(manifestPath), identities[1]!)
|
||||
) {
|
||||
const entry: StoredSelection = { schema: 1, licenses: manifest.licenses, selected };
|
||||
const entry: StoredSelection = { schema: 2, licenses: manifest.licenses, selected };
|
||||
store.write(key, JSON.stringify(entry));
|
||||
}
|
||||
} catch {
|
||||
@@ -201,13 +208,14 @@ function readStoredSelection(text: string | null): StoredSelection | null {
|
||||
if (value === null || typeof value !== "object") return null;
|
||||
const entry = value as Partial<StoredSelection>;
|
||||
if (
|
||||
entry.schema !== 1 ||
|
||||
entry.schema !== 2 ||
|
||||
!Array.isArray(entry.licenses) ||
|
||||
entry.selected === undefined ||
|
||||
entry.selected === null ||
|
||||
typeof entry.selected !== "object" ||
|
||||
!Array.isArray(entry.selected.runtime) ||
|
||||
!Array.isArray(entry.selected.archives) ||
|
||||
!Array.isArray(entry.selected.data) ||
|
||||
!Array.isArray(entry.selected.systemLibraries)
|
||||
)
|
||||
return null;
|
||||
@@ -239,21 +247,53 @@ function readVerified(
|
||||
try {
|
||||
const text = store?.read(key) ?? null;
|
||||
if (text === null) return verified;
|
||||
const stored = JSON.parse(text) as StoredVerification;
|
||||
if (stored.schema === 1)
|
||||
for (const artifact of stored.artifacts) verified.set(artifact.path, artifact);
|
||||
for (const artifact of storedArtifacts(text)) verified.set(artifact.path, artifact);
|
||||
} catch {
|
||||
/* A malformed entry verifies everything again. */
|
||||
}
|
||||
return verified;
|
||||
}
|
||||
|
||||
/** The well-formed entries of a stored verification; none when the text is
|
||||
* not a stored verification. */
|
||||
function storedArtifacts(text: string): VerifiedArtifact[] {
|
||||
const stored = JSON.parse(text) as Partial<StoredVerification> | null;
|
||||
if (stored === null || stored.schema !== 1 || !Array.isArray(stored.artifacts)) return [];
|
||||
const number = (value: unknown): boolean => typeof value === "number" && Number.isFinite(value);
|
||||
return stored.artifacts.filter(
|
||||
(artifact): artifact is VerifiedArtifact =>
|
||||
artifact !== null &&
|
||||
typeof artifact === "object" &&
|
||||
typeof artifact.path === "string" &&
|
||||
typeof artifact.sha256 === "string" &&
|
||||
number(artifact.dev) &&
|
||||
number(artifact.ino) &&
|
||||
number(artifact.size) &&
|
||||
number(artifact.mtime) &&
|
||||
number(artifact.ctime),
|
||||
);
|
||||
}
|
||||
|
||||
/** `known` records `artifact`'s digest and size for a file with `identity`. */
|
||||
function verifiedAs(
|
||||
known: VerifiedArtifact | undefined,
|
||||
artifact: { sha256: string; size: number },
|
||||
identity: FileIdentity,
|
||||
): boolean {
|
||||
return (
|
||||
known !== undefined &&
|
||||
known.sha256 === artifact.sha256 &&
|
||||
known.size === artifact.size &&
|
||||
sameIdentity(known, identity)
|
||||
);
|
||||
}
|
||||
|
||||
function stageArtifact(
|
||||
root: string,
|
||||
stage: string,
|
||||
artifact: RuntimePackArtifact,
|
||||
verified: Map<string, VerifiedArtifact>,
|
||||
newlyVerified: VerifiedArtifact[],
|
||||
records: readonly Map<string, VerifiedArtifact>[],
|
||||
verified: VerifiedArtifact[],
|
||||
): string {
|
||||
const source = join(root, artifact.path);
|
||||
const destination = join(stage, artifact.path);
|
||||
@@ -276,26 +316,17 @@ function stageArtifact(
|
||||
}
|
||||
// The bytes are those last hashed when the file kept the identity it had
|
||||
// then for the whole read.
|
||||
const known = verified.get(artifact.path);
|
||||
const unchanged = sameIdentity(before, after);
|
||||
const trusted =
|
||||
known !== undefined &&
|
||||
known.sha256 === artifact.sha256 &&
|
||||
known.size === artifact.size &&
|
||||
unchanged &&
|
||||
known.dev === before.dev &&
|
||||
known.ino === before.ino &&
|
||||
known.size === before.size &&
|
||||
known.mtime === before.mtime &&
|
||||
known.ctime === before.ctime;
|
||||
unchanged && records.some((record) => verifiedAs(record.get(artifact.path), artifact, before));
|
||||
if (
|
||||
bytes.length !== artifact.size ||
|
||||
(!trusted && createHash("sha256").update(bytes).digest("hex") !== artifact.sha256)
|
||||
) {
|
||||
throw new RuntimePackError(`runtime pack artifact hash mismatch: ${artifact.path}`, "invalid");
|
||||
}
|
||||
if (!trusted && unchanged)
|
||||
newlyVerified.push({
|
||||
if (unchanged)
|
||||
verified.push({
|
||||
path: artifact.path,
|
||||
sha256: artifact.sha256,
|
||||
dev: before.dev,
|
||||
@@ -353,10 +384,12 @@ export function stageNativeRuntimeSelection(
|
||||
const packagePath = join(root, "package.json");
|
||||
const manifestPath = join(root, "runtime-pack.json");
|
||||
const key = storeKey(JSON.stringify({ schema: 1, verified: root }));
|
||||
const verified = readVerified(store, key);
|
||||
const newlyVerified: VerifiedArtifact[] = [];
|
||||
const cacheRecord = readVerified(store, key);
|
||||
const packRecordPath = join(root, PACK_VERIFIED_FILE);
|
||||
const packRecord = readPackVerified(packRecordPath);
|
||||
const verified: VerifiedArtifact[] = [];
|
||||
const stage = (artifact: RuntimePackArtifact): string =>
|
||||
stageArtifact(root, stageRoot, artifact, verified, newlyVerified);
|
||||
stageArtifact(root, stageRoot, artifact, [cacheRecord, packRecord], verified);
|
||||
const runtimeObjects = selected.runtime.map(stage);
|
||||
const archives = selected.archives.map(stage);
|
||||
for (const license of licenses) {
|
||||
@@ -372,14 +405,112 @@ export function stageNativeRuntimeSelection(
|
||||
) {
|
||||
throw new RuntimePackError("runtime pack changed while staging verified artifacts", "invalid");
|
||||
}
|
||||
if (store !== undefined && newlyVerified.length > 0) {
|
||||
for (const artifact of newlyVerified) verified.set(artifact.path, artifact);
|
||||
const entry: StoredVerification = { schema: 1, artifacts: [...verified.values()] };
|
||||
// Each record learns the artifacts verified here that it does not list
|
||||
// with their current identity.
|
||||
const missing = (record: Map<string, VerifiedArtifact>): VerifiedArtifact[] =>
|
||||
verified.filter((artifact) => !verifiedAs(record.get(artifact.path), artifact, artifact));
|
||||
const cacheMissing = missing(cacheRecord);
|
||||
if (store !== undefined && cacheMissing.length > 0) {
|
||||
for (const artifact of cacheMissing) cacheRecord.set(artifact.path, artifact);
|
||||
const entry: StoredVerification = { schema: 1, artifacts: [...cacheRecord.values()] };
|
||||
try {
|
||||
store.write(key, JSON.stringify(entry));
|
||||
} catch {
|
||||
/* A failed write only means hashing again next time. */
|
||||
}
|
||||
}
|
||||
const packMissing = missing(packRecord);
|
||||
if (packMissing.length > 0) writePackVerified(packRecordPath, packMissing);
|
||||
return { runtimeObjects, archives, systemLibraries: selected.systemLibraries };
|
||||
}
|
||||
|
||||
/** Install the selected RUN-time data artifacts (ICU's icudt78l.dat) beside an
|
||||
* executable this host linked. The in-process linker does the same from its
|
||||
* own selection (backend/linker.ts); this host writes executables
|
||||
* synchronously and has no link plan, so it installs them here.
|
||||
*
|
||||
* A file already present at the pinned size is left alone (11.1 MiB of pure
|
||||
* cost otherwise), and the copy is atomic — a private temporary, then a
|
||||
* rename — so two concurrent builds of one program never read a partial
|
||||
* file. */
|
||||
export function installNativeRuntimeData(
|
||||
selection: NativeRuntimeSelection,
|
||||
outputPath: string,
|
||||
): void {
|
||||
const data = selection.selected.data ?? [];
|
||||
if (data.length === 0) return;
|
||||
const outputDir = dirname(resolve(outputPath));
|
||||
for (const artifact of data) {
|
||||
const destination = join(outputDir, basename(artifact.path));
|
||||
const existing = statSync(destination, { throwIfNoEntry: false });
|
||||
if (existing?.isFile() === true && existing.size === artifact.size) continue;
|
||||
const temporary = join(
|
||||
outputDir,
|
||||
`.${basename(destination)}.scriptc-${process.pid}-${randomUUID()}`,
|
||||
);
|
||||
copyFileSync(join(selection.root, artifact.path), temporary);
|
||||
try {
|
||||
renameSync(temporary, destination);
|
||||
} catch {
|
||||
rmSync(destination, { force: true });
|
||||
renameSync(temporary, destination);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The verification record kept beside the runtime pack, so a build with an
|
||||
* empty cache does not hash the pack's artifacts again. Like the cache's
|
||||
* record, it trusts an artifact only while its file keeps the identity
|
||||
* (device, inode, size, mtime, ctime) the record lists. */
|
||||
export const PACK_VERIFIED_FILE = "runtime-pack.verified.json";
|
||||
|
||||
/** The pack record's well-formed entries; none when it is missing,
|
||||
* unreadable or malformed. */
|
||||
function readPackVerified(path: string): Map<string, VerifiedArtifact> {
|
||||
const verified = new Map<string, VerifiedArtifact>();
|
||||
try {
|
||||
for (const artifact of storedArtifacts(readFileSync(path, "utf8")))
|
||||
verified.set(artifact.path, artifact);
|
||||
} catch {
|
||||
/* The artifacts are hashed again. */
|
||||
}
|
||||
return verified;
|
||||
}
|
||||
|
||||
/** Adds `artifacts` to the pack record: the record as it is now, with these
|
||||
* entries replacing the ones for the same paths, written to a new file and
|
||||
* renamed over the record, so a reader sees a whole record. Of two builds
|
||||
* writing at once, the last rename wins; the entries the other added are
|
||||
* hashed again by a later build. When the pack directory cannot be written,
|
||||
* the cache's record is the only one. */
|
||||
function writePackVerified(path: string, artifacts: readonly VerifiedArtifact[]): void {
|
||||
const temporary = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
||||
try {
|
||||
const record = readPackVerified(path);
|
||||
for (const artifact of artifacts) record.set(artifact.path, artifact);
|
||||
const entry: StoredVerification = { schema: 1, artifacts: [...record.values()] };
|
||||
const bytes = Buffer.from(JSON.stringify(entry));
|
||||
const fd = openSync(
|
||||
temporary,
|
||||
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL,
|
||||
0o644,
|
||||
);
|
||||
try {
|
||||
let offset = 0;
|
||||
while (offset < bytes.length) {
|
||||
const written = writeSync(fd, bytes, offset, bytes.length - offset);
|
||||
if (written === 0) throw new Error("the pack record was not written");
|
||||
offset += written;
|
||||
}
|
||||
} finally {
|
||||
closeSync(fd);
|
||||
}
|
||||
renameSync(temporary, path);
|
||||
} catch {
|
||||
try {
|
||||
rmSync(temporary, { force: true });
|
||||
} catch {
|
||||
/* Cleanup is best effort. */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 47, marker: "scr_runtime_abi_v47" },
|
||||
runtime_abi: { version: 48, marker: "scr_runtime_abi_v48" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
|
||||
flavors: {
|
||||
@@ -120,6 +120,7 @@ const features = {
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
watch: false,
|
||||
watchFile: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
tls: false,
|
||||
|
||||
@@ -62,6 +62,7 @@ const BASE: NativeLinkFeatures = {
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
watch: false,
|
||||
watchFile: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
tls: false,
|
||||
@@ -135,7 +136,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 47, marker: "scr_runtime_abi_v47" },
|
||||
runtime_abi: { version: 48, marker: "scr_runtime_abi_v48" },
|
||||
compiler: {
|
||||
command: "clang",
|
||||
identity: "fixture clang",
|
||||
|
||||
@@ -40,12 +40,19 @@ export interface RuntimePackSelection {
|
||||
features: RuntimeFeatureSet;
|
||||
runtimeObjects: string[];
|
||||
archives: string[];
|
||||
/** Files the executable loads at run time, installed beside it. */
|
||||
dataFiles: string[];
|
||||
/** The ICU headers an Intl selection ships (manifest `headers`), all under
|
||||
* `<include root>/unicode/`, verified like the other artifacts. */
|
||||
headerFiles: string[];
|
||||
systemLibraries: string[];
|
||||
dependencyPaths: string[];
|
||||
/** Exact installed inputs observed while the selected artifacts were verified. */
|
||||
sourceDependencies: NativeArtifactDependency[];
|
||||
selectedRuntimeArtifacts: RuntimePackArtifact[];
|
||||
selectedArchiveArtifacts: RuntimePackArtifact[];
|
||||
selectedDataArtifacts: RuntimePackArtifact[];
|
||||
selectedHeaderArtifacts: RuntimePackArtifact[];
|
||||
}
|
||||
|
||||
async function verifyArtifact(root: string, artifact: RuntimePackArtifact): Promise<string> {
|
||||
@@ -209,18 +216,27 @@ export async function loadRuntimePack(options: {
|
||||
const features = selected.features;
|
||||
const selectedVariants = selected.runtime;
|
||||
const selectedArchives = selected.archives;
|
||||
const selectedArtifactPaths = [...selectedVariants, ...selectedArchives].map((artifact) =>
|
||||
join(root, artifact.path),
|
||||
);
|
||||
const selectedData = selected.data;
|
||||
// The C-source path compiles scr_intl.c against these headers and links
|
||||
// the pack's ICU archive (index.ts runtimePackIcuInputs).
|
||||
const selectedHeaders = features.intl === true ? (manifest.headers ?? []) : [];
|
||||
const selectedArtifactPaths = [
|
||||
...selectedVariants,
|
||||
...selectedArchives,
|
||||
...selectedData,
|
||||
...selectedHeaders,
|
||||
].map((artifact) => join(root, artifact.path));
|
||||
let artifactDependencies: NativeArtifactDependency[];
|
||||
try {
|
||||
artifactDependencies = await snapshotDependencies(selectedArtifactPaths);
|
||||
} catch {
|
||||
throw new RuntimePackError("runtime pack artifact set changed during selection", "invalid");
|
||||
}
|
||||
const [runtimeObjects, archives] = await Promise.all([
|
||||
const [runtimeObjects, archives, dataFiles, headerFiles] = await Promise.all([
|
||||
Promise.all(selectedVariants.map((artifact) => verifyArtifact(root, artifact))),
|
||||
Promise.all(selectedArchives.map((artifact) => verifyArtifact(root, artifact))),
|
||||
Promise.all(selectedData.map((artifact) => verifyArtifact(root, artifact))),
|
||||
Promise.all(selectedHeaders.map((artifact) => verifyArtifact(root, artifact))),
|
||||
]);
|
||||
await Promise.all(manifest.licenses.map((license) => readFile(join(root, license.path)))).catch(
|
||||
() => {
|
||||
@@ -242,10 +258,21 @@ export async function loadRuntimePack(options: {
|
||||
features,
|
||||
runtimeObjects,
|
||||
archives,
|
||||
dataFiles,
|
||||
headerFiles,
|
||||
systemLibraries: selected.systemLibraries,
|
||||
dependencyPaths: [packagePath, manifestPath, ...runtimeObjects, ...archives],
|
||||
dependencyPaths: [
|
||||
packagePath,
|
||||
manifestPath,
|
||||
...runtimeObjects,
|
||||
...archives,
|
||||
...dataFiles,
|
||||
...headerFiles,
|
||||
],
|
||||
sourceDependencies,
|
||||
selectedRuntimeArtifacts: selectedVariants,
|
||||
selectedArchiveArtifacts: selectedArchives,
|
||||
selectedDataArtifacts: selectedData,
|
||||
selectedHeaderArtifacts: selectedHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { availableParallelism, tmpdir } from "node:os";
|
||||
import { basename, join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
@@ -11,9 +11,32 @@ import {
|
||||
validCachedFile,
|
||||
} from "./build-cache.js";
|
||||
import type { CcDriver } from "./native/driver.js";
|
||||
import { NativeCodegenError } from "./native-codegen-core.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/** One C++ translation unit of the ICU archive: the source, the archive member
|
||||
* name it compiles to, and the flags of its group. */
|
||||
interface IcuUnit {
|
||||
path: string;
|
||||
member: string;
|
||||
flags: string[];
|
||||
define?: string;
|
||||
}
|
||||
|
||||
/** Zig's library directory, which carries the libc++/libc++abi sources the
|
||||
* ICU archive merges: `ZIG_LIB_DIR` when the caller pins one, else the pinned
|
||||
* toolchain's own answer. */
|
||||
async function zigLibDir(): Promise<string> {
|
||||
const pinned = process.env["ZIG_LIB_DIR"];
|
||||
if (pinned !== undefined && pinned !== "") return pinned;
|
||||
const { stdout } = await execFileAsync("zig", ["env"]);
|
||||
const match = /\.lib_dir\s*=\s*"([^"]+)"/.exec(stdout);
|
||||
if (match === null)
|
||||
throw new Error("Intl needs Zig 0.16.0's C++ toolchain: `zig env` named no lib_dir");
|
||||
return match[1]!;
|
||||
}
|
||||
|
||||
/** The section flags the libregexp/libunicode objects build with (the
|
||||
* runtime pack's executable_section_elimination flags). */
|
||||
const LRE_SECTION_FLAGS = ["-ffunction-sections", "-fdata-sections"];
|
||||
@@ -24,13 +47,33 @@ function vendorObjectName(source: string): string {
|
||||
return source.replaceAll("/", "-").replace(/\.c$/, ".o");
|
||||
}
|
||||
|
||||
/** The sha256 of a buffer, as lowercase hex. */
|
||||
function sha256(bytes: Uint8Array): string {
|
||||
return createHash("sha256").update(bytes).digest("hex");
|
||||
}
|
||||
|
||||
/** Whether `path` is a non-empty file. */
|
||||
async function presentFile(path: string): Promise<boolean> {
|
||||
try {
|
||||
return (await stat(path)).size > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
import {
|
||||
QJS_COMMIT,
|
||||
MBEDTLS_VERSION,
|
||||
ZLIB_VERSION,
|
||||
BROTLI_VERSION,
|
||||
ICU_VERSION,
|
||||
ICU_SOURCE_URL,
|
||||
ICU_SOURCE_SHA256,
|
||||
ICU_SOURCE_SIZE,
|
||||
ICU_CXX_TARGET,
|
||||
LRE_SOURCES,
|
||||
ZLIB_SOURCES,
|
||||
icuSourceList,
|
||||
zlibInputs,
|
||||
} from "./vendor-inputs.js";
|
||||
export {
|
||||
@@ -38,6 +81,7 @@ export {
|
||||
MBEDTLS_VERSION,
|
||||
ZLIB_VERSION,
|
||||
BROTLI_VERSION,
|
||||
ICU_VERSION,
|
||||
LRE_SOURCES,
|
||||
ZLIB_SOURCES,
|
||||
} from "./vendor-inputs.js";
|
||||
@@ -532,9 +576,271 @@ export function createVendorArchives(context: VendorArchiveContext) {
|
||||
return archive;
|
||||
}
|
||||
|
||||
/** The fetched ICU release root (`icu/`, holding `source/` and LICENSE),
|
||||
* downloaded once per cache root and reused after that. ICU is not
|
||||
* vendored: the release's own source tarball is pinned by version and
|
||||
* sha256 in vendor-inputs.ts, verified on every use, and extracted by the
|
||||
* system `tar` as the libnode build does. An existing extracted tree is
|
||||
* enough, so an offline build works from a pre-populated cache. */
|
||||
const icuSourcesByCacheRoot = new Map<string, Promise<string>>();
|
||||
function ensureIcuSources(cacheRoot: string = vendorBuildCacheRoot()): Promise<string> {
|
||||
const pending = icuSourcesByCacheRoot.get(cacheRoot);
|
||||
if (pending !== undefined) return pending;
|
||||
const pendingFetch = (async (): Promise<string> => {
|
||||
const tree = join(cacheRoot, `icu4c-${ICU_VERSION}-source`);
|
||||
if (await presentFile(join(tree, "source", "common", "sources.txt"))) return tree;
|
||||
const tarball = join(cacheRoot, `icu4c-${ICU_VERSION}-sources.tgz`);
|
||||
let archive: Buffer;
|
||||
try {
|
||||
archive = await readFile(tarball);
|
||||
if (archive.length !== ICU_SOURCE_SIZE || sha256(archive) !== ICU_SOURCE_SHA256)
|
||||
throw new Error("cached ICU source tarball does not match its pin");
|
||||
} catch {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(ICU_SOURCE_URL);
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`could not download ICU's sources (${ICU_SOURCE_URL}); ` +
|
||||
`populate ${tarball} or allow network access`,
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`could not download ICU's sources (${ICU_SOURCE_URL}): ` +
|
||||
`${response.status} ${response.statusText}`,
|
||||
);
|
||||
archive = Buffer.from(await response.arrayBuffer());
|
||||
if (archive.length !== ICU_SOURCE_SIZE || sha256(archive) !== ICU_SOURCE_SHA256)
|
||||
throw new Error(
|
||||
`ICU source tarball from ${ICU_SOURCE_URL} is ${archive.length} bytes with sha256 ` +
|
||||
`${sha256(archive)}, expected ${ICU_SOURCE_SIZE} bytes with sha256 ` +
|
||||
`${ICU_SOURCE_SHA256}`,
|
||||
);
|
||||
await mkdir(cacheRoot, { recursive: true });
|
||||
await writeFile(tarball, archive);
|
||||
}
|
||||
await mkdir(cacheRoot, { recursive: true });
|
||||
const stage = await mkdtemp(join(cacheRoot, ".icu-sources-"));
|
||||
try {
|
||||
await execFileAsync("tar", ["-xzf", tarball, "-C", stage]);
|
||||
if (!(await presentFile(join(stage, "icu", "source", "common", "sources.txt"))))
|
||||
throw new Error(`ICU source tarball ${ICU_SOURCE_URL} does not hold icu/source`);
|
||||
await rm(tree, { recursive: true, force: true });
|
||||
await rename(join(stage, "icu"), tree);
|
||||
} finally {
|
||||
await rm(stage, { recursive: true, force: true });
|
||||
}
|
||||
return tree;
|
||||
})();
|
||||
icuSourcesByCacheRoot.set(cacheRoot, pendingFetch);
|
||||
pendingFetch.catch(() => icuSourcesByCacheRoot.delete(cacheRoot));
|
||||
return pendingFetch;
|
||||
}
|
||||
|
||||
/** The archive is the same for sanitized and plain builds: it is never
|
||||
* instrumented (see ensureIcuArchive), so neither the path nor the build
|
||||
* carries a sanitizer flavor. */
|
||||
function icuArchivePath(
|
||||
driver: CcDriver,
|
||||
buildIdentity: string,
|
||||
cacheRoot: string = vendorBuildCacheRoot(),
|
||||
): string {
|
||||
const flavor = `plain-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
|
||||
return join(cacheRoot, `icu-${ICU_VERSION}-${flavor}`, "libicu.a");
|
||||
}
|
||||
|
||||
/** The ICU archive for one flavor, compiled lazily on the first
|
||||
* Intl-using build (~470 C++ TUs, parallelized) and cached under
|
||||
* <build-cache>/vendor/icu-<version>-<flavor>-<target>-<toolchain>/
|
||||
* libicu.a with the mbedTLS archive's atomic-rename publish.
|
||||
*
|
||||
* Three things make this archive different from every other vendored one:
|
||||
*
|
||||
* - It is C++, so the pack's C driver cannot compile it: the TUs (and the
|
||||
* standard library they use) build with Zig's C++ driver for the glibc
|
||||
* target the runtime packs pin. That target is the point. In C++ mode,
|
||||
* glibc 2.38+ headers redirect strtol/strtoul to the C23 __isoc23_*
|
||||
* names, and a host libstdc++ binds those names too, so a host-compiled
|
||||
* C++ archive would raise the glibc floor above the packs'. Zig's
|
||||
* bundled libc++ and libc++abi compile from the toolchain's own sources
|
||||
* for the target and bind the target's names only.
|
||||
* - ICU requires RTTI (`typeid` in unifiedcache.h and ustrenum.cpp,
|
||||
* `dynamic_cast` in uniset_props.cpp), so the design note's
|
||||
* `-fno-rtti` is not available; `-fno-exceptions` is, and ICU, libc++,
|
||||
* and libc++abi all build so. libc++abi's Itanium personality routine
|
||||
* (cxa_personality.cpp) is the one unit that needs exceptions, so it is
|
||||
* left out: nothing can throw into it.
|
||||
* - Both the standard library and the C++ runtime it needs are merged INTO
|
||||
* the archive, so the final link stays C-only: a plain `clang`/`zig` C
|
||||
* link with the runtime pack's `-lm` resolves every undefined symbol.
|
||||
* Members are prefixed by source (`cxx-libcxx-`, `cxx-libcxxabi-`) so
|
||||
* their names stay unique.
|
||||
*
|
||||
* ICU builds with symbol renaming on (version suffixes `_78`), which is
|
||||
* what lets the zero-cost gate assert that only Intl programs link ICU
|
||||
* symbols.
|
||||
*
|
||||
* Sanitized builds link this same uninstrumented archive. AddressSanitizer
|
||||
* cannot instrument it: Zig's bundled libc++ compiles with
|
||||
* `-fsanitize=address` only against a `sanitizer/asan_interface.h` its
|
||||
* tree does not ship, and compiling ~600 C++ TUs again for the sanitizer
|
||||
* lane would add minutes to the first Intl program of every sanitized
|
||||
* build. The runtime's own ICU code (scr_intl.c) still compiles with the
|
||||
* sanitizer, and AddressSanitizer's interceptors cover ICU's allocations,
|
||||
* so a sanitized Intl program reports heap errors from ICU data as it
|
||||
* does everywhere else — the ICU code itself is simply not instrumented. */
|
||||
async function ensureIcuArchive(
|
||||
driver: CcDriver,
|
||||
buildIdentity: string,
|
||||
cacheRoot: string = vendorBuildCacheRoot(),
|
||||
): Promise<string> {
|
||||
// The archive is host-targeted ELF for the packs' glibc floor; a cross
|
||||
// target would need a Zig C++ link recipe this slice has not validated.
|
||||
// Both refusals are compile diagnostics (SC3002), not internal errors:
|
||||
// an Intl program on any other host or target is a documented "not
|
||||
// here yet", the same answer the missing runtime pack gives.
|
||||
if (driver.target !== null)
|
||||
throw new NativeCodegenError(
|
||||
"SC3002",
|
||||
`Intl needs ICU, which scriptc builds for the host's linux-x64 gnu target only: ` +
|
||||
`the ${driver.target} target is not supported yet`,
|
||||
);
|
||||
if (process.platform !== "linux" || process.arch !== "x64")
|
||||
throw new NativeCodegenError(
|
||||
"SC3002",
|
||||
"Intl needs ICU, which this compiler builds for linux-x64 only: " +
|
||||
`the ${process.platform}-${process.arch} host is not supported yet`,
|
||||
);
|
||||
const flavor = `plain-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`;
|
||||
const archive = icuArchivePath(driver, buildIdentity, cacheRoot);
|
||||
if (await validVendorArtifact(archive)) return archive;
|
||||
|
||||
await mkdir(cacheRoot, { recursive: true });
|
||||
const buildDir = await mkdtemp(join(tmpdir(), `scriptc-vendor-icu-${flavor}-`));
|
||||
// The tarball extracts to `icu/source/...` with ICU's LICENSE beside it.
|
||||
const vendor = join(await ensureIcuSources(cacheRoot), "source");
|
||||
const libDir = await zigLibDir();
|
||||
const cxx = ["zig", "c++", "-target", ICU_CXX_TARGET];
|
||||
const optimize = ["-Os"];
|
||||
const commonFlags = [
|
||||
"-std=c++17",
|
||||
...optimize,
|
||||
"-fno-exceptions",
|
||||
"-ffunction-sections",
|
||||
"-fdata-sections",
|
||||
];
|
||||
const icuFlags = [
|
||||
...commonFlags,
|
||||
"-DU_STATIC_IMPLEMENTATION",
|
||||
"-DU_ATTRIBUTE_DEPRECATED=",
|
||||
"-DNDEBUG",
|
||||
"-Wno-deprecated-declarations",
|
||||
"-I",
|
||||
join(vendor, "common"),
|
||||
"-I",
|
||||
join(vendor, "i18n"),
|
||||
];
|
||||
// libc++ 20 sources need C++20 (`constinit`, ranges) and libc++'s
|
||||
// charconv reads a header from `libcxx/libc`.
|
||||
const cxxLibraryFlags = [
|
||||
"-std=c++23",
|
||||
...optimize,
|
||||
"-fno-exceptions",
|
||||
"-ffunction-sections",
|
||||
"-fdata-sections",
|
||||
"-I",
|
||||
join(libDir, "libcxx", "include"),
|
||||
"-I",
|
||||
join(libDir, "libcxx", "src"),
|
||||
"-I",
|
||||
join(libDir, "libcxx", "libc"),
|
||||
"-I",
|
||||
join(libDir, "libcxxabi", "include"),
|
||||
"-D_LIBCPP_BUILDING_LIBRARY",
|
||||
"-D_LIBCXXABI_BUILDING_LIBRARY",
|
||||
"-DLIBCXX_BUILDING_LIBCXXABI",
|
||||
"-Wno-unknown-warning-option",
|
||||
];
|
||||
const cxxLibraryUnits = async (
|
||||
subdirectory: string,
|
||||
memberPrefix: string,
|
||||
skip: ReadonlySet<string>,
|
||||
): Promise<IcuUnit[]> => {
|
||||
const sources = (await readdir(join(libDir, subdirectory, "src")))
|
||||
.filter((name) => name.endsWith(".cpp") && !skip.has(name))
|
||||
.sort();
|
||||
return sources.map((source) => ({
|
||||
path: join(libDir, subdirectory, "src", source),
|
||||
member: `${memberPrefix}${source}`,
|
||||
flags: cxxLibraryFlags,
|
||||
}));
|
||||
};
|
||||
try {
|
||||
const units: IcuUnit[] = [
|
||||
...icuSourceList(await readFile(join(vendor, "common", "sources.txt"), "utf8")).map(
|
||||
(source) => ({
|
||||
path: join(vendor, "common", source),
|
||||
member: `common-${source}`,
|
||||
flags: icuFlags,
|
||||
define: "U_COMMON_IMPLEMENTATION",
|
||||
}),
|
||||
),
|
||||
{
|
||||
path: join(vendor, "stubdata", "stubdata.cpp"),
|
||||
member: "stubdata-stubdata.cpp",
|
||||
flags: icuFlags,
|
||||
define: "U_COMMON_IMPLEMENTATION",
|
||||
},
|
||||
...icuSourceList(await readFile(join(vendor, "i18n", "sources.txt"), "utf8")).map(
|
||||
(source) => ({
|
||||
path: join(vendor, "i18n", source),
|
||||
member: `i18n-${source}`,
|
||||
flags: icuFlags,
|
||||
define: "U_I18N_IMPLEMENTATION",
|
||||
}),
|
||||
),
|
||||
...(await cxxLibraryUnits("libcxx", "cxx-libcxx-", new Set())),
|
||||
...(await cxxLibraryUnits("libcxxabi", "cxx-libcxxabi-", new Set(["cxa_personality.cpp"]))),
|
||||
];
|
||||
const width = availableParallelism();
|
||||
for (let i = 0; i < units.length; i += width) {
|
||||
await Promise.all(
|
||||
units.slice(i, i + width).map(async (unit) => {
|
||||
const output = join(buildDir, unit.member.replace(/\.cpp$/, ".o"));
|
||||
await execFileAsync(cxx[0]!, [
|
||||
...cxx.slice(1),
|
||||
...unit.flags,
|
||||
...(unit.define === undefined ? [] : [`-D${unit.define}`]),
|
||||
"-g0",
|
||||
"-c",
|
||||
unit.path,
|
||||
"-o",
|
||||
output,
|
||||
]);
|
||||
return output;
|
||||
}),
|
||||
);
|
||||
}
|
||||
await execFileAsync("ar", [
|
||||
"rcs",
|
||||
join(buildDir, "libicu.a"),
|
||||
...units.map((unit) => join(buildDir, unit.member.replace(/\.cpp$/, ".o"))),
|
||||
]);
|
||||
await publishVendorArtifact(join(buildDir, "libicu.a"), archive);
|
||||
} finally {
|
||||
await rm(buildDir, { recursive: true, force: true });
|
||||
}
|
||||
return archive;
|
||||
}
|
||||
|
||||
return {
|
||||
vendorEngineDir,
|
||||
vendorTlsDir,
|
||||
ensureIcuSources,
|
||||
icuArchivePath,
|
||||
ensureIcuArchive,
|
||||
vendorBrotliDir,
|
||||
brotliArchivePath,
|
||||
ensureBrotliArchive,
|
||||
|
||||
@@ -2,7 +2,21 @@ import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { expect, test } from "vitest";
|
||||
import { ZLIB_SOURCES, zlibInputs } from "./vendor-inputs.js";
|
||||
import {
|
||||
ZLIB_SOURCES,
|
||||
ICU_VERSION,
|
||||
ICU_SOURCE_URL,
|
||||
ICU_SOURCE_SHA256,
|
||||
ICU_SOURCE_SIZE,
|
||||
ICU_DATA_URL,
|
||||
ICU_DATA_FILE,
|
||||
ICU_DATA_SHA256,
|
||||
ICU_DATA_SIZE,
|
||||
ICU_DATA_ZIP_SHA256,
|
||||
ICU_DATA_ZIP_SIZE,
|
||||
icuSourceList,
|
||||
zlibInputs,
|
||||
} from "./vendor-inputs.js";
|
||||
|
||||
const repoRoot = fileURLToPath(new URL("../../../..", import.meta.url));
|
||||
const zlibDir = join(repoRoot, "packages/runtime/vendor/zlib");
|
||||
@@ -78,3 +92,39 @@ test("zlib inputs follow Node's deps/zlib/zlib.gyp", () => {
|
||||
expect(wasm.sources).not.toContain("contrib/optimizations/inflate.c");
|
||||
expect(wasm.defines).toEqual(["HAVE_HIDDEN", "CPU_NO_SIMD"]);
|
||||
});
|
||||
|
||||
test("the pinned ICU is the release the oracle Node bundles", () => {
|
||||
// Node's own Intl answers from the ICU it was built with, so the fetched
|
||||
// release must be that version exactly: the differential corpus compares
|
||||
// scriptc's ICU output against Node's, byte for byte.
|
||||
expect(process.versions.icu).toBe(ICU_VERSION);
|
||||
});
|
||||
|
||||
test("the compiler's ICU pins match the runtime packs'", async () => {
|
||||
const pack = (await import(
|
||||
pathToFileURL(join(repoRoot, "packages/runtime-pack-common/scripts/icu-inputs.mjs")).href
|
||||
)) as Record<string, string | number>;
|
||||
expect(pack["ICU_VERSION"]).toBe(ICU_VERSION);
|
||||
expect(pack["ICU_SOURCE_URL"]).toBe(ICU_SOURCE_URL);
|
||||
expect(pack["ICU_SOURCE_SHA256"]).toBe(ICU_SOURCE_SHA256);
|
||||
expect(pack["ICU_SOURCE_SIZE"]).toBe(ICU_SOURCE_SIZE);
|
||||
expect(pack["ICU_DATA_URL"]).toBe(ICU_DATA_URL);
|
||||
expect(pack["ICU_DATA_FILE"]).toBe(ICU_DATA_FILE);
|
||||
expect(pack["ICU_DATA_SHA256"]).toBe(ICU_DATA_SHA256);
|
||||
expect(pack["ICU_DATA_SIZE"]).toBe(ICU_DATA_SIZE);
|
||||
expect(pack["ICU_DATA_ZIP_SHA256"]).toBe(ICU_DATA_ZIP_SHA256);
|
||||
expect(pack["ICU_DATA_ZIP_SIZE"]).toBe(ICU_DATA_ZIP_SIZE);
|
||||
// Both pins name the official release assets for the pinned version.
|
||||
expect(ICU_SOURCE_URL).toBe(
|
||||
`https://github.com/unicode-org/icu/releases/download/release-${ICU_VERSION}/icu4c-${ICU_VERSION}-sources.tgz`,
|
||||
);
|
||||
expect(ICU_DATA_URL).toBe(
|
||||
`https://github.com/unicode-org/icu/releases/download/release-${ICU_VERSION}/icu4c-${ICU_VERSION}-data-bin-l.zip`,
|
||||
);
|
||||
expect(ICU_SOURCE_SHA256).toMatch(/^[0-9a-f]{64}$/);
|
||||
expect(ICU_DATA_SHA256).toMatch(/^[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
test("ICU source lists parse to the units a build compiles", () => {
|
||||
expect(icuSourceList("# a comment\n\n b.cpp \nc.cpp\n")).toEqual(["b.cpp", "c.cpp"]);
|
||||
});
|
||||
|
||||
@@ -6,6 +6,49 @@ export const MBEDTLS_VERSION = "3.6.7";
|
||||
export const ZLIB_VERSION = "1.3.2.1-motley-285e94b";
|
||||
/** The brotli release Node v24.22.0 bundles (packages/runtime/vendor/brotli). */
|
||||
export const BROTLI_VERSION = "1.2.0";
|
||||
/** ICU4C release 78.3 — the exact version Node v24.22.0 bundles
|
||||
* (`process.versions.icu` there), from the `release-78.3` tag. ICU is not
|
||||
* vendored: the build fetches the release's own source tarball and data zip
|
||||
* from unicode-org/icu's GitHub release, pinning each by version and sha256,
|
||||
* and caches the download. The runtime packs ship its data file icudt78l.dat
|
||||
* (33,107,232 B) without the categories no Intl form reads (11,653,984 B),
|
||||
* beside the executable; see packages/runtime-pack-common/scripts/
|
||||
* icu-data.mjs. */
|
||||
export const ICU_VERSION = "78.3";
|
||||
const ICU_RELEASE_URL = `https://github.com/unicode-org/icu/releases/download/release-${ICU_VERSION}`;
|
||||
/** The release's source tarball (icu, not icu4j), the only build input that
|
||||
* is not vendored. */
|
||||
export const ICU_SOURCE_URL = `${ICU_RELEASE_URL}/icu4c-${ICU_VERSION}-sources.tgz`;
|
||||
export const ICU_SOURCE_SHA256 = "3a2e7a47604ba702f345878308e6fefeca612ee895cf4a5f222e7955fabfe0c0";
|
||||
export const ICU_SOURCE_SIZE = 27_977_255;
|
||||
/** The release's prebuilt little-endian data zip. 969 source files are
|
||||
* reviewable; 33 MiB of compiled CLDR tables are not, so the data is fetched
|
||||
* too, pinned the same way. */
|
||||
export const ICU_DATA_URL = `${ICU_RELEASE_URL}/icu4c-${ICU_VERSION}-data-bin-l.zip`;
|
||||
export const ICU_DATA_ZIP_SHA256 =
|
||||
"982619632b78887f1895b063e96e8c3cc7f99283337c8abbd05aa71635de613c";
|
||||
export const ICU_DATA_ZIP_SIZE = 12_836_659;
|
||||
export const ICU_DATA_FILE = "icudt78l.dat";
|
||||
export const ICU_DATA_SHA256 = "d5cf2a40dccbe471781ec7af85693bff542ff12f0b670c9630c4e72d60714b8b";
|
||||
export const ICU_DATA_SIZE = 33_107_232;
|
||||
/** The glibc target the ICU archive compiles for: the floor the linux-x64-gnu
|
||||
* runtime pack builds against (packages/runtime-pack-common/scripts/
|
||||
* glibc-toolchain.mjs GLIBC_RUNTIME_FLOOR), spelled for Zig's C++ driver.
|
||||
* ICU is the one C++ archive, and C++ compiled against a newer host glibc
|
||||
* binds the C23 strtol names (__isoc23_*) that floor forbids. */
|
||||
export const ICU_CXX_TARGET = "x86_64-linux-gnu.2.34";
|
||||
/** The translation units one of ICU's own source lists names, in its order.
|
||||
* The fetched tarball carries the lists (`source/common/sources.txt`,
|
||||
* `source/i18n/sources.txt`) verbatim, so the units a build compiles are
|
||||
* exactly the units ICU's own autoconf build compiles for this release.
|
||||
* Entries are bare file names. The pack build reads the same files
|
||||
* (packages/runtime-pack-common/scripts/icu-data.mjs). */
|
||||
export function icuSourceList(text: string): string[] {
|
||||
return text
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
||||
}
|
||||
/** Exact translation-unit membership shared by cache builds and external
|
||||
* source-pack recipes. */
|
||||
export const LRE_SOURCES = ["libregexp.c", "libunicode.c"] as const;
|
||||
|
||||
@@ -63,6 +63,7 @@ export async function warmNativeCaches(
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
watch: false,
|
||||
watchFile: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
tls: false,
|
||||
|
||||
@@ -155,6 +155,12 @@ const typedInterfaceUnsupported = "typed source has no compiler bridge for this
|
||||
const metadataExclusion =
|
||||
"WebIDL brand metadata is observable reflection, not an executable compatibility operation";
|
||||
|
||||
/** ReadableStream methods a typed program reaches through the web stream
|
||||
* classes (scr_webstreams.c): on a stream it constructs with them, and on
|
||||
* the native stream through its web view (scr_fetch.c, sf_stream_web_view).
|
||||
* A program that calls one links the classes. */
|
||||
export const READABLE_STREAM_WEB_VIEW_CALLS = ["pipeThrough", "pipeTo", "tee"] as const;
|
||||
|
||||
function buildFetchCompatProfile() {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
@@ -233,7 +239,7 @@ function buildFetchCompatProfile() {
|
||||
],
|
||||
responseCalls: ["json", "text", "bytes", "clone"],
|
||||
readableStreamReads: ["locked"],
|
||||
readableStreamCalls: ["cancel", "getReader"],
|
||||
readableStreamCalls: ["cancel", "getReader", ...READABLE_STREAM_WEB_VIEW_CALLS],
|
||||
},
|
||||
operations: [
|
||||
{
|
||||
@@ -447,6 +453,13 @@ function buildFetchCompatProfile() {
|
||||
facets: ["webidl-conversion", "identity", "state-machine", "error-shape"],
|
||||
evidence: [generated("stream-traces"), fixture("static-stream")],
|
||||
},
|
||||
...READABLE_STREAM_WEB_VIEW_CALLS.map((member): FetchCompatOperation => ({
|
||||
id: `stdlib.readable-stream.${member}`,
|
||||
name: `ReadableStream.${member}`,
|
||||
kind: "method",
|
||||
facets: ["identity", "promise-settlement", "state-machine", "error-shape"],
|
||||
evidence: [fixture("static-web-streams")],
|
||||
})),
|
||||
{
|
||||
id: "stdlib.readable-stream-default-reader.closed",
|
||||
name: "ReadableStreamDefaultReader.closed",
|
||||
@@ -757,25 +770,15 @@ function buildFetchCompatProfile() {
|
||||
"getReader",
|
||||
"prototype",
|
||||
),
|
||||
...["pipeThrough", "values"].map((member) =>
|
||||
unsupportedEntry(
|
||||
`stdlib.readable-stream.${member}`,
|
||||
"ReadableStream",
|
||||
member,
|
||||
"prototype",
|
||||
member === "values"
|
||||
? "direct for-await consumption compiles statically; storing or driving the first-class iterator handle is unsupported"
|
||||
: "the wider Web Streams graph is outside the native readable-stream slice",
|
||||
),
|
||||
...READABLE_STREAM_WEB_VIEW_CALLS.map((member) =>
|
||||
staticEntry(`stdlib.readable-stream.${member}`, "ReadableStream", member, "prototype"),
|
||||
),
|
||||
...["pipeTo", "tee"].map((member) =>
|
||||
unsupportedEntry(
|
||||
`stdlib.readable-stream.${member}`,
|
||||
"ReadableStream",
|
||||
member,
|
||||
"prototype",
|
||||
"this operation is outside the native readable-stream implementation",
|
||||
),
|
||||
unsupportedEntry(
|
||||
"stdlib.readable-stream.values",
|
||||
"ReadableStream",
|
||||
"values",
|
||||
"prototype",
|
||||
"direct for-await consumption compiles statically; storing or driving the first-class iterator handle is unsupported",
|
||||
),
|
||||
unsupportedEntry(
|
||||
"stdlib.readable-stream.symbol.asyncIterator",
|
||||
@@ -954,7 +957,8 @@ export interface StaticFetchMembers {
|
||||
readableStreamReads: ReadonlySet<string>;
|
||||
readableStreamCalls: ReadonlySet<string>;
|
||||
/** ReadableStream prototype members the native stream does not
|
||||
* implement, symbol-keyed ones as `[Symbol.name]`. */
|
||||
* implement itself, symbol-keyed ones as `[Symbol.name]`: the unsupported
|
||||
* ones, and the calls it answers through its web view. */
|
||||
readableStreamUnsupported: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
@@ -979,8 +983,8 @@ export function staticFetchMembers(): StaticFetchMembers {
|
||||
),
|
||||
readableStreamReads: new Set(profile.members.readableStreamReads),
|
||||
readableStreamCalls: new Set(profile.members.readableStreamCalls),
|
||||
readableStreamUnsupported: new Set(
|
||||
profile.inventory.entries
|
||||
readableStreamUnsupported: new Set([
|
||||
...profile.inventory.entries
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.owner === "ReadableStream" &&
|
||||
@@ -988,7 +992,8 @@ export function staticFetchMembers(): StaticFetchMembers {
|
||||
entry.status === "unsupported",
|
||||
)
|
||||
.map((entry) => entry.member),
|
||||
),
|
||||
...READABLE_STREAM_WEB_VIEW_CALLS,
|
||||
]),
|
||||
};
|
||||
return staticFetchMemberSets;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ function coverageNotes(): string[] {
|
||||
"Entries with status 'unsupported' describe where the named code is raised: forms of the construct outside the supported subset are refused with that code — not that every form of the named feature is refused. Supported forms appear as their own static entries where a table projects them.",
|
||||
`The engine-free fetch projection targets Node ${fetchTarget.node} with bundled Undici ${fetchTarget.undici}. Each projected row names the differential evidence that guards it; changing the pinned Node or Undici version is an explicit profile update.`,
|
||||
"The fetch profile also contains a runtime-reflected census of the selected fetch, abort, Headers, and readable-stream interfaces plus RequestInit/ResponseInit dictionary reads. Static and unsupported census rows are projected here; its explicitly out-of-scope metadata rows and adjacent-interface exclusions remain in the profile so absence is deliberate rather than ambiguous.",
|
||||
"Process-level diagnostic codes are not surface entries: SC0001-SC0004 are preflight gates, SC1110 is a comptime evaluation failure, SC3001/SC3002 are backend/target tier refusals, SC9001/SC9002 are internal errors.",
|
||||
"Process-level diagnostic codes are not surface entries: SC0001-SC0007 are preflight gates (SC0005-SC0007 are the ECMAScript early errors), SC1110 is a comptime evaluation failure, SC3001/SC3002 are backend/target tier refusals, SC9001/SC9002 are internal errors.",
|
||||
"Entry statuses are projected for the desktop targets. The mobile targets (aarch64-apple-ios, aarch64-apple-ios-simulator, aarch64-linux-android) compile library-mode archives only: the library-admissible surface (what SC4005's async_free requirement and the library link set admit) is supported there, the executable lane refuses those triples with SC3002, and no entry outside the library-admissible surface carries a mobile support claim. iOS archives build for iOS 15.0 on darwin hosts; Android archives build against NDK API level 26.",
|
||||
"No scheduling metadata is published; entry ids are the stable diff keys across releases.",
|
||||
];
|
||||
|
||||
@@ -19,6 +19,9 @@ export const DIAGNOSTIC_CODES = {
|
||||
SC0002: "project configuration incompatible with scriptc",
|
||||
SC0003: "malformed imported JSON module",
|
||||
SC0004: "TypeScript checker panic",
|
||||
SC0005: "duplicate lexical declaration in a block or switch statement list",
|
||||
SC0006: "lexical declaration colliding with a var in the same block",
|
||||
SC0007: "declaration in a single-statement position",
|
||||
SC1010: "package imports outside the supported set",
|
||||
SC1012: "default exports/imports (residual edges)",
|
||||
SC1013: "namespace imports (residual edges)",
|
||||
@@ -40,6 +43,7 @@ export const DIAGNOSTIC_CODES = {
|
||||
SC1071: "generators",
|
||||
SC1080: "'this' outside a class method",
|
||||
SC1090: "this syntax (generic construct fence)",
|
||||
SC1091: "write to an undeclared name in a JavaScript source",
|
||||
SC1100: "operations on 'unknown' values",
|
||||
SC1101: "converting typed values to 'unknown'",
|
||||
SC1110: "comptime evaluation failure",
|
||||
|
||||
@@ -7,8 +7,12 @@
|
||||
* Code bands:
|
||||
* SC0xxx preflight gates: TypeScript errors passed through (SC0001),
|
||||
* project-config incompatibilities (SC0002), malformed imported
|
||||
* JSON modules (SC0003), and upstream tsgo checker panics
|
||||
* surfaced as source-anchored diagnostics (SC0004)
|
||||
* JSON modules (SC0003), upstream tsgo checker panics surfaced
|
||||
* as source-anchored diagnostics (SC0004), and the ECMAScript
|
||||
* early errors the checker does not model: a duplicated or
|
||||
* `var`-colliding lexical declaration in a block or switch
|
||||
* statement list (SC0005/SC0006) and a declaration where the
|
||||
* grammar admits only a Statement (SC0007)
|
||||
* SC1xxx supported-TypeScript-not-yet: valid TS outside the current subset
|
||||
* SC2xxx scriptc type rules (types we cannot compile yet)
|
||||
* SC3xxx backend/target coverage: the program is valid, but the selected
|
||||
@@ -366,6 +370,24 @@ export function sharedModuleDuplicatedDiag(
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8020 — the program replaces a process output stream's write method
|
||||
* while a Node island exists: the island's `process.stdout` is its own
|
||||
* object, so the replacement and the island's writes cannot see each other.
|
||||
* H2 shares the native stream object with the islands and removes this. */
|
||||
export function stdioPatchAcrossIslandsDiag(
|
||||
stream: "stdout" | "stderr" | null,
|
||||
loc: SrcLoc,
|
||||
): ScrDiagnostic {
|
||||
const what =
|
||||
stream === null ? "process.stdout.write or process.stderr.write" : `process.${stream}.write`;
|
||||
return {
|
||||
code: "SC8020",
|
||||
message: `${what} is replaced while a Node island exists: the island's process output stream would not see this patch, and island writes would bypass it`,
|
||||
loc,
|
||||
hint: "the island's process.stdout would not see this patch",
|
||||
};
|
||||
}
|
||||
|
||||
/** SC8022 — island code extends a class of a module the program compiles.
|
||||
* A native instance cannot take an island subclass's overrides, so the
|
||||
* module is not owned by the program: a package runs in Node for both
|
||||
@@ -687,6 +709,11 @@ export const UNSUPPORTED: Record<string, UnsupportedEntry> = {
|
||||
// remains is `this` outside any method (top level, plain functions).
|
||||
SC1080: { feature: "'this' outside a class method", milestone: "later" },
|
||||
SC1090: { feature: "this syntax", milestone: "later" }, // generic fallback; message names the construct
|
||||
SC1091: {
|
||||
feature: "writing an undeclared name in a JavaScript source",
|
||||
milestone: "later",
|
||||
hint: "a write to an undeclared name has no native lowering: sloppy mode makes it a property on Node's global object, which a native binary has none of, and strict mode makes it throw; declare the name, or keep the module in a Node island (sloppy-mode programs route there)",
|
||||
},
|
||||
SC1100: {
|
||||
feature: "operations on 'unknown' values",
|
||||
milestone: "later",
|
||||
|
||||
@@ -60,6 +60,7 @@ const native: EarlyExecutableNativeFeatures = {
|
||||
dgramDyn: false,
|
||||
textCodecDyn: false,
|
||||
watch: false,
|
||||
watchFile: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
tls: false,
|
||||
@@ -220,7 +221,7 @@ test("early executable cache misses on source and resolution changes", async ()
|
||||
expect(await readEarlyExecutableCache(f.root, f.options)).toBeNull();
|
||||
});
|
||||
|
||||
test("early executable keys isolate compile modes, paths, implementation, checker, function layouts, and FFI manifests", async () => {
|
||||
test("early executable keys isolate compile modes, paths, implementation, checker, function layouts, shape inference, and FFI manifests", async () => {
|
||||
const f = await fixture();
|
||||
const tracker = new FrontendInputTracker();
|
||||
tracker.run(() => trackedReadFile(f.source));
|
||||
@@ -245,6 +246,8 @@ test("early executable keys isolate compile modes, paths, implementation, checke
|
||||
{ ...f.options, profile: "profile-use:digest" },
|
||||
{ ...f.options, functionLayout: "layout-digest" },
|
||||
{ ...f.options, checker: "tsc-ts-executable-identity" },
|
||||
{ ...f.options, shapeOverlay: true },
|
||||
{ ...f.options, jsInference: true },
|
||||
{ ...f.options, npmStatic: "auto" },
|
||||
{ ...f.options, npmStatic: ["commander"] },
|
||||
{ ...f.options, outPath: join(f.options.outDir, "other") },
|
||||
|
||||
@@ -64,10 +64,19 @@ export interface EarlyExecutableNativeFeatures {
|
||||
eventTarget?: boolean;
|
||||
wasm?: boolean;
|
||||
warningReplace?: boolean;
|
||||
recordViewPartial?: boolean;
|
||||
stdioExtended?: boolean;
|
||||
fsFlags?: boolean;
|
||||
strdecDyn?: boolean;
|
||||
processExtras?: boolean;
|
||||
/** Error-taking write completion callbacks: SCR_STREAM_WRITE_ERRORS. */
|
||||
streamWriteErrors?: boolean;
|
||||
/** The process output write slot: SCR_STDIO_WRITE_SLOT. */
|
||||
stdioWriteSlot?: boolean;
|
||||
nodeIsland?: boolean;
|
||||
moduleNamespace?: boolean;
|
||||
moduleNamespaceDyn?: boolean;
|
||||
primitiveWrapper?: boolean;
|
||||
zlib: boolean;
|
||||
brotli?: boolean;
|
||||
assert: boolean;
|
||||
@@ -111,6 +120,7 @@ export interface EarlyExecutableNativeFeatures {
|
||||
dgramDyn: boolean;
|
||||
textCodecDyn: boolean;
|
||||
watch: boolean;
|
||||
watchFile: boolean;
|
||||
foreignFfi: boolean;
|
||||
nodeTest: boolean;
|
||||
tls: boolean;
|
||||
@@ -123,6 +133,8 @@ export interface EarlyExecutableNativeFeatures {
|
||||
perfLoop: boolean;
|
||||
tlsCa: boolean;
|
||||
webcrypto: boolean;
|
||||
/** Intl over ICU (scr_intl.c, the ICU archive and its data file). */
|
||||
intl?: boolean;
|
||||
}
|
||||
|
||||
interface EarlyExecutableCacheStamp {
|
||||
@@ -198,6 +210,9 @@ export interface EarlyExecutableCacheOptions {
|
||||
/** Omitted unless shape inference's overlay compiles the packages
|
||||
* (frontend/shape-inference). */
|
||||
shapeOverlay?: true;
|
||||
/** Omitted unless shape inference types the program's own JavaScript
|
||||
* (SCRIPTC_INFER_JS=1, frontend/js-inference.ts). */
|
||||
jsInference?: true;
|
||||
/** Raw manifest identity: path and bytes. Native archives remain under the
|
||||
* stricter native cache's independent dependency validation. */
|
||||
ffiProfile: { path: string; bytes: Uint8Array } | null;
|
||||
@@ -299,6 +314,7 @@ const BOOLEAN_NATIVE_KEYS = [
|
||||
"dgramDyn",
|
||||
"textCodecDyn",
|
||||
"watch",
|
||||
"watchFile",
|
||||
"foreignFfi",
|
||||
"nodeTest",
|
||||
"tls",
|
||||
@@ -355,6 +371,8 @@ function cacheKey(options: EarlyExecutableCacheOptions): string {
|
||||
...(options.youngNursery ? ["young-nursery"] : []),
|
||||
...(options.nodeIslands !== undefined ? [`node-islands:${options.nodeIslands}`] : []),
|
||||
...(options.heapCensus ? ["heap-census"] : []),
|
||||
...(options.shapeOverlay ? ["shape-overlay"] : []),
|
||||
...(options.jsInference ? ["js-inference"] : []),
|
||||
options.npmStatic === null
|
||||
? "<npm-static-off>"
|
||||
: options.npmStatic === "auto"
|
||||
@@ -406,6 +424,8 @@ function routeKey(options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment"
|
||||
if (options.youngNursery) hash.update("young-nursery\0");
|
||||
if (options.nodeIslands !== undefined) hash.update(`node-islands:${options.nodeIslands}\0`);
|
||||
if (options.heapCensus) hash.update("heap-census\0");
|
||||
if (options.shapeOverlay) hash.update("shape-overlay\0");
|
||||
if (options.jsInference) hash.update("js-inference\0");
|
||||
hash
|
||||
.update(
|
||||
options.npmStatic === null
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
npmStaticRefusalDiag,
|
||||
iceDiag,
|
||||
isCheckerPanic,
|
||||
stdioPatchAcrossIslandsDiag,
|
||||
type ScrDiagnostic,
|
||||
} from "../diagnostics/diagnostic.js";
|
||||
import type { IrModule, SrcLoc } from "../ir/ir.js";
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
} from "../backend/target-diagnostics.js";
|
||||
import type { LowerResult, LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { FrontendFactory } from "../frontend/pipeline.js";
|
||||
import type { UnitStore } from "../frontend/lowering/unit-reuse.js";
|
||||
import type { CompilationTiming } from "../timing.js";
|
||||
import { readProjectConfig } from "../project-config.js";
|
||||
import { nodeIslandPolicy } from "../node-islands/policy.js";
|
||||
@@ -42,7 +44,9 @@ export interface PreparedExecutableModule {
|
||||
warnings: ScrDiagnostic[];
|
||||
}
|
||||
|
||||
/** Release the parser/checker before native object generation begins. */
|
||||
/** Release the parser/checker before native object generation begins.
|
||||
* `unitStore` keeps the lowered units of this build for the next one
|
||||
* (unit-reuse.ts). */
|
||||
export function prepareExecutableModule(
|
||||
entryPath: string,
|
||||
opts: CompileRequestOptions,
|
||||
@@ -52,6 +56,7 @@ export function prepareExecutableModule(
|
||||
timing: CompilationTiming = () => {},
|
||||
lowerIslands: ReadonlyMap<string, IslandPackage> = new Map(),
|
||||
retryFences = true,
|
||||
unitStore: UnitStore | null = null,
|
||||
): PreparedExecutableModule | CompileFailure {
|
||||
// The island decision reads the detector's scan unless islands are off.
|
||||
const islandScan =
|
||||
@@ -115,6 +120,7 @@ export function prepareExecutableModule(
|
||||
timing,
|
||||
grown,
|
||||
retryFences,
|
||||
unitStore,
|
||||
);
|
||||
}
|
||||
return fail(fe.preflight);
|
||||
@@ -127,6 +133,7 @@ export function prepareExecutableModule(
|
||||
targetPlatform: buildPlatform,
|
||||
...(ffi !== null ? { ffiImports: ffi.functions } : {}),
|
||||
...(opts.tier !== undefined ? { tierMode: opts.tier } : {}),
|
||||
...(unitStore !== null ? { unitStore } : {}),
|
||||
},
|
||||
(module) => validateModule(module).length === 0,
|
||||
);
|
||||
@@ -165,6 +172,7 @@ export function prepareExecutableModule(
|
||||
timing,
|
||||
grown,
|
||||
retryFences,
|
||||
unitStore,
|
||||
);
|
||||
}
|
||||
// A failed program whose compiled packages need the runtime or a
|
||||
@@ -195,6 +203,14 @@ export function prepareExecutableModule(
|
||||
return fail(lowered.diagnostics);
|
||||
}
|
||||
|
||||
// A program that replaces a process output stream's write method has
|
||||
// nowhere to route an island's writes: the island's process.stdout is
|
||||
// its own object. Until islands share the native stream object (H2),
|
||||
// refuse the combination instead of silently missing the patch.
|
||||
if (islandDecision !== null && lowered.stdioPatches !== undefined) {
|
||||
return fail(lowered.stdioPatches.map((p) => stdioPatchAcrossIslandsDiag(p.stream, p.loc)));
|
||||
}
|
||||
|
||||
const validation = validateModule(lowered.module);
|
||||
timing("ir-validate");
|
||||
if (validation.length > 0) {
|
||||
@@ -267,6 +283,7 @@ export function prepareExecutableModule(
|
||||
timing,
|
||||
islands,
|
||||
false,
|
||||
unitStore,
|
||||
);
|
||||
if (islanded.ok) return islanded;
|
||||
firstError ??= islanded.diagnostics[0] ?? null;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user