diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a402f912f..1b0efe2da 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -111,7 +111,8 @@ jobs:
needs: check-release
if: needs.check-release.outputs.should_release == 'true' && needs.check-release.outputs.reuse_artifacts != 'true'
runs-on: ubuntu-24.04
- timeout-minutes: 75
+ # The layout and the profile's two phases each build an instrumented compiler.
+ timeout-minutes: 120
continue-on-error: true
permissions:
contents: read
@@ -148,7 +149,7 @@ jobs:
env:
SCRIPTC_NATIVE_LAYOUT: ${{ runner.temp }}/cli-profile/cli.layout.gz
run: >-
- node scripts/refresh-cli-profile.mjs --tsc-ts "$RUNNER_TEMP/tsc-ts" --jobs 4
+ node scripts/refresh-cli-profile.mjs --tsc-ts "$RUNNER_TEMP/tsc-ts" --jobs 4 --context-sensitive
--output "$RUNNER_TEMP/cli-profile/cli.profdata.gz" --work "$RUNNER_TEMP/cli-profile-work"
- uses: actions/upload-artifact@v4
with:
diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx
index b1583a617..f2d91f1bd 100644
--- a/docs/content/docs/cli.mdx
+++ b/docs/content/docs/cli.mdx
@@ -173,10 +173,10 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
On x86-64 targets, compile the program's code twice, for the baseline and for x86-64-v3, and choose the copy when the program starts. The executable runs on any x86-64 CPU and uses the x86-64-v3 code on CPUs that support it. Not used with a --target-cpu level above baseline. Valid only for executable builds. See Runtime CPU dispatch.
--profile-generate <pattern>
- On Linux, build an instrumented executable for profile-guided optimization. Each run writes a raw profile to the path pattern, where %p expands to the process ID; the LLVM_PROFILE_FILE environment variable overrides the pattern at run time. A relative pattern resolves from the current working directory. Instrumented programs run slower. Valid only for executable builds with --optimization=release or speed.
+ On Linux, build an instrumented executable for profile-guided optimization. Each run writes a raw profile to the path pattern, where %p expands to the process ID; the LLVM_PROFILE_FILE environment variable overrides the pattern at run time. A relative pattern resolves from the current working directory. Instrumented programs run slower. With --profile-use, the build is optimized by that profile and instrumented after inlining, the second phase of a context-sensitive profile. Valid only for executable builds with --optimization=release or speed.
--profile-use <file.profdata>
- Optimize the executable with a profile from scriptc merge-profiles: code is laid out, inlined, and split by how the profiled runs used it. Code the profile does not describe, or describes from an older version of the program, is optimized as without a profile, so a stale profile only loses part of its gain. A new or changed profile compiles every partition of the program again. scriptc build prints how the profile matched the program's functions on stderr; see Profile-guided optimization. Valid only for executable builds with --optimization=release or speed.
+ Optimize the executable with a profile from scriptc merge-profiles: code is laid out, inlined, and split by how the profiled runs used it. Code the profile does not describe, or describes from an older version of the program, is optimized as without a profile, so a stale profile only loses part of its gain. A new or changed profile compiles every partition of the program again. scriptc build prints how the profile matched the program's functions on stderr; see Profile-guided optimization. A profile with context-sensitive counts also orders each function's blocks by them on Linux. Valid only for executable builds with --optimization=release or speed.
--npm-static <pkg[,pkg…]|auto>
Name npm packages to compile even if they don't ship their own type declarations (repeatable). A trailing ..., as in fs-extra..., also names every package that package depends on. auto uses the default automatic selection. Packages that can't compile produce diagnostics. See npm Dependencies.
@@ -246,7 +246,7 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
The TypeScript 7 API server that checks the program: tsc-ts (the default), which scriptc includes, or tsgo, from the typescript package scriptc also includes. Both give the same diagnostics and generated code.
SCRIPTC_CHECKER_SERVER
- Set to 1 on Linux and macOS to check with a tsc-ts server that later builds share. It takes effect with a tsc-ts release that serves tsc-ts --api --listen. The tsc-ts 0.0.9 that this version of scriptc includes does not, so each build checks with its own tsc-ts process, as without the variable. A build connects to the server for its working directory, starting it when none answers, and the server keeps the files it parsed, so the next build of the same program loads it faster. A build that finds the server in use by another build, or cannot start or reach it, checks with its own tsc-ts process. The server exits after 5 minutes without a build, or after a build when its resident memory is above 2048 MB (TSC_TS_LISTEN_MEMORY sets another limit in megabytes). Diagnostics and generated code are the same with and without it. It applies to the default checker, tsc-ts.
+ On Linux and macOS, builds check with a tsc-ts server that later builds share. The first build in a working directory starts it: a tsc-ts process, started with --api and --listen, that keeps running in the background with no terminal and keeps the files it parsed, so the next build in that directory loads its program faster. Each user, working directory and scriptc installation gets its own server, and a build connects only to the server of its own scriptc and tsc-ts. The socket is in $XDG_RUNTIME_DIR/scriptc, or /tmp/scriptc-<uid>, a directory only that user can access. A build that finds the server in use by another build, cannot start or reach it, or loses its connection to it, checks with its own tsc-ts process. The server exits after 10 minutes without a build, or after a build when its resident memory is above 2048 MB (TSC_TS_LISTEN_MEMORY sets another limit in megabytes). To stop it sooner, end that process, for example with pkill -f 'tsc-ts --api .*--listen'. Set to 0 to check with a tsc-ts process of each build's own. When CI is set to a value other than 0 or false, builds use their own process unless this variable is 1. Diagnostics and generated code are the same with and without the server. It applies to the default checker, tsc-ts.
```console
@@ -287,6 +287,23 @@ scriptc: warning: 3739 of the 8947 functions app.profdata describes (41.8%) chan
The build still succeeds. Collecting the profile again with the current program and scriptc version restores the counts.
+### Context-sensitive profiles
+
+A profile from the steps above counts each function's branches before the optimizer inlines calls. A second phase counts them again after inlining, separately for every place a function was inlined into:
+
+```console
+$ scriptc build app.ts --optimization=speed --profile-use app.profdata --profile-generate profiles-cs/%p.profraw -o app-instrumented-cs
+$ ./app-instrumented-cs typical-input.json
+$ scriptc merge-profiles -o app-cs.profdata app.profdata profiles-cs/*.profraw
+$ scriptc build app.ts --optimization=speed --profile-use app-cs.profdata
+```
+
+- **First phase:** `--profile-generate` alone. It instruments the program before inlining and gives the profile of the steps above.
+- **Second phase:** `--profile-use` with the first profile and `--profile-generate` together. The build is optimized by the first profile and instrumented after inlining, and `scriptc merge-profiles` adds its counts to the first profile.
+- **Optimized build:** `--profile-use` with the merged profile. On Linux, it also orders each function's blocks by the counts and moves blocks the runs never reached into separate sections, so the code that runs sits together.
+
+A profile that already holds context-sensitive counts cannot start a second phase. Merge the second phase's raw profiles into the first-phase profile instead. Both phases run the same workloads.
+
## Target CPU
By default, x86-64 executables run on any x86-64 CPU, so the compiler uses only the baseline instruction set (SSE2). `--target-cpu` compiles the program's code for a higher x86-64 microarchitecture level. Each level includes the ones below it:
diff --git a/docs/content/docs/platforms.mdx b/docs/content/docs/platforms.mdx
index 72cd65a3c..462a043ba 100644
--- a/docs/content/docs/platforms.mdx
+++ b/docs/content/docs/platforms.mdx
@@ -53,6 +53,8 @@ fib-linux: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically
Linux executables use the native Linux implementations of the event loop (epoll), servers, TLS (with the system's CA bundle), and `fs.watch`, and they are tested against Node.js on Linux, including Alpine.
+An executable with at least 4 MiB of program code is linked with 2 MiB segment alignment when lld or zig links it, so the kernel can map its code with 2 MiB pages instead of 4 KiB pages. This takes effect when transparent huge pages are set to `madvise` or `always` and the file system keeps files in the page cache as 2 MiB folios (XFS, for example). scriptc writes such an executable with one write so that the page cache holds it that way; a copy written in small chunks maps with 4 KiB pages until it is read from disk again. The alignment also aligns the executable's load address to 2 MiB, which leaves 19 bits of address randomization for it instead of 28. Smaller executables, and links by GNU ld, keep 4 KiB alignment.
+
### Windows (x86_64)
```console
diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx
index 25427a307..57a43e67a 100644
--- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx
+++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx
@@ -60,4 +60,8 @@
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 `ReadableStream` class as the global; its other classes are unsupported.
+ `node:stream/web` exports the same `ReadableStream` class as the global; in TypeScript, its other classes are unsupported.
+ In JavaScript, `ReadableStream`, `ReadableStreamDefaultReader`, `ReadableStreamDefaultController`, `CountQueuingStrategy`, and `ByteLengthQueuingStrategy`, as globals and as `node:stream/web` exports, are constructor values with Node's prototypes, brand checks, and errors.
+ Their underlying sources (`start`, `pull`, `cancel`), queuing strategies with a `size` function, readers, `tee`, `ReadableStream.from`, and async iteration settle promises in Node's order.
+ A JavaScript `ReadableStream` given as a `Response` or `Request` body, or as `fetch`'s body, is read through a stream of its own: `response.body` is that stream rather than the one passed in, and the stream passed in is locked from the start.
+ In JavaScript, a byte stream (`type: 'bytes'`), `pipeTo`, `pipeThrough`, `Object.getPrototypeOf` on these streams, readers, controllers, and strategies, their use as `WeakMap` and `WeakSet` keys, `ReadableStream.from` of a function that is not iterable, and the other `node:stream/web` classes are unsupported.
diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json
index aac6cfb14..43aa5e38b 100644
--- a/internal/compatibility/generated/node-v24-internal.json
+++ b/internal/compatibility/generated/node-v24-internal.json
@@ -111928,6 +111928,7 @@
"tests/corpus/7743-transform-stream-identity.ts",
"tests/corpus/stream-web-readable.ts",
"tests/corpus/stream-web-require.cjs",
+ "tests/corpus/web-stream-readable-untyped.mjs",
"tests/harness/fetch-conformance.test.ts"
]
},
@@ -112200,6 +112201,7 @@
"evidence": "compiler-feature:webstreams.ReadableStream",
"tests": [
"tests/corpus/stream-web-readable.ts",
+ "tests/corpus/web-stream-readable-untyped.mjs",
"tests/corpus/stream-web-require.cjs",
"tests/corpus/2862-web-readable-async-iteration.ts"
]
diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json
index 033e199c7..25a693c7c 100644
--- a/internal/compatibility/static-support.json
+++ b/internal/compatibility/static-support.json
@@ -433,7 +433,7 @@
{ "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] },
{ "chapter": "dns", "symbols": ["dns.lookup", "dns.lookupService", "dns.getServers", "dns.getDefaultResultOrder", "dns.setDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/5648-dns-lookup-forms.ts", "tests/corpus/5649-dns-service-servers.ts", "tests/corpus/5650-dns-lookup-errors-untyped.cjs"] },
{ "chapter": "dns", "symbols": ["dnsPromises.lookup", "dnsPromises.lookupService", "dnsPromises.getDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/dns-promises-lookup.ts", "tests/corpus/dns-promises-untyped.cjs"] },
- { "chapter": "webstreams", "symbols": ["ReadableStream"], "status": "partial", "evidence": ["tests/corpus/stream-web-readable.ts", "tests/corpus/stream-web-require.cjs", "tests/corpus/2862-web-readable-async-iteration.ts"] },
+ { "chapter": "webstreams", "symbols": ["ReadableStream"], "status": "partial", "evidence": ["tests/corpus/stream-web-readable.ts", "tests/corpus/web-stream-readable-untyped.mjs", "tests/corpus/stream-web-require.cjs", "tests/corpus/2862-web-readable-async-iteration.ts"] },
{ "chapter": "webstreams", "symbols": ["TransformStream"], "status": "partial", "evidence": ["tests/corpus/7743-transform-stream-identity.ts"] },
{ "chapter": "globals", "signatures": ["Class: `TransformStream`"], "status": "partial", "evidence": ["tests/corpus/7743-transform-stream-identity.ts"] }
],
diff --git a/native/llvm-codegen/src/emit.cpp b/native/llvm-codegen/src/emit.cpp
index d3d868fae..98e07e9ce 100644
--- a/native/llvm-codegen/src/emit.cpp
+++ b/native/llvm-codegen/src/emit.cpp
@@ -111,9 +111,6 @@ std::optional parseEmitOptions(int Argc, char **Argv) {
}
if (Options.Input.empty() || Options.Outputs.empty())
return std::nullopt;
- // A build either collects a profile or uses one.
- if (!Options.ProfileGenerate.empty() && !Options.ProfileUse.empty())
- return std::nullopt;
return Options;
}
@@ -150,20 +147,48 @@ PipelineTuningOptions tuningFor(OptimizationLevel Level) {
return Tuning;
}
-// The profile-guided optimization a build asks for: IR instrumentation that
-// writes raw profiles to the ProfileGenerate pattern, or annotation from the
-// indexed ProfileUse profile. Functions the profile does not describe, or
-// whose control flow changed since it was collected, are optimized as without
-// one, so a stale profile loses only its gain.
+// Whether the ProfileUse profile also holds context-sensitive counts, which
+// a second instrumentation after inlining recorded (inspectProfile).
+bool ContextProfile = false;
+
+// The profile-guided optimization a build asks for:
+// - IR instrumentation that writes raw profiles to the ProfileGenerate
+// pattern;
+// - annotation from the indexed ProfileUse profile, and from its
+// context-sensitive counts after inlining when it has them;
+// - with both, annotation from ProfileUse and a second instrumentation after
+// inlining that writes context-sensitive counts to the ProfileGenerate
+// pattern. `scriptc merge-profiles` adds them to the first profile.
+// Functions the profile does not describe, or whose control flow changed
+// since it was collected, are optimized as without one, so a stale profile
+// loses only its gain.
std::optional profileOptions(const EmitOptions &Options) {
+ if (!Options.ProfileGenerate.empty() && !Options.ProfileUse.empty())
+ return PGOOptions(Options.ProfileUse, Options.ProfileGenerate, "", "",
+ PGOOptions::IRUse, PGOOptions::CSIRInstr);
if (!Options.ProfileGenerate.empty())
return PGOOptions(Options.ProfileGenerate, "", "", "",
PGOOptions::IRInstr);
if (!Options.ProfileUse.empty())
- return PGOOptions(Options.ProfileUse, "", "", "", PGOOptions::IRUse);
+ return PGOOptions(Options.ProfileUse, "", "", "", PGOOptions::IRUse,
+ ContextProfile ? PGOOptions::CSIRUse
+ : PGOOptions::NoCSAction);
return std::nullopt;
}
+// Block placement by the extended TSP model, which needs the exact block
+// counts a context-sensitive profile gives. The option is process-wide; the
+// helper sets it once.
+void enableExtTspPlacement() {
+ static const bool Enabled = [] {
+ if (cl::Option *Option = cl::getRegisteredOptions().lookup(
+ "enable-ext-tsp-block-placement"))
+ Option->addOccurrence(0, "enable-ext-tsp-block-placement", "true");
+ return true;
+ }();
+ (void)Enabled;
+}
+
// With a profile, the thin link imports the callees of hot call edges up to
// 10 times, and of the hottest edges up to 100 times, the usual size limit,
// into every partition that calls them. On large programs that triples the
@@ -318,7 +343,8 @@ void reportProfileMatch(const ProfileMatch &Match) {
}
// Reads the profile's header once, before any partition needs it: it must
-// hold IR-level counts.
+// hold IR-level counts. Context-sensitive counts lay out the code by them;
+// a profile that has them cannot take a second set.
std::optional inspectProfile(const EmitOptions &Options) {
if (Options.ProfileUse.empty())
return std::nullopt;
@@ -333,6 +359,16 @@ std::optional inspectProfile(const EmitOptions &Options) {
return EmitFailure{"profile_read_failed",
Options.ProfileUse +
": not an IR-level instrumentation profile"};
+ ContextProfile = (*Reader)->hasCSIRLevelProfile();
+ if (ContextProfile && !Options.ProfileGenerate.empty())
+ return EmitFailure{"profile_read_failed",
+ Options.ProfileUse +
+ ": already holds context-sensitive counts; "
+ "collect them on top of the IR-level profile"};
+ if (ContextProfile && Triple(Options.Target).isOSBinFormatELF()) {
+ enableProfileLayout();
+ enableExtTspPlacement();
+ }
return std::nullopt;
}
@@ -804,6 +840,14 @@ compileOptimizedPartitions(const EmitOptions &Options,
: Level == OptimizationLevel::O3 ? 3
: 2;
Config.PTO = tuningFor(Level);
+ // The thin backend instruments or annotates context-sensitive counts after
+ // its inlining (see profileOptions).
+ if (!Options.ProfileGenerate.empty() && !Options.ProfileUse.empty()) {
+ Config.RunCSIRInstr = true;
+ Config.CSIRProfile = Options.ProfileGenerate;
+ } else if (ContextProfile) {
+ Config.CSIRProfile = Options.ProfileUse;
+ }
Config.CGFileType = CodeGenFileType::ObjectFile;
Config.DefaultTriple = Options.Target;
Config.DiagHandler = [&](const DiagnosticInfo &Info) {
diff --git a/native/llvm-codegen/src/emit.h b/native/llvm-codegen/src/emit.h
index b62a9d01c..1bdc578df 100644
--- a/native/llvm-codegen/src/emit.h
+++ b/native/llvm-codegen/src/emit.h
@@ -22,7 +22,9 @@ struct EmitOptions {
std::string CacheDirectory;
// Instruments the program for IR profiling. The instrumented executable
// writes raw profiles to this path pattern (LLVM_PROFILE_FILE overrides it
- // at run time). Empty: no instrumentation.
+ // at run time). Empty: no instrumentation. With ProfileUse, the program is
+ // optimized by that profile and instrumented again after inlining, which
+ // records context-sensitive counts.
std::string ProfileGenerate;
// Indexed profile (.profdata) that guides optimization. Empty: none.
std::string ProfileUse;
diff --git a/native/llvm-codegen/src/target.cpp b/native/llvm-codegen/src/target.cpp
index fd2866761..2695d478f 100644
--- a/native/llvm-codegen/src/target.cpp
+++ b/native/llvm-codegen/src/target.cpp
@@ -118,11 +118,19 @@ CodeGenOptLevel codeGenLevel(StringRef Level) {
return CodeGenOptLevel::Default;
}
+static bool ProfileLayout = false;
+
+void enableProfileLayout() { ProfileLayout = true; }
+
TargetOptions targetOptions(const Triple &TargetTriple) {
TargetOptions Options;
// The Android API 26 runtime pack uses the NDK's emulated TLS ABI.
// Program globals and runtime thread instances must use the same ABI.
Options.EmulatedTLS = TargetTriple.isAndroid();
+ if (ProfileLayout && TargetTriple.isOSBinFormatELF()) {
+ Options.FunctionSections = true;
+ Options.EnableMachineFunctionSplitter = true;
+ }
return Options;
}
diff --git a/native/llvm-codegen/src/target.h b/native/llvm-codegen/src/target.h
index 5049d34f5..5ad469bcf 100644
--- a/native/llvm-codegen/src/target.h
+++ b/native/llvm-codegen/src/target.h
@@ -40,6 +40,10 @@ bool supportsTarget(llvm::StringRef Triple);
// The target options and code generation level every helper target machine
// uses, shared with the partitioned optimizer's configuration.
llvm::TargetOptions targetOptions(const llvm::Triple &TargetTriple);
+// Lays out code by a profile's context-sensitive counts from now on: one
+// section per function, and cold blocks split into .text.split sections.
+// ELF targets only; the helper calls it once, before any target machine.
+void enableProfileLayout();
llvm::CodeGenOptLevel codeGenLevel(llvm::StringRef OptLevel);
std::unique_ptr
createTargetMachine(llvm::StringRef Triple, llvm::StringRef OptLevel,
diff --git a/package.json b/package.json
index 9b0213b44..e54ee164f 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"test:test262:fetch": "node tests/test262/fetch.mjs",
"test:cache-identity": "node tests/harness/cache-identity.mjs",
"test:lowering-order": "node scripts/lowering-order.mjs --fail-on names",
+ "test:recorded-lowering": "node scripts/recorded-lowering.mjs",
"test:sandbox": "node scripts/sandbox-test.mjs",
"test:sandbox:image": "node scripts/sandbox-image.mjs",
"test:watch": "vitest",
diff --git a/packages/cli/test/output-options.test.ts b/packages/cli/test/output-options.test.ts
index 45d5c55b8..d1e157ea7 100644
--- a/packages/cli/test/output-options.test.ts
+++ b/packages/cli/test/output-options.test.ts
@@ -55,7 +55,6 @@ describe("output option compatibility", () => {
[{ emit: "asm", cpuDispatch: true }, /cpu-dispatch/],
[{ emit: "obj", profileGenerate: "/tmp/%p.profraw" }, /profile-generate/],
[{ emit: "llvm", profileUse: "/tmp/a.profdata" }, /profile-use/],
- [{ profileGenerate: "/tmp/%p.profraw", profileUse: "/tmp/a.profdata" }, /cannot be combined/],
] as const)("rejects %j", (override, message) => {
const result = resolveOutputOptions("build", { ...BASE, ...override });
expect(result).toEqual({ ok: false, message: expect.stringMatching(message) });
@@ -120,4 +119,14 @@ describe("output option compatibility", () => {
outputKind: "exe",
});
});
+
+ test("a second profile phase combines --profile-use and --profile-generate", () => {
+ expect(
+ resolveOutputOptions("build", {
+ ...BASE,
+ profileGenerate: "/tmp/%p.profraw",
+ profileUse: "/tmp/a.profdata",
+ }),
+ ).toMatchObject({ ok: true, outputKind: "exe" });
+ });
});
diff --git a/packages/cli/test/profile.test.ts b/packages/cli/test/profile.test.ts
index da40a15b6..ab3f198ac 100644
--- a/packages/cli/test/profile.test.ts
+++ b/packages/cli/test/profile.test.ts
@@ -109,10 +109,6 @@ test("profile options are refused where they cannot apply", async () => {
await writeFile(entry, PROGRAM);
for (const [args, message] of [
[["build", entry, "--emit=llvm", "--profile-use", "a.profdata"], "--emit=exe"],
- [
- ["build", entry, "--profile-generate", "%p.profraw", "--profile-use", "a.profdata"],
- "cannot be combined",
- ],
[["coverage", entry, "--profile-use", "a.profdata"], "build/run option"],
[["merge-profiles", "a.profraw"], "merge-profiles takes -o"],
[["merge-profiles", "-o", "out.profdata"], "merge-profiles takes -o"],
diff --git a/packages/compiler/ambient/node-fallback/stream-web.d.ts b/packages/compiler/ambient/node-fallback/stream-web.d.ts
index f947e667d..6761186cc 100644
--- a/packages/compiler/ambient/node-fallback/stream-web.d.ts
+++ b/packages/compiler/ambient/node-fallback/stream-web.d.ts
@@ -1,13 +1,32 @@
/* node:stream/web — the module exports the web stream globals themselves
- * (require("stream/web").ReadableStream === globalThis.ReadableStream).
- * ReadableStream is the implemented class; its reader and controller are
- * exported as types. */
+ * (require("stream/web").ReadableStream === globalThis.ReadableStream). */
declare module "stream/web" {
export const ReadableStream: typeof globalThis.ReadableStream;
export type ReadableStream = globalThis.ReadableStream;
+ export const ReadableStreamDefaultReader: typeof globalThis.ReadableStreamDefaultReader;
export type ReadableStreamDefaultReader = globalThis.ReadableStreamDefaultReader;
+ export const ReadableStreamDefaultController: typeof globalThis.ReadableStreamDefaultController;
export type ReadableStreamDefaultController =
globalThis.ReadableStreamDefaultController;
+ export const WritableStream: typeof globalThis.WritableStream;
+ export type WritableStream = globalThis.WritableStream;
+ export const WritableStreamDefaultWriter: typeof globalThis.WritableStreamDefaultWriter;
+ export type WritableStreamDefaultWriter = globalThis.WritableStreamDefaultWriter;
+ export const WritableStreamDefaultController: typeof globalThis.WritableStreamDefaultController;
+ export type WritableStreamDefaultController = globalThis.WritableStreamDefaultController;
+ export const TransformStream: typeof globalThis.TransformStream;
+ export type TransformStream = globalThis.TransformStream;
+ export const TransformStreamDefaultController: typeof globalThis.TransformStreamDefaultController;
+ export type TransformStreamDefaultController =
+ globalThis.TransformStreamDefaultController;
+ export const CountQueuingStrategy: typeof globalThis.CountQueuingStrategy;
+ export type CountQueuingStrategy = globalThis.CountQueuingStrategy;
+ export const ByteLengthQueuingStrategy: typeof globalThis.ByteLengthQueuingStrategy;
+ export type ByteLengthQueuingStrategy = globalThis.ByteLengthQueuingStrategy;
+ export const TextEncoderStream: typeof globalThis.TextEncoderStream;
+ export type TextEncoderStream = globalThis.TextEncoderStream;
+ export const TextDecoderStream: typeof globalThis.TextDecoderStream;
+ export type TextDecoderStream = globalThis.TextDecoderStream;
export type UnderlyingSource = globalThis.UnderlyingSource;
export type QueuingStrategy = globalThis.QueuingStrategy;
}
diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts
index dff6b6b05..3f2124e28 100644
--- a/packages/compiler/ambient/scriptc-node-fallback.d.ts
+++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts
@@ -1084,6 +1084,12 @@ interface ReadableStreamDefaultController {
close(): void;
error(reason?: unknown): void;
}
+interface StreamPipeOptions {
+ preventAbort?: boolean;
+ preventCancel?: boolean;
+ preventClose?: boolean;
+ signal?: AbortSignal;
+}
interface UnderlyingSource {
start?(controller: ReadableStreamDefaultController): unknown;
pull?(controller: ReadableStreamDefaultController): unknown;
@@ -1093,6 +1099,12 @@ interface ReadableStream {
readonly locked: boolean;
cancel(reason?: unknown): Promise;
getReader(): ReadableStreamDefaultReader;
+ tee(): [ReadableStream, ReadableStream];
+ pipeTo(destination: WritableStream, options?: StreamPipeOptions): Promise;
+ pipeThrough(
+ transform: { readable: ReadableStream; writable: WritableStream },
+ options?: StreamPipeOptions,
+ ): ReadableStream;
[Symbol.asyncIterator](options?: { preventCancel?: boolean }): AsyncIterableIterator;
values(options?: { preventCancel?: boolean }): AsyncIterableIterator;
}
@@ -1131,6 +1143,79 @@ declare var TransformStream: {
new (): TransformStream;
readonly prototype: TransformStream;
};
+/* The other web stream interfaces. JavaScript sources construct and read
+ * them as checked-dynamic values (scr_webstreams.c); TypeScript sources
+ * have no static lowering for them. */
+declare var ReadableStreamDefaultReader: {
+ new (stream: ReadableStream): ReadableStreamDefaultReader;
+ readonly prototype: ReadableStreamDefaultReader;
+};
+declare var ReadableStreamDefaultController: {
+ new (): ReadableStreamDefaultController;
+ readonly prototype: ReadableStreamDefaultController;
+};
+interface WritableStreamDefaultController {
+ readonly signal: AbortSignal;
+ error(reason?: unknown): void;
+}
+declare var WritableStream: {
+ new (sink?: object, strategy?: QueuingStrategy): WritableStream;
+ readonly prototype: WritableStream;
+};
+declare var WritableStreamDefaultWriter: {
+ new (stream: WritableStream): WritableStreamDefaultWriter;
+ readonly prototype: WritableStreamDefaultWriter;
+};
+declare var WritableStreamDefaultController: {
+ new (): WritableStreamDefaultController;
+ readonly prototype: WritableStreamDefaultController;
+};
+interface TransformStreamDefaultController {
+ readonly desiredSize: number | null;
+ enqueue(chunk?: O): void;
+ error(reason?: unknown): void;
+ terminate(): void;
+}
+declare var TransformStreamDefaultController: {
+ new (): TransformStreamDefaultController;
+ readonly prototype: TransformStreamDefaultController;
+};
+interface CountQueuingStrategy extends QueuingStrategy {
+ readonly highWaterMark: number;
+ readonly size: QueuingStrategySize;
+}
+declare var CountQueuingStrategy: {
+ new (init: { highWaterMark: number }): CountQueuingStrategy;
+ readonly prototype: CountQueuingStrategy;
+};
+interface ByteLengthQueuingStrategy extends QueuingStrategy {
+ readonly highWaterMark: number;
+ readonly size: QueuingStrategySize;
+}
+declare var ByteLengthQueuingStrategy: {
+ new (init: { highWaterMark: number }): ByteLengthQueuingStrategy;
+ readonly prototype: ByteLengthQueuingStrategy;
+};
+interface TextEncoderStream {
+ readonly encoding: "utf-8";
+ readonly readable: ReadableStream;
+ readonly writable: WritableStream;
+}
+declare var TextEncoderStream: {
+ new (): TextEncoderStream;
+ readonly prototype: TextEncoderStream;
+};
+interface TextDecoderStream {
+ readonly encoding: string;
+ readonly fatal: boolean;
+ readonly ignoreBOM: boolean;
+ readonly readable: ReadableStream;
+ readonly writable: WritableStream;
+}
+declare var TextDecoderStream: {
+ new (label?: string, options?: { fatal?: boolean; ignoreBOM?: boolean }): TextDecoderStream;
+ readonly prototype: TextDecoderStream;
+};
/* fetch, typed as the native JSON + readable-stream slice: one URL, an
* optional init with the members CLI requests carry, and a Response
@@ -1665,4 +1750,5 @@ declare module "node:tty" {
interface ErrorConstructor {
stackTraceLimit: number;
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
}
diff --git a/packages/compiler/native/ts7-process-child.test.c b/packages/compiler/native/ts7-process-child.test.c
index 4d1cbc081..d387f735a 100644
--- a/packages/compiler/native/ts7-process-child.test.c
+++ b/packages/compiler/native/ts7-process-child.test.c
@@ -20,8 +20,9 @@
/* `--listen PATH --idle-exit SECONDS`: a stand-in for a shared server. In
* mode "listen-exit" it exits as an executable without --listen does. In
* mode "listen" it answers each client, in order, with its client number,
- * its listen arguments and TSC_TS_LISTEN_MEMORY, then echoes until the
- * client closes, and exits after SECONDS (at most 5) without a client. */
+ * its listen arguments, TSC_TS_LISTEN_MEMORY and its working directory, then
+ * echoes until the client closes, and exits after SECONDS (at most 5) without
+ * a client. */
static int listen_main(const char *mode, char **argv) {
if (strcmp(mode, "listen-exit") == 0) return 2;
if (strcmp(mode, "listen") != 0) return 73;
@@ -42,9 +43,10 @@ static int listen_main(const char *mode, char **argv) {
if (poll(&entry, 1, idle * 1000) <= 0) break;
int fd = accept(listener, NULL, NULL);
if (fd < 0) return 79;
- char line[512];
- int size = snprintf(line, sizeof(line), "client %d\n%s %s\n%s\n", client, argv[0], argv[2],
- memory == NULL ? "unset" : memory);
+ char line[1024], cwd[256];
+ if (getcwd(cwd, sizeof(cwd)) == NULL) return 81;
+ int size = snprintf(line, sizeof(line), "client %d\n%s %s\n%s\n%s\n", client, argv[0], argv[2],
+ memory == NULL ? "unset" : memory, cwd);
if (size <= 0 || write(fd, line, (size_t)size) != size) return 80;
uint8_t bytes[17];
ssize_t count;
diff --git a/packages/compiler/native/ts7-process.c b/packages/compiler/native/ts7-process.c
index 8261c341b..bf486c87d 100644
--- a/packages/compiler/native/ts7-process.c
+++ b/packages/compiler/native/ts7-process.c
@@ -414,8 +414,10 @@ static int connect_server(const char *path) {
/* Starts the server in its own process group, so that a terminal's signals
* to the build do not reach it, with its standard streams on /dev/null and
* no other descriptor of this process, so that it holds no pipe a caller of
- * the build waits on. `limit` (TSC_TS_LISTEN_MEMORY=...) is added to the
- * environment unless it sets TSC_TS_LISTEN_MEMORY. */
+ * the build waits on. It runs in the root directory, so that it does not
+ * keep the build's directory in use; its working directory for the build's
+ * files is the --cwd argument. `limit` (TSC_TS_LISTEN_MEMORY=...) is added
+ * to the environment unless it sets TSC_TS_LISTEN_MEMORY. */
static pid_t start_server(char **args, char *limit) {
#ifdef __APPLE__
char **inherited = *_NSGetEnviron();
@@ -440,7 +442,7 @@ static pid_t start_server(char **args, char *limit) {
// Only async-signal-safe calls until exec.
int null = open("/dev/null", O_RDWR);
if (setpgid(0, 0) != 0 || null < 0 || dup2(null, STDIN_FILENO) < 0 || dup2(null, STDOUT_FILENO) < 0 ||
- dup2(null, STDERR_FILENO) < 0)
+ dup2(null, STDERR_FILENO) < 0 || chdir("/") != 0)
_exit(127);
#if defined(__linux__) && defined(SYS_close_range)
if (syscall(SYS_close_range, 3U, ~0U, 0U) != 0)
diff --git a/packages/compiler/native/ts7-process.test.c b/packages/compiler/native/ts7-process.test.c
index 85de63db1..e07a7d10c 100644
--- a/packages/compiler/native/ts7-process.test.c
+++ b/packages/compiler/native/ts7-process.test.c
@@ -208,9 +208,9 @@ static void attach_cases(void) {
uint8_t bytes[4];
snprintf(directory, sizeof(directory), "%s/scriptc", runtime);
snprintf(socket_path, sizeof(socket_path), "%s/server.sock", directory);
- // The first build starts the server with its idle time and memory limit.
- // The server holds none of the build's descriptors, such as an output
- // pipe its caller reads to the end.
+ // The first build starts the server with its idle time and memory limit,
+ // in the root directory. The server holds none of the build's descriptors,
+ // such as an output pipe its caller reads to the end.
int output[2];
assert(pipe(output) == 0);
uint32_t first = attach("server", "listen", 123);
@@ -224,6 +224,7 @@ static void attach_cases(void) {
snprintf(expected, sizeof(expected), "%s 1\n", socket_path);
expect_line(first, expected);
expect_line(first, "123\n");
+ expect_line(first, "/\n");
write_exact(first, (const uint8_t *)"ping", 4, 0, 4);
read_exact(first, bytes, 4);
assert(memcmp(bytes, "ping", 4) == 0);
diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts
index 1d95285b4..c82828a62 100644
--- a/packages/compiler/src/backend/executable-features.ts
+++ b/packages/compiler/src/backend/executable-features.ts
@@ -36,6 +36,8 @@ export function executableLinkFeatures(mod: IrModule, largePages = false): Nativ
...(features.dynForAwait ? { dynForAwait: true } : {}),
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
...(features.mime ? { mime: true } : {}),
+ ...(features.webStreams ? { webStreams: true } : {}),
+ ...(features.abortSignal ? { abortSignal: true } : {}),
...(features.utilTiming ? { utilTiming: true } : {}),
...(features.dynBuffer ? { dynBuffer: true } : {}),
...(features.builtinValues ? { builtinValues: true } : {}),
diff --git a/packages/compiler/src/backend/huge-text.test.ts b/packages/compiler/src/backend/huge-text.test.ts
new file mode 100644
index 000000000..39ef52408
--- /dev/null
+++ b/packages/compiler/src/backend/huge-text.test.ts
@@ -0,0 +1,89 @@
+import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { afterEach, expect, test } from "vitest";
+import {
+ HUGE_TEXT_MIN_CODE_BYTES,
+ elfObjectCodeBytes,
+ hugeTextLinkerArgs,
+ hugeTextProgram,
+} from "./huge-text.js";
+
+const dirs: string[] = [];
+afterEach(() => {
+ for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
+});
+
+/** An ELF64 little-endian relocatable object's headers: one section per
+ * entry, as [type, flags, size]. Section contents are left out, since only
+ * the headers are read. */
+function elfObject(sections: readonly [number, number, number][]): Buffer {
+ const shoff = 64;
+ const bytes = Buffer.alloc(shoff + (sections.length + 1) * 64);
+ bytes.writeUInt32BE(0x7f454c46, 0);
+ bytes[4] = 2; // ELFCLASS64
+ bytes[5] = 1; // ELFDATA2LSB
+ bytes[6] = 1;
+ bytes.writeUInt16LE(1, 16); // ET_REL
+ bytes.writeUInt16LE(62, 18); // EM_X86_64
+ bytes.writeBigUInt64LE(BigInt(shoff), 40);
+ bytes.writeUInt16LE(64, 52);
+ bytes.writeUInt16LE(64, 58);
+ bytes.writeUInt16LE(sections.length + 1, 60);
+ sections.forEach(([type, flags, size], index) => {
+ const at = shoff + (index + 1) * 64;
+ bytes.writeUInt32LE(type, at + 4);
+ bytes.writeBigUInt64LE(BigInt(flags), at + 8);
+ bytes.writeBigUInt64LE(BigInt(size), at + 32);
+ });
+ return bytes;
+}
+
+function file(name: string, bytes: Buffer): string {
+ const dir = mkdtempSync(join(tmpdir(), "scriptc-huge-text-"));
+ dirs.push(dir);
+ const path = join(dir, name);
+ writeFileSync(path, bytes);
+ return path;
+}
+
+const PROGBITS = 1;
+const NOBITS = 8;
+const ALLOC = 0x2;
+const EXECINSTR = 0x4;
+
+test("an object's code is the size of its executable PROGBITS sections", () => {
+ const object = file(
+ "program.o",
+ elfObject([
+ [PROGBITS, ALLOC | EXECINSTR, 3 << 20],
+ [PROGBITS, ALLOC | EXECINSTR, 1000],
+ [PROGBITS, ALLOC, 5 << 20],
+ [NOBITS, ALLOC, 7 << 20],
+ ]),
+ );
+ expect(elfObjectCodeBytes(object)).toBe((3 << 20) + 1000);
+ expect(elfObjectCodeBytes(file("not-elf.o", Buffer.from("not an object")))).toBe(0);
+ expect(elfObjectCodeBytes(join(object, "..", "missing.o"))).toBe(0);
+});
+
+test("only Linux programs with an interior 2 MiB window of code link for huge pages", () => {
+ const linux = { platform: "linux" } as const;
+ const half = file(
+ "half.o",
+ elfObject([[PROGBITS, ALLOC | EXECINSTR, HUGE_TEXT_MIN_CODE_BYTES / 2]]),
+ );
+ const small = file("small.o", elfObject([[PROGBITS, ALLOC | EXECINSTR, 1 << 20]]));
+ expect(hugeTextProgram(linux, [half, small])).toBe(false);
+ // Partitions of one program count together.
+ expect(hugeTextProgram(linux, [half, half])).toBe(true);
+ expect(hugeTextProgram({ platform: "darwin" } as const, [half, half])).toBe(false);
+ expect(hugeTextProgram(linux, [])).toBe(false);
+});
+
+test("the 2 MiB alignment applies to lld and zig links of huge-text programs", () => {
+ expect(hugeTextLinkerArgs(true, true)).toEqual(["-Wl,-z,max-page-size=0x200000"]);
+ // GNU ld pads the file to the alignment.
+ expect(hugeTextLinkerArgs(true, false)).toEqual([]);
+ expect(hugeTextLinkerArgs(false, true)).toEqual([]);
+});
diff --git a/packages/compiler/src/backend/huge-text.ts b/packages/compiler/src/backend/huge-text.ts
new file mode 100644
index 000000000..4db824074
--- /dev/null
+++ b/packages/compiler/src/backend/huge-text.ts
@@ -0,0 +1,110 @@
+/**
+ * Text on 2 MiB pages for large Linux executables.
+ *
+ * The kernel maps a 2 MiB window of an executable's text with one huge page
+ * when the window's virtual address and file offset are both 2 MiB aligned
+ * and the page cache holds the window as one 2 MiB folio. A link with
+ * `-z max-page-size=0x200000` keeps every segment's address congruent to its
+ * file offset modulo 2 MiB, so every 2 MiB window inside the text segment
+ * qualifies. lld and zig add no file padding for it; GNU ld pads the file to
+ * the alignment, so links that fall back to it leave the flag out.
+ *
+ * The alignment also aligns a position-independent executable's load
+ * address to 2 MiB, which leaves 19 bits of address randomization for it
+ * instead of 28. Only executables whose program code fills an interior 2 MiB
+ * window can gain from huge pages, so smaller executables link as before.
+ *
+ * lld writes its output through a memory mapping, which leaves the file in
+ * the page cache as 4 KiB folios. A large executable is published by writing
+ * its bytes with one write instead, which leaves 2 MiB folios where the file
+ * system supports them. The runtime advises the interior windows huge pages
+ * at startup (scr_lib.c), so a file that is read back from disk is read as
+ * 2 MiB folios too.
+ */
+import {
+ chmodSync,
+ closeSync,
+ openSync,
+ readFileSync,
+ readSync,
+ renameSync,
+ statSync,
+ writeFileSync,
+} from "node:fs";
+import type { NativeTargetSpec } from "./targets.js";
+
+/** Program code from which an executable is linked for huge pages: text of
+ * this size has at least one 2 MiB window inside it wherever it starts. */
+export const HUGE_TEXT_MIN_CODE_BYTES = 4 << 20;
+
+const SHF_EXECINSTR = 0x4n;
+
+/** The bytes of executable sections in an ELF64 little-endian object, or 0
+ * when the file is not one or cannot be read. Reads only the file and
+ * section headers. */
+export function elfObjectCodeBytes(path: string): number {
+ let fd: number;
+ try {
+ fd = openSync(path, "r");
+ } catch {
+ return 0;
+ }
+ try {
+ const header = Buffer.alloc(64);
+ if (readSync(fd, header, 0, 64, 0) !== 64) return 0;
+ const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
+ if (view.getUint32(0, false) !== 0x7f454c46 || header[4] !== 2 || header[5] !== 1) return 0;
+ const shoff = Number(view.getBigUint64(40, true));
+ const shentsize = view.getUint16(58, true);
+ let shnum = view.getUint16(60, true);
+ if (shentsize !== 64 || shoff === 0) return 0;
+ if (shnum === 0) {
+ const first = Buffer.alloc(64);
+ if (readSync(fd, first, 0, 64, shoff) !== 64) return 0;
+ shnum = Number(new DataView(first.buffer, first.byteOffset, 64).getBigUint64(32, true));
+ }
+ const table = Buffer.alloc(shnum * 64);
+ if (readSync(fd, table, 0, table.length, shoff) !== table.length) return 0;
+ const sections = new DataView(table.buffer, table.byteOffset, table.byteLength);
+ let code = 0;
+ for (let i = 0; i < shnum; i++) {
+ const at = i * 64;
+ // SHT_PROGBITS sections with SHF_EXECINSTR.
+ if (sections.getUint32(at + 4, true) !== 1) continue;
+ if ((sections.getBigUint64(at + 8, true) & SHF_EXECINSTR) === 0n) continue;
+ code += Number(sections.getBigUint64(at + 32, true));
+ }
+ return code;
+ } finally {
+ closeSync(fd);
+ }
+}
+
+/** Whether a link of these program objects is laid out for huge pages. */
+export function hugeTextProgram(
+ target: Pick,
+ programObjects: readonly string[],
+): boolean {
+ if (target.platform !== "linux") return false;
+ let code = 0;
+ for (const object of programObjects) {
+ code += elfObjectCodeBytes(object);
+ if (code >= HUGE_TEXT_MIN_CODE_BYTES) return true;
+ }
+ return false;
+}
+
+/** The link arguments that align a huge-text executable's segments to
+ * 2 MiB, for links by lld or zig. */
+export function hugeTextLinkerArgs(hugeText: boolean, linksWithLldOrZig: boolean): string[] {
+ return hugeText && linksWithLldOrZig ? ["-Wl,-z,max-page-size=0x200000"] : [];
+}
+
+/** Rewrites a linked huge-text executable with one write, so the page cache
+ * holds it in 2 MiB folios; its caller then publishes it as usual. */
+export function rewriteHugeTextExecutable(linked: string): void {
+ const written = `${linked}.written`;
+ writeFileSync(written, readFileSync(linked));
+ chmodSync(written, statSync(linked).mode & 0o7777);
+ renameSync(written, linked);
+}
diff --git a/packages/compiler/src/backend/link-plan-core.ts b/packages/compiler/src/backend/link-plan-core.ts
index f810a4a17..847dca065 100644
--- a/packages/compiler/src/backend/link-plan-core.ts
+++ b/packages/compiler/src/backend/link-plan-core.ts
@@ -8,7 +8,7 @@ import {
type NativeTargetSpec,
type WindowsSubsystem,
} from "./targets.js";
-import { profileGenerateLinkerArgs } from "./native-profile.js";
+import { profileGenerateLinkerArgs, profileLayoutLinkerArgs } from "./native-profile.js";
export interface ExecutableLinkInputs {
inputs: string[];
@@ -34,6 +34,8 @@ export function executableLinkInputs(options: {
windowsSubsystem?: WindowsSubsystem;
/** Instrumented for profile-guided optimization. */
profileGenerate?: boolean;
+ /** Laid out by a profile's context-sensitive counts (nativeProfileLayout). */
+ profileLayout?: boolean;
}): ExecutableLinkInputs {
if (options.ffiFrameworks.length !== 0 && options.target.platform !== "darwin")
throw new Error("FFI frameworks require a Darwin target");
@@ -71,6 +73,7 @@ export function executableLinkInputs(options: {
...executableStaticLinkerArgs(options.target, options.staticLink ?? false),
...windowsSubsystemLinkerArgs(options.target.platform, options.windowsSubsystem),
...profileGenerateLinkerArgs(options.profileGenerate ?? false),
+ ...profileLayoutLinkerArgs(options.target, options.profileLayout ?? false),
],
};
}
diff --git a/packages/compiler/src/backend/link-plan.ts b/packages/compiler/src/backend/link-plan.ts
index 73139d22a..19cff2d5f 100644
--- a/packages/compiler/src/backend/link-plan.ts
+++ b/packages/compiler/src/backend/link-plan.ts
@@ -13,6 +13,7 @@ import type { NativeArtifactDependency } from "./native/contracts.js";
import { needsDarwinDebugSymbols } from "./debug-symbols.js";
import { loadRuntimePack, type RuntimePackSelection } from "./runtime-pack.js";
import { executableLinkInputs } from "./link-plan-core.js";
+import { hugeTextProgram } from "./huge-text.js";
import type { NativeTargetSpec, WindowsSubsystem } from "./targets.js";
export interface NativeLinkPlan {
@@ -30,6 +31,8 @@ export interface NativeLinkPlan {
darwinDebugSymbols?: boolean;
/** Linked with `--static`: the linked executable is checked before use. */
staticLink?: boolean;
+ /** Program code large enough for text on huge pages (huge-text.ts). */
+ hugeText?: boolean;
}
export async function createNativeLinkPlan(options: {
@@ -44,6 +47,7 @@ export async function createNativeLinkPlan(options: {
staticLink?: boolean;
windowsSubsystem?: WindowsSubsystem;
profileGenerate?: boolean;
+ profileLayout?: boolean;
programObjectDependencies?: readonly NativeArtifactDependency[];
env?: NodeJS.ProcessEnv;
resolver?: (specifier: string) => string;
@@ -78,10 +82,17 @@ export async function createNativeLinkPlan(options: {
? {}
: { windowsSubsystem: options.windowsSubsystem }),
...(options.profileGenerate === true ? { profileGenerate: true } : {}),
+ ...(options.profileLayout === true ? { profileLayout: true } : {}),
}),
dependencyPaths: [...runtimePack.dependencyPaths, ...(options.ffi?.libraries ?? [])],
programObjectDependencies: [...(options.programObjectDependencies ?? [])],
runtimePack,
...(options.staticLink === true ? { staticLink: true } : {}),
+ ...(hugeTextProgram(options.target, [
+ options.programObject,
+ ...(options.programPartitions ?? []),
+ ])
+ ? { hugeText: true }
+ : {}),
};
}
diff --git a/packages/compiler/src/backend/linker.ts b/packages/compiler/src/backend/linker.ts
index fe476360d..ab76597ed 100644
--- a/packages/compiler/src/backend/linker.ts
+++ b/packages/compiler/src/backend/linker.ts
@@ -24,6 +24,7 @@ import { type NativeArtifactDependency } from "./native/contracts.js";
import { RuntimePackError, stageRuntimePackArtifacts } from "./runtime-pack.js";
import type { NativeLinkPlan } from "./link-plan.js";
import { inputsWithoutLld, lldLinkerArgs } from "./lld.js";
+import { hugeTextLinkerArgs, rewriteHugeTextExecutable } from "./huge-text.js";
import { systemLibraryLinkerArgs } from "./link-plan-core.js";
import {
createDarwinDebugSymbols,
@@ -248,11 +249,13 @@ export async function linkNativeExecutable(
stagedRoot = staged.root;
// Linux links with lld when the default driver finds it.
const lld = explicitLinker === undefined ? lldLinkerArgs(plan.target.platform, linker) : [];
+ const zig = explicitLinker === undefined && plan.target.defaultLinker === "zig";
const linkArgs = (withLld: boolean): string[] => {
const inputs = plan.inputs.map((input) => staged.replacements.get(input) ?? input);
return [
...(explicitLinker === undefined ? plan.target.defaultLinkerArgs : []),
...(withLld ? lld : []),
+ ...hugeTextLinkerArgs(plan.hugeText === true, (withLld && lld.length !== 0) || zig),
...plan.driverFlags,
...mapArgs,
...(withLld ? inputs : inputsWithoutLld(plan.target.platform, inputs)),
@@ -298,6 +301,7 @@ export async function linkNativeExecutable(
});
const output = await stat(privateOut);
if (!output.isFile() || output.size === 0) throw new Error("linker produced no executable");
+ if (plan.hugeText === true) rewriteHugeTextExecutable(privateOut);
if (mapArgs.length !== 0) {
const refusal = staticExecutableRefusal(plan.target, true, await readFile(linkMap, "utf8"));
if (refusal !== null) throw new NativeCodegenError("SC3002", refusal);
diff --git a/packages/compiler/src/backend/llvm/call-lifetimes.ts b/packages/compiler/src/backend/llvm/call-lifetimes.ts
index becd24805..dda240fc7 100644
--- a/packages/compiler/src/backend/llvm/call-lifetimes.ts
+++ b/packages/compiler/src/backend/llvm/call-lifetimes.ts
@@ -9,7 +9,8 @@ import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
import { borrowsStringInputs } from "./string-lifetimes.js";
import { borrowsMapReadInputs } from "./map-read-lifetimes.js";
import { LazyCaptures } from "./lazy-captures.js";
-import { storedLastUseRead } from "./local-extents.js";
+import { BodyLastUses, storedLastUseRead } from "./local-extents.js";
+import { InternalCompilerError } from "../../errors.js";
interface ForwardedUse {
callee: string;
@@ -497,6 +498,9 @@ export function analyzeCallLifetimes(
// environment box takes a reference of its own when it is created.
const unchangedCaptures = lazyCaptures.parameters(fn);
const walks = walkParameters.get(fn.name);
+ // One walk of the body answers every parameter's last use (consumedParameter).
+ let lastUses: BodyLastUses | undefined;
+ const bodyLastUses = (): BodyLastUses => (lastUses ??= new BodyLastUses(fn.body));
fn.params.forEach((param, index) => {
const local = locals.get(param.localId);
if (
@@ -511,7 +515,7 @@ export function analyzeCallLifetimes(
!walks?.has(index) &&
!unchangedCaptures.has(local.id) &&
param.name !== "this" &&
- consumedParameter(fn, param.localId)
+ consumedParameter(fn, param.localId, bodyLastUses)
)
consumed.push({ fn: fn.name, index });
}
@@ -626,6 +630,9 @@ export function analyzeCallLifetimes(
return result;
}
+/** SCRIPTC_EMIT_FACTS_VERIFY=1: every BodyLastUses answer is checked against storedLastUseRead. */
+const verifiesLastUses = process.env["SCRIPTC_EMIT_FACTS_VERIFY"] === "1";
+
/** A parameter whose last use in the body stores it (a field, record or
* element store, a record literal field, a push) and moves its reference
* there (lastUseRead). Its owned convention lets a caller hand over a
@@ -633,8 +640,12 @@ export function analyzeCallLifetimes(
* copy while the caller releases its temporary right after the call. The
* read must be the only node of its kind in the function, like every
* moved read. */
-function consumedParameter(fn: IrFunction, localId: string): boolean {
- const read = storedLastUseRead(fn.body, -1, localId, true);
+function consumedParameter(fn: IrFunction, localId: string, lastUses: () => BodyLastUses): boolean {
+ const read = lastUses().storedRead(localId);
+ if (verifiesLastUses && read !== storedLastUseRead(fn.body, -1, localId, true))
+ throw new InternalCompilerError(
+ `call lifetimes: the body's last use of ${localId} in ${fn.name} differs`,
+ );
if (read === null) return false;
let count = 0;
const expr = (e: IrExpr): boolean => {
diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts
index dd639393f..e4e8bd459 100644
--- a/packages/compiler/src/backend/llvm/classes.ts
+++ b/packages/compiler/src/backend/llvm/classes.ts
@@ -863,6 +863,20 @@ function undefFieldInits(
host: ClassHost,
meta: LlClassMeta,
nullable: NullableRefFields | undefined,
+): string[] {
+ return classUndefinedFieldInits(host, meta, nullable, "%o", "%uf");
+}
+
+/** The stores that start an instance's union and dyn fields at undefined,
+ * for the instance at `object`, with temporaries named ``
+ * and `v`. A nullable-pointer field's zeroed slot already is
+ * its unit arm. */
+export function classUndefinedFieldInits(
+ host: ClassHost,
+ meta: LlClassMeta,
+ nullable: NullableRefFields | undefined,
+ object: string,
+ prefix: string,
): string[] {
const out: string[] = [];
meta.def.fields.forEach((f, i) => {
@@ -880,17 +894,17 @@ function undefFieldInits(
const tag = undefinedArmTag(f.type, host.unionsById);
if (tag < 0) return;
out.push(
- ` %uf${i} = getelementptr inbounds %${mangleClassStruct(meta.def.name)}, ptr %o, i64 0, i32 ${index}, i32 0`,
- ` store i32 ${tag}, ptr %uf${i} ; ${llvmCommentText(f.name)} starts undefined`,
+ ` ${prefix}${i} = getelementptr inbounds %${mangleClassStruct(meta.def.name)}, ptr ${object}, i64 0, i32 ${index}, i32 0`,
+ ` store i32 ${tag}, ptr ${prefix}${i} ; ${llvmCommentText(f.name)} starts undefined`,
);
return;
}
if (f.type.kind === "dyn") {
host.declare("declare ptr @scr_dyn_undefined()");
out.push(
- ` %ufv${i} = call ptr @scr_dyn_undefined()`,
- ` %uf${i} = getelementptr inbounds %${mangleClassStruct(meta.def.name)}, ptr %o, i64 0, i32 ${index}`,
- ` store ptr %ufv${i}, ptr %uf${i} ; ${llvmCommentText(f.name)} starts undefined`,
+ ` ${prefix}v${i} = call ptr @scr_dyn_undefined()`,
+ ` ${prefix}${i} = getelementptr inbounds %${mangleClassStruct(meta.def.name)}, ptr ${object}, i64 0, i32 ${index}`,
+ ` store ptr ${prefix}v${i}, ptr ${prefix}${i} ; ${llvmCommentText(f.name)} starts undefined`,
);
return;
}
@@ -898,8 +912,8 @@ function undefFieldInits(
const tag = undefinedArmTag(f.type, host.unionsById);
if (tag < 0) return;
out.push(
- ` %uf${i} = getelementptr inbounds %${mangleClassStruct(meta.def.name)}, ptr %o, i64 0, i32 ${index}`,
- ` store ptr ${host.unitInstanceRef(f.type.unionId, tag)}, ptr %uf${i} ; ${llvmCommentText(f.name)} starts undefined`,
+ ` ${prefix}${i} = getelementptr inbounds %${mangleClassStruct(meta.def.name)}, ptr ${object}, i64 0, i32 ${index}`,
+ ` store ptr ${host.unitInstanceRef(f.type.unionId, tag)}, ptr ${prefix}${i} ; ${llvmCommentText(f.name)} starts undefined`,
);
});
return out;
diff --git a/packages/compiler/src/backend/llvm/counted-loops.test.ts b/packages/compiler/src/backend/llvm/counted-loops.test.ts
new file mode 100644
index 000000000..b5cd5dd24
--- /dev/null
+++ b/packages/compiler/src/backend/llvm/counted-loops.test.ts
@@ -0,0 +1,46 @@
+import { expect, test } from "vitest";
+import type { IntegerCountedForLoop } from "../../ir/integer-loops.js";
+import { F64, type IrExpr } from "../../ir/ir.js";
+import type { IntegerRange } from "../../ir/integer-ranges.js";
+import { BlockBuilder } from "./blocks.js";
+import { emitCountedLoopLimit } from "./counted-loops.js";
+import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
+
+const loc = { file: "test.ts", start: 0, end: 0 };
+
+/** The endpoint emitCountedLoopLimit answers for a limit that emits as
+ * `value` and has the proven `range`, and the lines it writes. */
+function endpoint(
+ value: LlValue,
+ range: IntegerRange,
+ step: number,
+ inclusive: boolean,
+): { endpoint: string; lines: string } {
+ const limit: IrExpr = { loc, kind: "varRef", localId: "n", type: F64 };
+ const B = new BlockBuilder();
+ const host = {
+ B,
+ integerRanges: new Map([[limit, range]]),
+ emitExpr: () => value,
+ declare: () => {},
+ } as unknown as LlvmEmitterContext;
+ const loop = { step, inclusive, limit } as unknown as IntegerCountedForLoop;
+ const result = emitCountedLoopLimit(host, loop);
+ return { endpoint: result, lines: B.render() };
+}
+
+test("a negative int32 limit whose ToUint32 view is a literal keeps its sign", () => {
+ // A number literal's value carries its ToUint32 bits as a literal, so -1
+ // has the view 4294967295; the endpoint is -1, as in `i > -1`.
+ const minusOne: LlValue = { name: "-1.0", type: F64, uint32: "4294967295" };
+ expect(endpoint(minusOne, { min: -1, max: -1 }, -1, false).endpoint).toBe("-1");
+ const inclusive = endpoint(minusOne, { min: -1, max: -1 }, -1, true);
+ expect(inclusive.lines).toContain("add nsw i64 -1, -1");
+ const minusTwo: LlValue = { name: "-2.0", type: F64, uint32: "4294967294" };
+ expect(endpoint(minusTwo, { min: -2, max: -2 }, 1, false).endpoint).toBe("-2");
+ // A uint32 past int32 stays positive.
+ const wide: LlValue = { name: "4294967295.0", type: F64, uint32: "4294967295" };
+ expect(endpoint(wide, { min: 4294967295, max: 4294967295 }, 1, false).endpoint).toBe(
+ "4294967295",
+ );
+});
diff --git a/packages/compiler/src/backend/llvm/counted-loops.ts b/packages/compiler/src/backend/llvm/counted-loops.ts
index 88af25e47..454fea32a 100644
--- a/packages/compiler/src/backend/llvm/counted-loops.ts
+++ b/packages/compiler/src/backend/llvm/counted-loops.ts
@@ -13,7 +13,10 @@ import { integerOrNaNRange } from "./wrapping-sums.js";
/** The caller has proved or guarded the direction's finite endpoint. Clamp
* the opposite infinity before integer conversion. Fractional limits become
- * exclusive integer endpoints for signed comparisons in either direction. */
+ * exclusive integer endpoints for signed comparisons in either direction. A
+ * limit that is an exact integer (an int32 local, an integer view, or a
+ * proven range) is its own floor and ceiling and within ±2^53, so it is the
+ * endpoint without the double conversion and rounding. */
export function emitCountedLoopLimit(
host: LlvmEmitterContext,
loop: IntegerCountedForLoop,
@@ -35,6 +38,24 @@ export function emitCountedLoopLimit(
}
const B = host.B;
const value = host.emitExpr(loop.limit);
+ const exact = exactInteger(host, value, loop.limit);
+ if (exact) {
+ // An i32 literal may be the unsigned bit pattern of a negative int32
+ // (a ToUint32 view): read it back with the integer's signedness.
+ const integer = /^-?\d+$/.test(exact.name)
+ ? String(
+ exact.type === "i64"
+ ? Number(exact.name)
+ : exact.signed
+ ? Number(exact.name) | 0
+ : Number(exact.name) >>> 0,
+ )
+ : widenInteger(host, exact);
+ if (!loop.inclusive) return integer;
+ const limit = B.tmp();
+ B.line(`${limit} = add nsw i64 ${integer}, ${ascending ? 1 : -1}`);
+ return limit;
+ }
const within = B.tmp(),
bounded = B.tmp(),
rounded = B.tmp(),
diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts
index 762bc52f4..dfeffdecb 100644
--- a/packages/compiler/src/backend/llvm/dyn.ts
+++ b/packages/compiler/src/backend/llvm/dyn.ts
@@ -1,5 +1,5 @@
import { emitPerKeyOrder } from "./key-orders.js";
-import { rcMembers, typedRefConstructor } from "./shapes.js";
+import { typedRefConstructor } from "./shapes.js";
import { bindLiveArrayOps } from "./expr-stream-bridges.js";
import {
BYTES_ELEM_NUM,
@@ -45,7 +45,6 @@ import {
DYN_HANDLE_KINDS,
isDirentShape,
KEY_ORDER_FIELD,
- shapeHasAccessorSlots,
shapeKeyOrders,
isDynTypedRefType,
isRefCounted,
@@ -261,7 +260,7 @@ export class LlDyn {
/** Emitted function definitions, in interning order. */
readonly defs: string[] = [];
- private readonly prefixSources = new Map();
+ private readonly recordViewTables = new Map();
constructor(private readonly host: DynHost) {}
@@ -1161,51 +1160,47 @@ export class LlDyn {
);
}
- /** The record shapes whose values a pointer of shape `targetId` may
- * reference unchanged: their struct begins with exactly the target's
- * fields (same names, types and representations, so the same offsets),
- * and everything the target's release, teardown and collector support
- * would get wrong on them is absent. The extra fields hold no
- * references (the target's release frees the block without visiting
- * them; frees are size-class agnostic), neither shape carries a shape
- * word, a cycle header, an overflow map, key order variants or accessor
- * slots, and both freeze alike. A record that keeps undeclared source
- * properties is refused at run time (scr_dyn_typed_ref_prefix_view). */
- private prefixViewSources(targetId: string): string[] {
- const memo = this.prefixSources.get(targetId);
- if (memo) return memo;
+ /** `scr_dyn_record_view(d, sources, n, key, key_len, new)` for
+ * view target `targetId`: the view (+1) of the record a capsule `%d`
+ * holds when its type is one of the target's sources, else NULL. The
+ * sources table lists each source's type key and the byte offsets of
+ * the target's fields in it. */
+ private recordViewCall(B: BlockBuilder, targetId: string): string {
const host = this.host;
- const plain = (shape: IrRecordShape): boolean =>
- !shape.tuple &&
- !shape.indexValue &&
- !isDirentShape(shape) &&
- !shapeHasAccessorSlots(shape) &&
- shapeKeyOrders(shape).length === 1 &&
- !shape.fields.some((field) => field.name === KEY_ORDER_FIELD) &&
- host.nullableUnions.shapeNumber(shape.id) === 0 &&
- !host.tracedShapes.has(`record:${shape.id}`);
- const target = host.recordsById.get(targetId);
- const out: string[] = [];
- if (target && plain(target) && target.fields.length > 0) {
- const base = host.nullableUnions.fieldBase(targetId);
- for (const shape of host.recordsById.values()) {
- if (shape.id === targetId || shape.fields.length <= target.fields.length) continue;
- if (!plain(shape) || shape.freezable !== target.freezable) continue;
- if (host.nullableUnions.fieldBase(shape.id) !== base) continue;
- const prefix = target.fields.every(
- (field, i) =>
- shape.fields[i]!.name === field.name &&
- typeKey(shape.fields[i]!.type) === typeKey(field.type),
+ let table = this.recordViewTables.get(targetId);
+ const sources = host.recordViews!.sources(targetId);
+ const target = host.recordsById.get(targetId)!;
+ if (table === undefined) {
+ table = `@sc_rv_${this.recordViewTables.size}`;
+ this.recordViewTables.set(targetId, table);
+ const S = this.S;
+ const entries = sources.map((sourceId, k) => {
+ const source = host.recordsById.get(sourceId)!;
+ 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 offsetTable = `${table}_${k}`;
+ this.defs.push(
+ `${offsetTable} = internal constant [${offsets.length} x ${S}] [${offsets.join(", ")}]`,
);
- if (!prefix) continue;
- const extra = rcMembers(shape, host.inlineUnions, base).some(
- (member) => member.index >= target.fields.length + base && isRefCounted(member.type),
- );
- if (!extra) out.push(shape.id);
- }
+ const key = typeKey({ kind: "record", shapeId: sourceId });
+ return `{ ptr, ${S}, ptr } { ptr ${host.cstr(key)}, ${S} ${Buffer.byteLength(key, "utf8")}, ptr ${offsetTable} }`;
+ });
+ this.defs.push(
+ `${table} = internal constant [${entries.length} x { ptr, ${S}, ptr }] [${entries.join(", ")}]`,
+ ``,
+ );
}
- this.prefixSources.set(targetId, out);
- return out;
+ const key = typeKey({ kind: "record", shapeId: targetId });
+ host.declare(`declare ptr @scr_dyn_record_view(ptr, ptr, ${this.S}, ptr, ${this.S}, ptr)`);
+ const view = B.tmp();
+ B.line(
+ `${view} = call ptr @scr_dyn_record_view(ptr %d, ptr ${table}, ${this.S} ${sources.length}, ptr ${host.cstr(key)}, ${this.S} ${Buffer.byteLength(key, "utf8")}, ptr @${mangleRecordNew(targetId)})`,
+ );
+ return view;
}
private typedRefMatchesAny(B: BlockBuilder, types: readonly IrType[]): string {
@@ -2222,31 +2217,19 @@ export class LlDyn {
B.line(`${ref} = call ptr @scr_dyn_typed_ref_unbox(ptr %d)`);
B.terminate(`ret ptr ${ref}`);
B.startBlock(lNext);
- if (t.kind === "record") {
- // A record whose storage begins with exactly this type's storage
- // is this type's value too: the conversion passes the pointer, so
- // both names keep one object (decision D3).
- const sources = this.prefixViewSources(t.shapeId);
- if (sources.length > 0) {
- const viewed = this.typedRefMatchesAny(
- B,
- sources.map((shapeId) => ({ kind: "record", shapeId })),
- );
- const lView = B.newLabel("dc.prefix");
- const lOther = B.newLabel("dc.noprefix");
- B.condBr(viewed, lView, lOther);
- B.startBlock(lView);
- host.declare(`declare ptr @scr_dyn_typed_ref_prefix_view(ptr)`);
- const view = B.tmp();
- B.line(`${view} = call ptr @scr_dyn_typed_ref_prefix_view(ptr %d)`);
- const passed = B.tmp();
- B.line(`${passed} = icmp ne ptr ${view}, null`);
- const lPass = B.newLabel("dc.prefix.pass");
- B.condBr(passed, lPass, lOther);
- B.startBlock(lPass);
- B.terminate(`ret ptr ${view}`);
- B.startBlock(lOther);
- }
+ if (t.kind === "record" && host.recordViews?.targets.has(t.shapeId) === true) {
+ // A capsule of a record with all of this type's fields converts
+ // to a view of that record: both names keep one object
+ // (record-views.ts, decision D3).
+ const view = this.recordViewCall(B, t.shapeId);
+ const viewed = B.tmp();
+ B.line(`${viewed} = icmp ne ptr ${view}, null`);
+ const lView = B.newLabel("dc.view");
+ const lOther = B.newLabel("dc.noview");
+ B.condBr(viewed, lView, lOther);
+ B.startBlock(lView);
+ B.terminate(`ret ptr ${view}`);
+ B.startBlock(lOther);
}
// A constructor-function stream (scr_dyn_builtin_stream.c) carries
// its runtime stream.
diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts
index 7732fb8be..de43d3c67 100644
--- a/packages/compiler/src/backend/llvm/emit-statements.ts
+++ b/packages/compiler/src/backend/llvm/emit-statements.ts
@@ -153,7 +153,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
break;
}
const stackObject = emitter.localStackObjects.get(s.localId);
- if (stackObject) {
+ if (stackObject && b.kind !== "boxed") {
const value = emitStackObject(emitter, stackObject, (fields) =>
emitter.ownScopeFields(fields),
);
@@ -222,7 +222,11 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
B.line(`store ptr ${stack.box}, ptr ${b.slot}`);
if (stack.owner) emitter.scopes[emitter.scopes.length - 1]!.push(stack.owner);
if (s.init) {
- const value = emitter.emitExpr(s.init);
+ const value = stackObject
+ ? emitter.markImmortal(
+ emitStackObject(emitter, stackObject, (fields) => emitter.ownScopeFields(fields)),
+ )
+ : emitter.emitExpr(s.init);
if (isRefCounted(value.type)) emitter.toFrame(value);
emitter.boxSet(stack.box, b.type, value.name);
}
@@ -253,7 +257,13 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
if (sentinel !== null) B.line(`store ptr ${sentinel}, ptr ${emitter.boxSlot(box)}`);
break;
}
- const v = emitter.emitExpr(s.init);
+ // A frame object's binding box holds its frame storage, which the
+ // closures that capture the box never keep (frame-escapes.ts).
+ const v = stackObject
+ ? emitter.markImmortal(
+ emitStackObject(emitter, stackObject, (fields) => emitter.ownScopeFields(fields)),
+ )
+ : emitter.emitExpr(s.init);
if (isRefCounted(v.type)) emitter.toHeap(v); // the box takes ownership
if (b.local!.tdz === true && boxAccess(b.type) !== "ref") {
emitter.tdzScalarInit(box, b.type, v.name);
@@ -2992,7 +3002,12 @@ function emitProvenLoop(
}
const deferred = new Set();
for (const id of plan.locals.keys()) deferred.add(id);
- emitter.speculation = { writes: plan.writes, resume: new Map(), deferred };
+ emitter.speculation = {
+ writes: plan.writes,
+ resume: new Map(),
+ deferred,
+ increments: plan.increments,
+ };
try {
const leave = B.newLabel("integer.leave");
emitCopy(leave);
diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts
index e6c1b98c5..819782aa0 100644
--- a/packages/compiler/src/backend/llvm/emitter.ts
+++ b/packages/compiler/src/backend/llvm/emitter.ts
@@ -1,7 +1,9 @@
import type { TargetCpu } from "../target-cpu.js";
import { loopEntryInlines } from "./loop-entry-versions.js";
import type { FunctionLayout } from "./function-layout.js";
+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 { JsIcSites } from "../../ir/js-ic.js";
@@ -357,6 +359,7 @@ import {
import { children, type Node } from "./region-ir.js";
import { LlDyn, recordDynKeepsIdentity, type DynHost } from "./dyn.js";
import { VirtualBorrows } from "./virtual-borrows.js";
+import { planRecordViews, recordViewFieldPtr } from "./record-views.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import { LlWalkers } from "./walkers.js";
import { isObjectArm, NULLABLE_ABSENT, NULLABLE_NULL, NullableUnions } from "./nullable-unions.js";
@@ -520,6 +523,11 @@ export interface LlvmTargetOptions {
forkPrimitives?: ForkPrimitives | undefined;
/** Functions to write first, by tier (function-layout.ts). */
functionLayout?: FunctionLayout;
+ /** Marks emission's serial steps: `llvm-emit-rewrites` after the module
+ * passes, `llvm-emit-int32` after int32 selection, and
+ * `llvm-emit-analyses` after the emitter's whole-program analyses. The
+ * caller's next phase is the function bodies. */
+ timing?: CompilationTiming;
/** Exact frontend sources for dev-build locations and variable metadata. */
debugSources?: ReadonlyMap;
/** Pointer width of the target C ABI. Native targets are 64-bit today. */
@@ -701,6 +709,7 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
};
// Keep source storage intact for debugger inspection in dev builds.
if (options.debugSources !== undefined) return new LlEmitter(mod, options, null, null);
+ const timing = options.timing ?? (() => {});
const passed = versionParameters(
addRecordResultEntries(
scalarizeLiteralResults(
@@ -732,6 +741,7 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
),
),
);
+ timing("llvm-emit-rewrites");
let emitted = selectInt32Arrays(passed, proven, resultEntryTargets);
let prepared = rounds.last;
// A proof that revised functions analyzed the module of the functions it
@@ -753,7 +763,15 @@ function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
}
if (process.env["SCRIPTC_INT32_VERIFY"] === "1")
verifyInt32Selection(passed, emitted, prepared?.mod === emitted ? prepared : null);
- return new LlEmitter(emitted, options, null, prepared?.mod === emitted ? prepared : null);
+ timing("llvm-emit-int32");
+ const emitter = new LlEmitter(
+ emitted,
+ options,
+ null,
+ prepared?.mod === emitted ? prepared : null,
+ );
+ timing("llvm-emit-analyses");
+ return emitter;
}
/** SCRIPTC_INT32_VERIFY=1: throws unless the selection with revisions
@@ -1396,6 +1414,8 @@ export class LlEmitter {
readonly dyn: LlDyn;
/** Explicit callback boundary for standalone layout and walker stages. */
readonly shapeHost: DynHost;
+ /** The record view plan's target set (record-views.ts), shared by reference. */
+ private recordViewTargets: Set | null = null;
/** External declarations, in first-use order. */
readonly decls = new Set();
/** Declared functions referenced as values: each needs an env-signature
@@ -1440,6 +1460,9 @@ export class LlEmitter {
/** The current function's fused string-key Map get-then-set pairs (map-entry-fusion.ts). */
mapEntryFusion: MapEntryFusion | null = null;
readonly callLifetimes: CallLifetimes;
+ /** The locals whose value never leaves its frame (frame-escapes.ts), for
+ * class instances with reference fields in frame storage. */
+ frameEscapes: FrameEscapes | null = null;
readonly constantCallbacks: Map>;
currentConstantCallbacks: ReadonlyMap = new Map();
/** The current function's running maximum and minimum updates, whose
@@ -1775,6 +1798,9 @@ export class LlEmitter {
ran?: string;
/** Writes that speculate a rewritten value (rewriteSpeculable). */
values?: ReadonlyMap;
+ /** A proven integer loop's expression-position `++`/`--` of its
+ * shadowed bindings (ProvenLoopPlan.increments). */
+ increments?: ReadonlySet;
} | null = null;
/** Speculative loop sites numbered so far in the module. */
speculativeLoopSites = 0;
@@ -2113,7 +2139,7 @@ export class LlEmitter {
(options.targetTriple
? /-linux-(?:gnu|musl)/.test(options.targetTriple)
: process.platform === "linux")
- ? unwindAbiFunctions(mod, this.mayThrow, options.unwindExcluded)
+ ? unwindAbiFunctions(mod, this.mayThrow, options.unwindExcluded, this.nodeIndex)
: new Set();
this.workerEntryPolls = mt.workerEntryPolls ?? null;
this.stackChecks = mt.stackChecks ?? null;
@@ -2206,6 +2232,7 @@ export class LlEmitter {
(type.kind !== "union" ||
this.nullableUnions.single(type.unionId)?.arm.kind !== "string"),
this.walkBorrowsByFunction.get(fn.name) ?? new Set(),
+ this.nodeIndex,
);
if (rebound.size === 0) continue;
this.reboundByFunction.set(fn.name, rebound);
@@ -2231,6 +2258,7 @@ export class LlEmitter {
},
(wrap) => this.borrowableNullableWrap(wrap) === "ref",
);
+ if (this.stackObjects) this.frameEscapes = analyzeFrameEscapes(this.mod);
if (this.debug === null) {
const strings = findLentStringParameters(this.fnByName, this.callLifetimes);
this.lendingFacts = {
@@ -2401,6 +2429,20 @@ export class LlEmitter {
}
}
}
+ // Planned before any host copies the shape host (record-views.ts).
+ {
+ const views = planRecordViews(mod, {
+ traced: (id) => this.tracedShapes.has(`record:${id}`),
+ // A tagged shape's values share field reads with the other arms of
+ // its shape union.
+ excluded: (id) =>
+ this.nullableUnions.shapeNumber(id) !== 0 ||
+ this.publishedTypes?.recordGuarded(id) === true ||
+ mod.workers === true,
+ });
+ this.shapeHost.recordViews = views;
+ this.recordViewTargets = views.targets as Set;
+ }
this.walkers = new LlWalkers(this.shapeHost);
this.dyn = new LlDyn(this.shapeHost);
for (const g of mod.globals ?? []) {
@@ -2523,6 +2565,13 @@ export class LlEmitter {
this.shapeHost.arenasClosed = this.arenas.allClosed;
}
}
+ // Arena shapes are known only now; the plan's target set is shared by
+ // every host that copied the shape host, so dropping them here reaches
+ // all of them.
+ const arenaShapes = this.shapeHost.arenaShapes;
+ const viewTargets = this.recordViewTargets;
+ if (arenaShapes !== undefined && viewTargets !== null)
+ for (const id of arenaShapes) viewTargets.delete(id);
for (const [name, rec] of RUNTIME_ERROR_CLASSES) {
const meta = this.classMeta.get(name);
if (!meta) break; // hand-written IR without the builtin defs: no stamps
@@ -4844,10 +4893,25 @@ export class LlEmitter {
throw new InternalCompilerError(
`llvm emitter bug: unknown field ${field} on shape ${shapeId}`,
);
- const p = this.B.tmp();
- this.B.line(
- `${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${idx + this.nullableUnions.fieldBase(shapeId)}`,
- );
+ const base = this.nullableUnions.fieldBase(shapeId);
+ let p: string;
+ if (this.shapeHost.recordViews?.targets.has(shapeId) === true) {
+ p = recordViewFieldPtr(
+ this.B,
+ this.sizeType,
+ mangleRecordStruct(shapeId),
+ objName,
+ idx + base,
+ idx,
+ (decl) => this.declare(decl),
+ this.rcMode === undefined,
+ );
+ } else {
+ p = this.B.tmp();
+ this.B.line(
+ `${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${idx + base}`,
+ );
+ }
const type = shape.fields[idx]!.type;
return { ptr: p, type, inline: this.inlineUnions.record(type) };
}
diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts
index 9b2f2e4d8..a088cca01 100644
--- a/packages/compiler/src/backend/llvm/expr-calls.ts
+++ b/packages/compiler/src/backend/llvm/expr-calls.ts
@@ -18,7 +18,12 @@ import { liveRefView } from "./expr-stream-bridges.js";
import { typedRefConstructor, vAdapters } from "./shapes.js";
import { INT32_ABI_SUFFIX, SCALAR_UNION_RESULT } from "./int32-abi.js";
import { dispatchesCall, INTEGER_DISPATCH_SUFFIX } from "./integer-clones.js";
-import { canStackObject, emitStackObject } from "./stack-objects.js";
+import {
+ canStackObject,
+ emitStackObject,
+ frameClass,
+ type StackObjectSource,
+} from "./stack-objects.js";
import { collectFfiRetainedOps, parseFfiCallbackKey } from "../ffi-callbacks.js";
import {
mangleAsyncAdoptCall,
@@ -129,6 +134,19 @@ export function emitCallExpr(
if (borrowed) host.frames.push([]);
let suffixes: Writes[] | undefined;
const args = e.args.map((a, index) => {
+ // A fresh instance of a frame class that the parameter keeps in the
+ // callee's frame (frame-escapes.ts) is built in the caller's frame;
+ // the call's statement frame releases its reference fields.
+ if (
+ a.kind === "new" &&
+ host.frameEscapes?.parameterStays(e.callee, index) === true &&
+ frameClass(host, a.className)
+ ) {
+ const value = emitStackObject(host, a as StackObjectSource, (fields) => {
+ for (const field of fields) host.ownSlot(field.ptr, field.type);
+ });
+ return host.markImmortal(value);
+ }
if (borrowed?.has(index)) {
// A heap-borrowing helper may retain or return its argument.
// Only projection-only parameters may see an unowned stack box,
diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts
index 088d392d6..63db3d67b 100644
--- a/packages/compiler/src/backend/llvm/expr-primitives.ts
+++ b/packages/compiler/src/backend/llvm/expr-primitives.ts
@@ -517,6 +517,29 @@ export function emitOperatorExpr(
// Expression-position ++/-- over an f64 binding (locals, module
// globals, capture boxes): read, write ±1, yield old (postfix) or
// new (prefix).
+ const shadow = host.integerLoopBindings.get(e.localId);
+ if (shadow !== undefined && host.speculation?.increments?.has(e)) {
+ // A proven integer loop's shadow: the loop range holds every value
+ // the binding takes, so the step cannot overflow.
+ const old = B.tmp();
+ const next = B.tmp();
+ const number = B.tmp();
+ B.line(`${old} = load i64, ptr ${shadow.slot}`);
+ B.line(`${next} = ${e.op === "+" ? "add" : "sub"} nsw i64 ${old}, 1`);
+ B.line(`store i64 ${next}, ptr ${shadow.slot}`);
+ const value = e.prefix ? next : old;
+ B.line(`${number} = sitofp i64 ${value} to double`);
+ return {
+ name: number,
+ type: e.type,
+ integer: {
+ name: value,
+ type: "i64",
+ signed: true,
+ range: shadow.range ?? { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER },
+ },
+ };
+ }
const b = host.binding(e.localId);
if (host.int32Locals.has(e.localId)) {
if (!host.int32LocalWrites.has(e))
diff --git a/packages/compiler/src/backend/llvm/frame-escapes.ts b/packages/compiler/src/backend/llvm/frame-escapes.ts
new file mode 100644
index 000000000..6156f6fc3
--- /dev/null
+++ b/packages/compiler/src/backend/llvm/frame-escapes.ts
@@ -0,0 +1,271 @@
+/* Frame escapes (stack-objects.ts): the locals whose value provably dies
+ * before their function returns, so that an object bound to one can live in
+ * the frame.
+ *
+ * A local stays in the frame when every use of its value does: field and
+ * element reads and writes through it (storing into it is fine), length and
+ * search operations, identity comparisons, a statement that discards it, and
+ * passing it to a parameter that itself stays in its callee's frame
+ * (region-ir.ts keepsInFrame). A value that a conditional, a union wrap or
+ * narrow, a cast or a sequence passes on takes that expression's fate. A
+ * `const` binding of the value in the same function shares the local's fate:
+ * it is declared inside the local's scope, so it cannot outlive it.
+ *
+ * A closure that captures the local keeps it in the frame when the closure
+ * itself stays in the frame (it is only invoked, or passed to a parameter
+ * that stays in its callee's frame) and its body keeps the captured binding
+ * in the frame. The binding's box then never outlives the frame either.
+ *
+ * Everything else escapes: returning or throwing it, storing the value
+ * itself anywhere (a field, an element, a collection, a module global or
+ * another binding by assignment), capturing it in a closure that escapes or
+ * in a local class, converting it, and every call whose target is not a
+ * known function with a proven parameter.
+ *
+ * Parameter and capture facts are solved optimistically, like
+ * CallLifetimes: every reference parameter stays in its callee's frame, and
+ * every captured binding in its closure's, until a use proves otherwise.
+ * They are solved on demand: a query explores only the facts its answer
+ * depends on (a parameter's fact depends on the calls its own value reaches),
+ * and each function's body is walked once into a summary of those uses.
+ * Async functions, generators and module code have no facts, since their
+ * frames can outlive a call or are the program's own. */
+import type { IrExpr, IrFunction, IrModule, IrStmt } from "../../ir/ir.js";
+import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
+import {
+ isExpr,
+ indexOfNode,
+ keepsInFrame,
+ same,
+ refType,
+ transparent,
+ type Node,
+} from "./region-ir.js";
+
+export interface FrameEscapes {
+ /** The reference locals of `fn` whose value never leaves its frame:
+ * never assigned after its declaration, captured only by closures that
+ * keep it in the frame, and every use keeping it in the frame. */
+ locals(fn: IrFunction): ReadonlySet;
+ /** Whether argument `index` of a call of `callee` stays in the callee's
+ * frame. */
+ parameterStays(callee: string, index: number): boolean;
+}
+
+function suspends(fn: IrFunction): boolean {
+ return fn.async === true || fn.generator !== undefined || /^%(init\.\d+|main)$/.test(fn.name);
+}
+
+/** A use whose fate a fact decides: argument `index` of a call of `fn`
+ * (`index` >= 0), or a binding `id` that a closure of lifted function `fn`
+ * captures. */
+interface FactUse {
+ fn: string;
+ index: number;
+ id: string;
+}
+
+/** What one walk of a function's body finds, by value group: a `const`
+ * binding of a value shares its fate, and a closure that of the locals it
+ * captures. */
+interface Summary {
+ /** The representative of each grouped value. */
+ root: Map;
+ /** Groups that escape whatever the facts say. */
+ escaping: Set;
+ /** Per group, the uses whose facts decide it. */
+ uses: Map;
+}
+
+const NO_USES: readonly FactUse[] = [];
+const NO_PARAMETERS: ReadonlyMap> = new Map();
+
+/** Walks `fn` once. */
+function summarize(fn: IrFunction): Summary {
+ const escapes = new Set();
+ const facts: { id: string; use: FactUse }[] = [];
+ const own = new Set(fn.locals.map((l) => l.id));
+ const group = new Map();
+ const find = (id: string): string => {
+ let root = id;
+ while (group.has(root)) root = group.get(root)!;
+ return root;
+ };
+ const unite = (a: string, b: string): void => {
+ const ra = find(a),
+ rb = find(b);
+ if (ra !== rb) group.set(ra, rb);
+ };
+ const declared = new Map();
+ // Captured bindings that a closure node of this function captures.
+ const closureCaptured = new Set();
+ let closures = 0;
+ const parents: Node[] = [];
+ /** The fate of the value `node` produces: shared with the binding a
+ * `const` declaration gives it, kept by its consumer, decided by a
+ * parameter fact, or escaping. */
+ const fate = (node: Node, id: string): void => {
+ let value: Node = node;
+ let at = parents.length - 1;
+ while (at >= 0 && transparent(value, parents[at])) value = parents[at--]!;
+ const parent = at >= 0 ? parents[at] : undefined;
+ if (
+ parent?.kind === "varDecl" &&
+ parent.init !== null &&
+ same(parent.init, value) &&
+ own.has(parent.localId) &&
+ parent.localId !== id
+ )
+ unite(id, parent.localId);
+ else if (parent?.kind === "call") {
+ const index = indexOfNode(parent.args, value);
+ if (index >= 0) facts.push({ id, use: { fn: parent.callee, index, id: "" } });
+ else escapes.add(id);
+ } else if (!keepsInFrame(value, parent, NO_PARAMETERS)) escapes.add(id);
+ };
+ const walk = (node: Node): void => {
+ if (node.kind === "varDecl") declared.set(node.localId, (declared.get(node.localId) ?? 0) + 1);
+ if (node.kind === "assign" || node.kind === "assignExpr" || node.kind === "incDec")
+ escapes.add(node.localId);
+ if (node.kind === "classRef") for (const id of node.captures ?? []) escapes.add(id);
+ if (node.kind === "closure") {
+ const closure = `%closure.${closures++}`;
+ fate(node, closure);
+ for (const id of node.captures ?? []) {
+ closureCaptured.add(id);
+ unite(id, closure);
+ facts.push({ id, use: { fn: node.fnName, index: -1, id } });
+ }
+ }
+ if (node.kind === "varRef") {
+ fate(node, node.localId);
+ return;
+ }
+ parents.push(node);
+ if (isExpr(node)) everyExprChild(node, visitExpr, visitStmt);
+ else everyStmtChild(node, visitExpr, visitStmt);
+ parents.pop();
+ };
+ const visitExpr = (child: IrExpr): boolean => {
+ walk(child);
+ return true;
+ };
+ const visitStmt = (child: IrStmt): boolean => {
+ walk(child);
+ return true;
+ };
+ for (const stmt of fn.body) walk(stmt);
+ const lifted = new Set((fn.captures ?? []).map((c) => c.localId));
+ for (const local of fn.locals) {
+ if (local.boxed === true && !lifted.has(local.id) && !closureCaptured.has(local.id))
+ escapes.add(local.id);
+ if ((declared.get(local.id) ?? 0) > 1) escapes.add(local.id);
+ }
+ const root = new Map();
+ for (const id of group.keys()) root.set(id, find(id));
+ const rootOf = (id: string): string => root.get(id) ?? id;
+ const escaping = new Set();
+ for (const id of escapes) escaping.add(rootOf(id));
+ const uses = new Map();
+ for (const { id, use } of facts) {
+ const r = rootOf(id);
+ if (escaping.has(r)) continue;
+ let list = uses.get(r);
+ if (!list) uses.set(r, (list = []));
+ list.push(use);
+ }
+ return { root, escaping, uses };
+}
+
+export function analyzeFrameEscapes(mod: IrModule): FrameEscapes {
+ const fns = new Map(mod.functions.map((fn) => [fn.name, fn]));
+ const summaries = new Map();
+ const summaryOf = (fn: IrFunction): Summary => {
+ let summary = summaries.get(fn.name);
+ if (summary === undefined) {
+ summary = summarize(fn);
+ summaries.set(fn.name, summary);
+ }
+ return summary;
+ };
+ /** A fact's key, the binding in its function that it is about, or null
+ * when the fact is false from the start. */
+ const subject = (use: FactUse): { key: string; fn: IrFunction; id: string } | null => {
+ const fn = fns.get(use.fn);
+ if (fn === undefined || suspends(fn)) return null;
+ if (use.index >= 0) {
+ const param = fn.params[use.index];
+ if (param === undefined || !refType(param.type)) return null;
+ return { key: `${use.fn}#${use.index}`, fn, id: param.localId };
+ }
+ if (!(fn.captures ?? []).some((c) => c.localId === use.id)) return null;
+ return { key: `${use.fn}@${use.id}`, fn, id: use.id };
+ };
+ const solved = new Map();
+ /** The facts `uses` decide, solved over the facts they depend on that are
+ * not solved yet: a greatest fixpoint on that part of the graph. */
+ const solve = (uses: readonly FactUse[]): void => {
+ const pending = new Map();
+ const stack: FactUse[] = [...uses];
+ while (stack.length > 0) {
+ const use = stack.pop()!;
+ const s = subject(use);
+ if (s === null || solved.has(s.key) || pending.has(s.key)) continue;
+ const summary = summaryOf(s.fn);
+ const r = summary.root.get(s.id) ?? s.id;
+ const node = { ok: !summary.escaping.has(r), deps: [] as string[] };
+ pending.set(s.key, node);
+ if (!node.ok) continue;
+ for (const dep of summary.uses.get(r) ?? NO_USES) {
+ const d = subject(dep);
+ if (d === null) {
+ node.ok = false;
+ break;
+ }
+ node.deps.push(d.key);
+ stack.push(dep);
+ }
+ }
+ for (let changed = true; changed;) {
+ changed = false;
+ for (const node of pending.values())
+ if (node.ok && node.deps.some((d) => !(solved.get(d) ?? pending.get(d)!.ok))) {
+ node.ok = false;
+ changed = true;
+ }
+ }
+ for (const [key, node] of pending) solved.set(key, node.ok);
+ };
+ /** Whether every use in `uses` keeps its value in the frame. */
+ const allStay = (uses: readonly FactUse[]): boolean => {
+ solve(uses);
+ for (const use of uses) {
+ const s = subject(use);
+ if (s === null || solved.get(s.key) !== true) return false;
+ }
+ return true;
+ };
+ const locals = new Map>();
+ return {
+ parameterStays(callee: string, index: number): boolean {
+ return allStay([{ fn: callee, index, id: "" }]);
+ },
+ locals(fn: IrFunction): ReadonlySet {
+ let kept = locals.get(fn.name);
+ if (kept === undefined) {
+ const set = new Set();
+ if (!suspends(fn)) {
+ const summary = summaryOf(fn);
+ for (const local of fn.locals) {
+ if (!refType(local.type)) continue;
+ const r = summary.root.get(local.id) ?? local.id;
+ if (!summary.escaping.has(r) && allStay(summary.uses.get(r) ?? NO_USES))
+ set.add(local.id);
+ }
+ }
+ locals.set(fn.name, (kept = set));
+ }
+ return kept;
+ },
+ };
+}
diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts
index c3acf766f..24e1e8548 100644
--- a/packages/compiler/src/backend/llvm/lib-dispatch.ts
+++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts
@@ -24,7 +24,7 @@ import {
} from "./lib-shared.js";
import { isDynUndefined } from "../../ir/runtime-features.js";
import { DYN_KIND } from "./dyn.js";
-import { caBytes } from "./lib-abi.js";
+import { caBytes, callRuntime, ptr } from "./lib-abi.js";
import { emitInlineAtomicsOp } from "./expr-bytes.js";
import { regexLiteralCaptures } from "./regex-matchers.js";
import { registerReplay } from "./emit-journal.js";
@@ -1110,14 +1110,44 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
if (e.fn === "record.isFrozen") {
// The frozen bit of the record's rc word (scr_rc.h SCR_RC_FROZEN) is
// its second-highest bit; an immortal word sets the highest one too.
- const value = host.emitReadReceiver(e.args[0]!);
+ // Where the record's shape may be published, an immortal word reads as
+ // frozen: publish() freezes what it reaches, as Node's deep freeze does.
+ const receiver = e.args[0]!;
+ const value = host.emitReadReceiver(receiver);
const S = host.sizeType;
const rc = host.B.tmp(),
- top = host.B.tmp(),
- t = host.B.tmp();
+ top = host.B.tmp();
+ let t = host.B.tmp();
host.B.line(`${rc} = load ${S}, ptr ${value.name}`);
host.B.line(`${top} = lshr ${S} ${rc}, ${S === "i64" ? 62 : 30}`);
host.B.line(`${t} = icmp eq ${S} ${top}, 1`);
+ if (
+ receiver.type.kind === "record" &&
+ host.publishedTypes?.recordGuarded(receiver.type.shapeId) === true
+ )
+ t = orPublished(host, t, rc);
+ return { name: t, type: e.type };
+ }
+ if (
+ (e.fn === "array.isFrozen" || e.fn === "collection.isExtensible") &&
+ host.publishedTypes?.containerPublished(e.args[0]!.type) === true
+ ) {
+ // publish() freezes what it reaches, as Node's deep freeze does: a
+ // published array is frozen, and a published Map or Set is not extensible.
+ const value = host.emitReadReceiver(e.args[0]!);
+ const answer = callRuntime(
+ host,
+ e.fn === "array.isFrozen" ? "scr_arr_is_frozen" : "scr_map_is_extensible",
+ "i1",
+ [ptr(value.name)],
+ );
+ const rc = host.B.tmp();
+ host.B.line(`${rc} = load ${host.sizeType}, ptr ${value.name}`);
+ if (e.fn === "array.isFrozen") return { name: orPublished(host, answer, rc), type: e.type };
+ const mortal = host.B.tmp(),
+ t = host.B.tmp();
+ host.B.line(`${mortal} = icmp ne ${host.sizeType} ${rc}, -1`);
+ host.B.line(`${t} = and i1 ${answer}, ${mortal}`);
return { name: t, type: e.type };
}
const prefix = e.fn.slice(0, e.fn.indexOf(".")) as LibCallPrefix;
@@ -1239,6 +1269,11 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "navigator":
case "mime":
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.
+ if (e.fn === "webstream.class") host.usesTimers = true;
+ return host.emitGenericLibCall(e);
case "test":
return emitTestLibCall(host, e);
case "tls":
@@ -1346,3 +1381,14 @@ function emitPromiseAllTuple(host: LlvmEmitterContext, e: LibCallExpr): LlValue
B.line(`call void @scr_promise_release(ptr ${all})`);
return host.own({ name: result, type: e.type });
}
+
+/** `frozen` or'ed with "the object whose rc word is `rc` is published": an
+ * immortal word (all ones), the test of the published write guards
+ * (emitter.ts emitPublishedTest). */
+function orPublished(host: LlvmEmitterContext, frozen: string, rc: string): string {
+ const immortal = host.B.tmp(),
+ t = host.B.tmp();
+ host.B.line(`${immortal} = icmp eq ${host.sizeType} ${rc}, -1`);
+ host.B.line(`${t} = or i1 ${frozen}, ${immortal}`);
+ return t;
+}
diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts
index bdb1fba27..31b69fc20 100644
--- a/packages/compiler/src/backend/llvm/lib-process.ts
+++ b/packages/compiler/src/backend/llvm/lib-process.ts
@@ -741,6 +741,32 @@ export function emitProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
const B = host.B;
+ if (e.fn === "error.captureStackTrace") {
+ // The second argument names the function constructorOpt skips through;
+ // its frame text is the constant its own frame pushes (emit-function.ts),
+ // or null when it has no frame, which leaves no frames.
+ const target = host.emitExpr(e.args[0]!);
+ const named = e.args[1]!;
+ const skip = e.args[2]!.kind === "boolLit" && e.args[2]!.value;
+ let through = "null";
+ if (named.kind === "strLit" && named.value !== "") {
+ const fn = host.mod.functions.find((f) => f.name === named.value);
+ if (
+ fn !== undefined &&
+ host.stackTraces &&
+ !host.wasi &&
+ fn.sourceName !== undefined &&
+ !host.unframedFunctions.has(fn.name)
+ )
+ through = host.cstr(` at ${fn.sourceName} (${fn.loc.file})`);
+ }
+ host.declare(`declare void @scr_error_capture_stack_trace(ptr, ptr, i1)`);
+ B.line(
+ `call void @scr_error_capture_stack_trace(ptr ${target.name}, ptr ${through}, i1 ${skip ? "true" : "false"})`,
+ );
+ host.emitPendingCheck();
+ return { name: "", type: e.type };
+ }
if (e.fn === "error.argTypeThrow") {
// Always throws with the runtime-rendered Received tail (the
// error.nodeThrow dummy pattern). Borrows all three.
diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts
index 747c9c883..0271664ac 100644
--- a/packages/compiler/src/backend/llvm/lib-shared.ts
+++ b/packages/compiler/src/backend/llvm/lib-shared.ts
@@ -118,6 +118,8 @@ export const LIB_FN_SYMS: Record = {
"util.diff": "scr_util_diff",
"util.debounce": "scr_util_debounce",
"util.throttle": "scr_util_throttle",
+ "webstream.class": "scr_webstream_class",
+ "webstream.instanceOf": "scr_webstream_instanceof",
"mime.typeNew": "scr_mime_type_new",
"mime.typeParse": "scr_mime_type_parse",
"mime.paramsNew": "scr_mime_params_new",
@@ -1030,10 +1032,6 @@ export const LIB_FN_SYMS: Record = {
"fetch.responseJson": "scr_fetch_response_json",
"fetch.responseText": "scr_fetch_response_text",
"fetch.responseBytes": "scr_fetch_response_bytes",
- "fetch.abortControllerNew": "scr_fetch_abort_controller_new",
- "fetch.abortTimeout": "scr_fetch_abort_timeout",
- "fetch.abortNow": "scr_fetch_abort_now",
- "fetch.abortAny": "scr_fetch_abort_any",
"fetch.webIs": "scr_fetch_web_is",
"fetch.streamIs": "scr_fetch_stream_is",
"zlib.streamIs": "scr_zlib_stream_is",
@@ -1693,7 +1691,6 @@ export const LIB_FN_SYMS: Record = {
"dyn.objectPrototype": "scr_dyn_object_prototype",
"dyn.errorPrototype": "scr_dyn_error_prototype",
"dyn.inheritsErrorPrototype": "scr_dyn_inherits_error_prototype",
- "dyn.errorCaptureStackTrace": "scr_dyn_error_capture_stack_trace",
"dyn.errorCaptureStackTraceFunction": "scr_dyn_error_capture_stack_trace_function",
"dyn.functionApply": "scr_dyn_function_apply",
"dyn.builtinMethod": "scr_dyn_builtin_method",
@@ -1838,14 +1835,11 @@ export const USES_TIMERS_LIB_FNS = new Set([
"fetch.input",
"fetch.function",
"fetch.requestNew",
- "fetch.abortTimeout",
"abort.timeout",
// EventTarget listeners report their exceptions from a nextTick.
"event.targetNew",
"abort.controllerNew",
"abort.any",
- "fetch.abortControllerNew",
- "fetch.abortAny",
"fetch.streamNew",
"fetch.streamFrom",
"fetch.transformNew",
diff --git a/packages/compiler/src/backend/llvm/local-extents.ts b/packages/compiler/src/backend/llvm/local-extents.ts
index 7706a9c0a..09f6cf22c 100644
--- a/packages/compiler/src/backend/llvm/local-extents.ts
+++ b/packages/compiler/src/backend/llvm/local-extents.ts
@@ -45,6 +45,15 @@ function lastUse(
const { extent, derived } = extentOf(stmts, index, localId, rebound);
const last = extent[extent.length - 1];
if (last === undefined) return null;
+ return lastUseIn(last, localId, derived);
+}
+
+/** The binding's read in `last`, the last statement of its extent, whose derived bindings are `derived`. */
+function lastUseIn(
+ last: IrStmt,
+ localId: string,
+ derived: ReadonlySet,
+): { read: IrExpr; stored: boolean } | null {
const state: { read: IrExpr | null; stored: boolean; ok: boolean } = {
read: null,
stored: false,
@@ -183,6 +192,109 @@ function collectAssignedExpr(e: IrExpr, assigned: Set): boolean {
);
}
+/**
+ * `storedLastUseRead(stmts, -1, localId, true)` for every parameter of one function body, from one walk of the
+ * body instead of a walk per binding. With `index` -1 and `rebound`, no binding is excluded as assigned, so a
+ * binding is derived when one of its definitions mentions a derived binding (reachability over "definition
+ * mentions") and the extent ends at the last statement that mentions one (`absentStmt`). The walk records, for
+ * each top-level statement, the bindings it mentions, and for each definition, the bindings its value mentions.
+ */
+export class BodyLastUses {
+ private readonly stmts: readonly IrStmt[];
+ /** The bindings each top-level statement mentions, as `absentStmt` reads them. */
+ private readonly mentions: Set[] = [];
+ /** For each binding, the bindings whose definitions mention it. */
+ private readonly definedFrom = new Map();
+
+ constructor(stmts: readonly IrStmt[]) {
+ this.stmts = stmts;
+ for (const s of stmts) {
+ const mentioned = new Set();
+ this.stmt(s, mentioned);
+ this.mentions.push(mentioned);
+ }
+ }
+
+ /** What `storedLastUseRead(stmts, -1, localId, true)` answers. */
+ storedRead(localId: string): IrExpr | null {
+ const derived = new Set([localId]);
+ const queue = [localId];
+ while (queue.length > 0) {
+ for (const target of this.definedFrom.get(queue.pop()!) ?? [])
+ if (!derived.has(target)) {
+ derived.add(target);
+ queue.push(target);
+ }
+ }
+ let last = -1;
+ for (let i = this.mentions.length - 1; i >= 0 && last < 0; i--)
+ for (const id of derived)
+ if (this.mentions[i]!.has(id)) {
+ last = i;
+ break;
+ }
+ if (last < 0) return null;
+ const use = lastUseIn(this.stmts[last]!, localId, derived);
+ return use !== null && use.stored ? use.read : null;
+ }
+
+ /** Adds the bindings `s` mentions to `into`, and records the definitions in it. */
+ private stmt(s: IrStmt, into: Set): boolean {
+ if (s.kind === "varDecl" || s.kind === "assign") into.add(s.localId);
+ if (s.kind === "varDecl" || s.kind === "forOf" || s.kind === "assign") {
+ const value = s.kind === "varDecl" ? s.init : s.kind === "assign" ? s.value : s.iterable;
+ if (value !== null) this.define(s.localId, value);
+ }
+ everyStmtChild(
+ s,
+ (e) => this.expr(e, into),
+ (c) => this.stmt(c, into),
+ );
+ return true;
+ }
+
+ private expr(e: IrExpr, into: Set): boolean {
+ if (e.kind === "varRef" || e.kind === "assignExpr" || e.kind === "incDec") into.add(e.localId);
+ else if (e.kind === "closure" || e.kind === "classRef")
+ for (const id of e.captures ?? []) into.add(id);
+ if (e.kind === "assignExpr") this.define(e.localId, e.value);
+ everyExprChild(
+ e,
+ (c) => this.expr(c, into),
+ (c) => this.stmt(c, into),
+ );
+ return true;
+ }
+
+ /** A definition of `target` whose value is `value`: every binding the value mentions derives `target`. */
+ private define(target: string, value: IrExpr): void {
+ const mentioned = new Set();
+ mentionsOf(value, mentioned);
+ for (const id of mentioned) {
+ let targets = this.definedFrom.get(id);
+ if (targets === undefined) this.definedFrom.set(id, (targets = []));
+ targets.push(target);
+ }
+ }
+}
+
+/** The bindings an expression mentions, as `absentExpr` reads them. */
+function mentionsOf(e: IrExpr, into: Set): void {
+ const expr = (x: IrExpr): boolean => {
+ if (x.kind === "varRef" || x.kind === "assignExpr" || x.kind === "incDec") into.add(x.localId);
+ else if (x.kind === "closure" || x.kind === "classRef")
+ for (const id of x.captures ?? []) into.add(id);
+ everyExprChild(x, expr, stmt);
+ return true;
+ };
+ const stmt = (s: IrStmt): boolean => {
+ if (s.kind === "varDecl" || s.kind === "assign") into.add(s.localId);
+ everyStmtChild(s, expr, stmt);
+ return true;
+ };
+ expr(e);
+}
+
/** True while no node of the tree mentions a derived binding. */
function absentExpr(e: IrExpr, derived: ReadonlySet): boolean {
if (e.kind === "varRef" || e.kind === "assignExpr" || e.kind === "incDec") {
diff --git a/packages/compiler/src/backend/llvm/loop-entry-versions.ts b/packages/compiler/src/backend/llvm/loop-entry-versions.ts
index 2c448cae4..662146cb6 100644
--- a/packages/compiler/src/backend/llvm/loop-entry-versions.ts
+++ b/packages/compiler/src/backend/llvm/loop-entry-versions.ts
@@ -1,4 +1,4 @@
-/* Loop-entry range versions (ir/loop-entry-ranges.ts): a while loop whose
+/* Loop-entry range versions (ir/loop-entry-ranges.ts): a while or for loop whose
* integer work depends on locals entering it without range facts runs as
* two copies. A test on entry checks that each such local holds an integer
* in its assumed range; the fast copy is emitted under the facts that hold
@@ -16,7 +16,7 @@ interface LoopEntryState {
fn: IrFunction | null;
facts: IntegerRangeFacts | null;
slots: IntegerSlotFacts;
- /** Found on the first while loop the function emits. */
+ /** Found on the first while or for loop the function emits. */
versions: Map | null;
/** Loops whose copies are being emitted: a loop is versioned once. */
readonly versioning: Set;
@@ -61,7 +61,7 @@ export function beginLoopEntryVersions(
* its locals: plain double slots of the current function, outside a
* speculative copy and a coroutine. */
export function loopEntryVersion(emitter: LlEmitter, loop: IrStmt): LoopEntryVersion | null {
- if (loop.kind !== "while") return null;
+ if (loop.kind !== "while" && loop.kind !== "for") return null;
const state = ENTRY_VERSIONS.of(emitter);
if (state.fn === null || state.facts === null || state.versioning.has(loop)) return null;
if (
diff --git a/packages/compiler/src/backend/llvm/nullable-unions.test.ts b/packages/compiler/src/backend/llvm/nullable-unions.test.ts
index f4e6a6866..ea0517b56 100644
--- a/packages/compiler/src/backend/llvm/nullable-unions.test.ts
+++ b/packages/compiler/src/backend/llvm/nullable-unions.test.ts
@@ -130,3 +130,19 @@ test("function arms are nullable pointers compared by identity root", () => {
expect(isPointerIdentityArm(single)).toBe(false);
expect(unions.armTypes().get("fu")).toEqual(fn);
});
+
+test("typed array arms are nullable pointers compared by identity", () => {
+ const i32: IrType = { kind: "bytes", elem: "i32" };
+ const unions = new NullableUnions(
+ module([
+ { id: "iu", arms: [i32, UNDEFINED_T] },
+ { id: "nin", arms: [NULL_T, i32, UNDEFINED_T] },
+ { id: "if", arms: [i32, F64] },
+ ]),
+ );
+ expect(unions.single("iu")).toMatchObject({ refTag: 0, unitTag: 1, nullTag: -1, arm: i32 });
+ expect(unions.single("nin")).toMatchObject({ refTag: 1, unitTag: 2, nullTag: 0, arm: i32 });
+ expect(unions.get("if")).toBeNull();
+ expect(isObjectArm(unions.single("iu")!)).toBe(true);
+ expect(isPointerIdentityArm(unions.single("iu")!)).toBe(true);
+});
diff --git a/packages/compiler/src/backend/llvm/nullable-unions.ts b/packages/compiler/src/backend/llvm/nullable-unions.ts
index eedb922b3..587f3c18c 100644
--- a/packages/compiler/src/backend/llvm/nullable-unions.ts
+++ b/packages/compiler/src/backend/llvm/nullable-unions.ts
@@ -88,17 +88,17 @@ export const NULLABLE_NULL = "@sc_nullable_null";
export const NULLABLE_ABSENT = "@sc_nullable_absent";
/** The reference arms whose instances are never NULL. Class and record
- * entry points all tolerate NULL; strings, Stats snapshots, arrays, maps and
- * sets retain through a NULL-skipping wrapper (shapes.ts), as do closures
- * (function arms). Object arms are always truthy and compare by identity
- * (function arms by identity root); string arms are truthy when non-empty
- * and compare bytes. */
+ * entry points all tolerate NULL; strings, Stats snapshots, arrays, typed
+ * arrays, maps and sets retain through a NULL-skipping wrapper (shapes.ts),
+ * as do closures (function arms). Object arms are always truthy and compare
+ * by identity (function arms by identity root); string arms are truthy when
+ * non-empty and compare bytes. */
export type NullableRefArm =
| { kind: "object"; className: string }
| { kind: "record"; shapeId: string }
| { kind: "string" }
| { kind: "stats" }
- | (IrType & { kind: "array" | "map" | "set" | "func" });
+ | (IrType & { kind: "array" | "bytes" | "map" | "set" | "func" });
/** The arm is a JS object: always truthy. */
export function isObjectArm(nullable: PointerUnion): boolean {
@@ -273,7 +273,13 @@ function nullableShape(
return { ...base, arm: { kind: "record", shapeId: arm.shapeId } };
if (arm.kind === "string") return { ...base, arm: { kind: "string" } };
if (arm.kind === "stats") return { ...base, arm: { kind: "stats" } };
- if (arm.kind === "array" || arm.kind === "map" || arm.kind === "set" || arm.kind === "func")
+ if (
+ arm.kind === "array" ||
+ arm.kind === "bytes" ||
+ arm.kind === "map" ||
+ arm.kind === "set" ||
+ arm.kind === "func"
+ )
return { ...base, arm };
return null;
}
diff --git a/packages/compiler/src/backend/llvm/rebound-parameters.ts b/packages/compiler/src/backend/llvm/rebound-parameters.ts
index b239195dd..722bdf717 100644
--- a/packages/compiler/src/backend/llvm/rebound-parameters.ts
+++ b/packages/compiler/src/backend/llvm/rebound-parameters.ts
@@ -1,5 +1,9 @@
import type { IrExpr, IrFunction, IrStmt, IrType } from "../../ir/ir.js";
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
+import { eachNodeOfKinds, type IrNodeIndex } from "../../ir/node-index.js";
+
+/** Assignment statements: only these rebind a parameter. */
+const ASSIGN_KINDS = ["assign"] as const;
/** Parameters that keep the borrowed calling convention although the body
* rebinds them (`source = (source as LiteralType).regularType`). The
@@ -15,6 +19,8 @@ export function findReboundParameters(
fn: IrFunction,
plainReference: (type: IrType) => boolean,
excluded: ReadonlySet,
+ /** The module's node index: a body that assigns no candidate parameter is not walked. */
+ nodes?: IrNodeIndex,
): ReadonlySet {
const result = new Set();
if (fn.async || fn.generator || fn.captures || fn.classCaptures) return result;
@@ -34,6 +40,15 @@ export function findReboundParameters(
.map((param) => param.localId),
);
if (candidates.size === 0) return result;
+ // A parameter is rebound only by an assignment statement to it.
+ if (nodes !== undefined) {
+ let assigned = false;
+ eachNodeOfKinds(fn, nodes.scan(ASSIGN_KINDS), (node) => {
+ if (node.kind === "assign" && candidates.has(node.localId)) assigned = true;
+ return !assigned;
+ });
+ if (!assigned) return result;
+ }
const rebound = new Set();
const invalid = new Set();
const expr = (e: IrExpr): boolean => {
diff --git a/packages/compiler/src/backend/llvm/record-views.test.ts b/packages/compiler/src/backend/llvm/record-views.test.ts
new file mode 100644
index 000000000..02a82d873
--- /dev/null
+++ b/packages/compiler/src/backend/llvm/record-views.test.ts
@@ -0,0 +1,130 @@
+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 type { IrModule } from "../../ir/ir.js";
+import { computeTraced } from "../cycle-analysis.js";
+import { planRecordViews } from "./record-views.js";
+
+/** The views planned for `source`'s module, as target field names → source
+ * field names (each list sorted). */
+async function plannedViews(source: string): Promise