Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/content/docs/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ Verifies and reads the installed precompiled runtime objects and vendor archives
<dt><code>--windows-subsystem &lt;console|gui&gt;</code></dt>
<dd>Select the subsystem of a Windows executable. <code>console</code> is the default; <code>gui</code> prevents Windows from opening a console window for the app. Valid only for executable builds targeting Windows.</dd>

<dt><code>--detached-exit</code></dt>
<dd>On Linux, when the program calls <code>process.exit()</code>, hand the release of its memory to a helper process so the parent sees the exit sooner. The kernel otherwise frees the whole address space before the exit is reported, which takes tens of milliseconds per gigabyte. Opt in only where the trade-offs are acceptable: for up to about 100 ms after the exit, the helper keeps the executable mapped (overwriting the binary in place fails with "text file busy"), its memory stays charged to the cgroup, and the teardown CPU time no longer appears in the parent's resource usage. Valid only for executable builds; it has no effect on other platforms.</dd>

<dt><code>--npm-static &lt;pkg[,pkg…]|auto&gt;</code></dt>
<dd>EXPERIMENTAL. Compile the named npm packages' shipped JS statically as program modules instead of embedding them for the engine (repeatable; <code>auto</code> opts in every eligible direct import). A package the preflight refuses falls back to the island with a coverage-report note. See <a href="/dependencies">npm Dependencies</a> for maturity notes.</dd>

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ async function tryFastPath(): Promise<number | null> {
backend: "llvm",
...(optimization === "dev" || optimization === "speed" ? { optimization } : {}),
...(values.strip ? { strip: true as const } : {}),
...(values["detached-exit"] ? { detachedExit: true as const } : {}),
npmStatic,
ffiProfile: ffiPath === null ? null : { path: ffiPath, bytes: ffiBytes! },
target: `${process.env["SCRIPTC_TARGET"] ?? "native"}:${buildPlatform}:${process.arch}:${
Expand Down Expand Up @@ -183,6 +184,7 @@ async function coverageFastPath(
(failOn !== undefined && failOn !== "blockers" && failOn !== "divergences") ||
values.emit !== undefined ||
values.strip ||
values["detached-exit"] ||
values.lib ||
values.profile !== undefined ||
values.ffi !== undefined ||
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/test/output-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ describe("output option compatibility", () => {
[{ emit: "llvm", windowsSubsystem: "gui" }, /windows-subsystem/],
[{ emit: "obj", strip: true }, /strip/],
[{ emit: "llvm", strip: true }, /strip/],
[{ emit: "obj", detachedExit: true }, /detached-exit/],
[{ emit: "llvm", detachedExit: true }, /detached-exit/],
] as const)("rejects %j", (override, message) => {
const result = resolveOutputOptions("build", { ...BASE, ...override });
expect(result).toEqual({ ok: false, message: expect.stringMatching(message) });
Expand Down
10 changes: 10 additions & 0 deletions packages/compiler/src/backend/llvm/emitter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ test("throwing-call count does not multiply identical scope cleanup", () => {
}
});

test("detached exit defines the runtime opt-in symbol only when requested", () => {
const module = moduleFor([call()]);
const plain = emitLlvmModule(module);
expect(emitLlvmModule(module, { detachedExit: false })).toBe(plain);
expect(plain).not.toContain("@scr_detached_exit");
const detached = emitLlvmModule(module, { detachedExit: true });
expect(detached.match(/^@scr_detached_exit = constant i8 1$/gm)).toHaveLength(1);
expect(detached.replace("@scr_detached_exit = constant i8 1\n", "")).toBe(plain);
});

test("inline RC is a speed-only emission mode and leaves release IR unchanged", () => {
for (const boxed of [false, true]) {
const module = moduleFor(Array.from({ length: 4 }, call), { boxed });
Expand Down
10 changes: 10 additions & 0 deletions packages/compiler/src/backend/llvm/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,10 @@ export interface LlvmTargetOptions {
/** Emit retain/release fast paths inline (the `speed` posture). Off, every
* retain and release is a runtime call. */
inlineRc?: boolean;
/** Define `scr_detached_exit` so the Linux runtime hands the address-space
* teardown after `process.exit` to a helper process (`--detached-exit`).
* Executables only; the runtime's weak default keeps it off. */
detachedExit?: boolean;
}

export function emitLlvmModule(mod: IrModule, options: LlvmTargetOptions = {}): string {
Expand Down Expand Up @@ -527,6 +531,7 @@ export class LlEmitter {
private readonly executableTls: boolean;
private readonly emitLibraryIdentity: boolean;
private readonly runtimeAbiMarker: boolean;
private readonly detachedExit: boolean;
/** Interned string literals: UTF-8 text → { symbol, byte length } —
* first-use order, the runtime ABI’s determinism discipline. */
private readonly literals = new Map<string, { sym: string; len: number }>();
Expand Down Expand Up @@ -835,6 +840,7 @@ export class LlEmitter {
!/apple|darwin|windows|mingw|cygwin|win32/.test(options.targetTriple || process.platform);
this.emitLibraryIdentity = options.emitLibraryIdentity !== false;
this.runtimeAbiMarker = options.runtimeAbiMarker === true;
this.detachedExit = options.detachedExit === true;
this.rcHelpers = options.inlineRc === true ? new Set() : null;
// ScrCycHdr.color stays 12 bytes behind a wasm32 object and 16 bytes
// behind a 64-bit object. The wasm32 header pads before color so its
Expand Down Expand Up @@ -1921,6 +1927,10 @@ export class LlEmitter {
...(this.runtimeAbiMarker && this.mod.lib === undefined
? [`declare void @${RUNTIME_ABI_MARKER}()`]
: []),
// Overrides the runtime's weak default (scr_lib.c).
...(this.detachedExit && this.mod.lib === undefined
? [`@scr_detached_exit = constant i8 1`]
: []),
);
for (const d of this.decls) out.push(d);
out.push(``);
Expand Down
9 changes: 8 additions & 1 deletion packages/compiler/src/cli/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
values.profile !== undefined ||
values.strip ||
values["windows-subsystem"] !== undefined ||
values["detached-exit"] ||
(values["npm-static"] ?? []).length > 0 ||
values["provenance-sources"] ||
externalTypeArgs.length > 0 ||
Expand Down Expand Up @@ -151,11 +152,12 @@ async function main(args: string[], host: CliHost): Promise<number> {
values.strip ||
values.ffi !== undefined ||
values["windows-subsystem"] !== undefined ||
values["detached-exit"] ||
(values["npm-static"] ?? []).length > 0 ||
externalTypeArgs.length > 0
) {
fail(
"scriptc build --lib takes no --dynamic/--backend/--emit/--print/--optimization/--strip/--windows-subsystem/--npm-static/--ffi/--external-types: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, and external type mappings belong to coverage",
"scriptc build --lib takes no --dynamic/--backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--npm-static/--ffi/--external-types: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, and external type mappings belong to coverage",
);
}
const profilePath = resolve(profileArg);
Expand Down Expand Up @@ -198,6 +200,9 @@ async function main(args: string[], host: CliHost): Promise<number> {
if (command === "coverage" && values.strip) {
fail(`--strip is a build/run option\n\n${USAGE}`);
}
if (command === "coverage" && values["detached-exit"]) {
fail(`--detached-exit is a build/run option\n\n${USAGE}`);
}
if (
values.print !== undefined &&
values.print !== "native-link-info" &&
Expand Down Expand Up @@ -292,6 +297,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
sanitize: values.sanitize,
...(values.optimization === undefined ? {} : { optimization: values.optimization }),
strip: values.strip,
detachedExit: values["detached-exit"],
...(windowsSubsystem === undefined ? {} : { windowsSubsystem }),
...(values.ffi === undefined ? {} : { ffi: values.ffi }),
});
Expand Down Expand Up @@ -380,6 +386,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
...(optimization !== undefined ? { optimization } : {}),
...(values.strip ? { strip: true } : {}),
...(windowsSubsystem !== undefined ? { windowsSubsystem } : {}),
...(values["detached-exit"] ? { detachedExit: true } : {}),
...(npmStatic !== undefined ? { npmStatic } : {}),
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
...(printNativeLinkInfo ? { nativeLinkInfo: true } : {}),
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler/src/cli/output-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface OutputOptionValues {
optimization?: string;
strip?: boolean;
windowsSubsystem?: string;
detachedExit?: boolean;
ffi?: string;
}

Expand Down Expand Up @@ -69,6 +70,9 @@ export function resolveOutputOptions(
if (values.strip && emit !== "exe") {
return { ok: false, message: `--strip is only supported with --emit=exe` };
}
if (values.detachedExit && emit !== "exe") {
return { ok: false, message: `--detached-exit is only supported with --emit=exe` };
}
if (emit === "ir" && backend !== undefined) {
return {
ok: false,
Expand Down
6 changes: 6 additions & 0 deletions packages/compiler/src/cli/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ Options:
--windows-subsystem <console|gui>
Windows executable subsystem (default: console). gui
prevents Windows from opening a console window
--detached-exit
Linux: after process.exit, free the program's memory in
a helper process so the parent sees the exit sooner. The
helper keeps the executable mapped and its memory charged
for up to ~100 ms after exit (opt in; --emit=exe only)
--keep-llvm keep generated LLVM IR beside the executable (default)
--no-keep-llvm delete generated LLVM IR after compiling
--emit-ir also write IR beside an executable or library archive;
Expand Down Expand Up @@ -81,6 +86,7 @@ export const CLI_OPTIONS = {
optimization: { type: "string" },
strip: { type: "boolean", default: false },
"windows-subsystem": { type: "string" },
"detached-exit": { type: "boolean", default: false },
"keep-llvm": { type: "boolean", default: true },
"emit-ir": { type: "boolean", default: false },
sanitize: { type: "boolean", default: false },
Expand Down
5 changes: 5 additions & 0 deletions packages/compiler/src/compile-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export interface CompileBaseOptions {
/** Windows PE executable subsystem. Console is the default; GUI suppresses
* automatic console-window creation. Only valid for Windows executables. */
windowsSubsystem?: WindowsSubsystem;
/** On Linux, hand the address-space teardown after `process.exit` to a
* helper process so the parent sees the exit sooner. The helper keeps the
* executable mapped and the memory charged for up to ~100 ms after exit.
* Executables only; no effect on other platforms. */
detachedExit?: boolean;
/** --npm-static: package names whose shipped, unminified JS compiles
* STATICALLY as program modules (inference types the bodies; statements
* the lowering cannot prove become runtime fences). "auto" opts in every
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/executable/executable-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ test("early executable keys isolate compile modes, paths, implementation, and FF
{ ...f.options, dynamic: true },
{ ...f.options, optimization: "dev" },
{ ...f.options, optimization: "speed" },
{ ...f.options, detachedExit: true },
{ ...f.options, npmStatic: "auto" },
{ ...f.options, npmStatic: ["commander"] },
{ ...f.options, outPath: join(f.options.outDir, "other") },
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler/src/executable/executable-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ export interface EarlyExecutableCacheOptions {
strip?: true;
/** Omitted for the default Windows console subsystem. */
windowsSubsystem?: "gui";
/** Omitted unless --detached-exit is set. */
detachedExit?: true;
npmStatic: readonly string[] | "auto" | null;
/** Raw manifest identity: path and bytes. Native archives remain under the
* stricter native cache's independent dependency validation. */
Expand Down Expand Up @@ -220,6 +222,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string {
...optimizationKeyParts(options.optimization),
...(options.strip ? ["strip"] : []),
...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []),
...(options.detachedExit ? ["detached-exit"] : []),
options.npmStatic === null
? "<npm-static-off>"
: options.npmStatic === "auto"
Expand Down Expand Up @@ -258,6 +261,7 @@ function routeKey(options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment"
for (const part of optimizationKeyParts(options.optimization)) hash.update(`${part}\0`);
if (options.strip) hash.update("strip\0");
if (options.windowsSubsystem === "gui") hash.update("windows-subsystem-gui\0");
if (options.detachedExit) hash.update("detached-exit\0");
hash
.update(
options.npmStatic === null
Expand Down
15 changes: 15 additions & 0 deletions packages/compiler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,7 @@ async function prepareExecutableInput(
runtimeAbiMarker: useRuntimePack,
objectAudit: opts.sanitize === true,
...(opts.optimization === "speed" ? { inlineRc: true } : {}),
...(opts.detachedExit === true ? { detachedExit: true } : {}),
});
} catch (err) {
if (!(err instanceof LlvmUnsupportedError)) throw err;
Expand Down Expand Up @@ -842,6 +843,19 @@ async function compileTracked(
sourceTexts: new Map(),
};
}
if (opts.detachedExit === true && outputKind !== "exe") {
return {
ok: false,
diagnostics: [
nativeCodegenDiag(
"SC3002",
"--detached-exit is only supported for executable output",
entryPath,
),
],
sourceTexts: new Map(),
};
}
if (opts.nativeLinkInfo === true && outputKind !== "obj") {
return {
ok: false,
Expand Down Expand Up @@ -1017,6 +1031,7 @@ async function compileTracked(
...optimizationField(opts.optimization),
...(opts.strip ? { strip: true as const } : {}),
...(opts.windowsSubsystem === "gui" ? { windowsSubsystem: "gui" as const } : {}),
...(opts.detachedExit === true ? { detachedExit: true as const } : {}),
npmStatic: opts.npmStatic ?? null,
ffiProfile:
opts.ffiProfilePath === undefined || ffiProfileBytes === null
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/native/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ export function prepareNativeExecutable(
runtimeAbiMarker: outputKind === "obj" || outputKind === "exe",
objectAudit: options.sanitize === true,
...(options.optimization === "speed" ? { inlineRc: true } : {}),
...(options.detachedExit === true && outputKind === "exe" ? { detachedExit: true } : {}),
...(options.optimization === "dev" && !options.strip
? { debugSources: prepared.sourceTexts }
: {}),
Expand Down
13 changes: 13 additions & 0 deletions packages/compiler/test/source-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ test("Windows subsystem selection refuses non-executable compiler output", async
await expect(readFile(outPath)).rejects.toMatchObject({ code: "ENOENT" });
});

test("detached exit refuses non-executable compiler output", async () => {
const { entry, outDir } = await fixture();
const outPath = join(outDir, "main.ll");
const result = await compile(entry, { outDir, outPath, outputKind: "llvm", detachedExit: true });
expect(result).toMatchObject({
ok: false,
diagnostics: [
{ code: "SC3002", message: expect.stringContaining("only supported for executable output") },
],
});
await expect(readFile(outPath)).rejects.toMatchObject({ code: "ENOENT" });
});

test("LLVM is the exact primary artifact and never creates an executable", async () => {
const { entry, outDir } = await fixture();
const llvmPath = join(outDir, "exact.llvm-output");
Expand Down
53 changes: 53 additions & 0 deletions packages/runtime/src/scr_lib.c
Original file line number Diff line number Diff line change
Expand Up @@ -1806,6 +1806,56 @@ double scr_process_exit_code_or_zero(void) {
return (double)scr_exit_code_hint_get();
}

#if defined(__linux__)
#include <sched.h>
#include <signal.h>
#include <sys/syscall.h>
#ifndef SYS_close_range
#define SYS_close_range 436 /* the generic syscall table: x86_64 and aarch64 */
#endif

/* Detached teardown (`scriptc build --detached-exit`): the kernel frees an
* exiting process's memory on its critical path, 30-40 ms per GB of 4 KiB
* pages, before its parent sees it exit. A helper process that shares the
* address space (CLONE_VM) and holds no descriptor but the read end of a
* pipe keeps the memory alive past the exit, then exits itself and frees it
* off the parent's critical path (the latency mold's --fork hides). The
* kernel releases an exiting process's memory (exit_mm) before its files
* (exit_files), so EOF on the pipe means the helper holds the last
* reference. The helper keeps the executable mapped and the memory charged
* to the cgroup until then, so a program opts in at build time: the program
* object then defines scr_detached_exit = 1, overriding this weak default. */
__attribute__((weak)) const unsigned char scr_detached_exit = 0;
static char scr_teardown_stack[16384] __attribute__((aligned(64)));

static int scr_teardown_main(void *arg) {
int fd = (int)(intptr_t)arg;
/* Raw syscalls only: the helper runs on the exiting thread's TLS. */
if (fd > 0) (void)syscall(SYS_close_range, 0u, (unsigned)fd - 1u, 0u);
(void)syscall(SYS_close_range, (unsigned)fd + 1u, ~0u, 0u);
char c;
while (syscall(SYS_read, fd, &c, 1) != 0) {
}
syscall(SYS_exit, 0);
return 0;
}

static void scr_detach_teardown(void) {
if (!scr_detached_exit) return;
int fds[2];
if (pipe2(fds, O_CLOEXEC) != 0) return;
sigset_t all, old;
sigfillset(&all);
pthread_sigmask(SIG_SETMASK, &all, &old);
pid_t pid = clone(scr_teardown_main, scr_teardown_stack + sizeof scr_teardown_stack, CLONE_VM | SIGCHLD,
(void *)(intptr_t)fds[0]);
pthread_sigmask(SIG_SETMASK, &old, NULL);
(void)close(fds[0]);
if (pid < 0) (void)close(fds[1]);
/* fds[1] stays open until the process's files are released. */
}
#endif

void scr_process_exit(double code) {
/* Node runs 'exit' listeners on explicit process.exit() too — they run
* HERE, synchronously, before the teardown-free exit below. The hook is
Expand All @@ -1823,6 +1873,9 @@ void scr_process_exit(double code) {
* Node), and the RC audit is meaningless mid-program (live values are
* expected). scr_init's flush-at-exit is also skipped — flush here. */
fflush(stdout);
#if defined(__linux__)
scr_detach_teardown();
#endif
_Exit((int)code);
}

Expand Down
4 changes: 4 additions & 0 deletions packages/runtime/src/scr_runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -3154,6 +3154,10 @@ bool scr_process_kill_named(double pid, const ScrStr *signal);
/* fflush stdout, then _Exit((int)code): no atexit handlers run — the RC
* audit is deliberately skipped (exiting mid-program leaves live values). */
void scr_process_exit(double code);
/* Nonzero hands the address-space teardown after scr_process_exit to a
* helper process (Linux, `--detached-exit`). scr_lib.c defines a weak 0;
* the program object of such an executable defines it as 1. */
extern const unsigned char scr_detached_exit;
void scr_process_set_exit_code(double code);
double scr_process_exit_code_or_zero(void);
/* process._exiting: true once the exit sequence began (process.exit or
Expand Down
Loading