Files
scriptc/packages/compiler/src/backend/lld.ts
T
Malte UblandClaude Opus 5.5 019fc51e31 Keep the generator tag reader and the glibc math unit out of hello
The static hello binary grew past the 96 KiB ceiling of the runtime
tree-shaking test on Linux hosts that link with GNU ld (99,040 B). CI does
not see it: its pinned LLVM ships ld.lld, which the compiler prefers, and
hello links to about 65 KB there. The Sandbox gate installs Ubuntu's
clang-22 without lld, so it links with GNU ld.

dd418b067 crossed the ceiling (98,032 -> 98,960 B). Its scr_glibc_math.c
defines floor, fmod, frexp and other glibc names as hidden functions so a
program can start without libm. With GNU ld that cannot work: ld decides
--as-needed before section GC, so the runtime's unreachable code keeps
libm needed, and it keeps every regular definition of a name a needed
shared library defines, hidden or not. Every executable carried the whole
unit (about 2.5 KB of code and unwind data) and still loaded libm. Links
without lld now leave the unit out; its functions answer what glibc's do.

192ea3247 installed scr_gen_tag_reader from a load-time constructor in
scr_async.c. A constructor is a link root on every format, so each
executable kept scr_gen_tag and, with GNU ld, all of scr_async.c's string
literals (about 1.1 KB). The generator runtime now installs the reader
when it creates a generator, the only time a reader can be needed.

Hello is now 93,928 B with GNU ld and 65,472 B with lld.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:35:52 -07:00

68 lines
2.6 KiB
TypeScript

import { realpathSync, statSync } from "node:fs";
import { basename, delimiter, dirname, isAbsolute, join, resolve } from "node:path";
function isFile(path: string): boolean {
try {
return statSync(path).isFile();
} catch {
return false;
}
}
/** The executable `command` names: a path, or the first match on PATH. */
function commandFile(command: string, env: NodeJS.ProcessEnv): string | null {
if (command.includes("/")) return isFile(command) ? resolve(command) : null;
for (const directory of (env["PATH"] ?? "/usr/bin:/bin").split(delimiter)) {
const candidate = join(directory || ".", command);
if (isFile(candidate)) return isAbsolute(candidate) ? candidate : resolve(candidate);
}
return null;
}
/**
* Whether the compiler driver `driver` finds ld.lld for `-fuse-ld=lld`: in
* the directory of the driver's executable (where clang looks first) or on
* PATH. Linking still falls back to the driver's default linker when a link
* with lld fails.
*/
export function driverFindsLld(driver: string, env: NodeJS.ProcessEnv = process.env): boolean {
const file = commandFile(driver, env);
if (file === null) return false;
try {
for (const directory of new Set([dirname(file), dirname(realpathSync(file))]))
if (isFile(join(directory, "ld.lld"))) return true;
} catch {
return false;
}
return commandFile("ld.lld", env) !== null;
}
/** `-fuse-ld=lld` for Linux links whose driver finds ld.lld. */
export function lldLinkerArgs(
platform: string,
driver: string,
env: NodeJS.ProcessEnv = process.env,
): string[] {
return platform === "linux" && env["SCRIPTC_LINKER"] === undefined && driverFindsLld(driver, env)
? ["-fuse-ld=lld"]
: [];
}
/**
* The link inputs for a Linux link without lld. The runtime's
* scr_glibc_math.c defines glibc's floor, fmod, frexp and a few others as
* hidden functions so that a program calling no other libm function starts
* without libm.so. That works with lld, which garbage-collects the unused
* definitions and decides `--as-needed` from the live code. GNU ld decides
* `--as-needed` before section GC, so the runtime's unreachable code keeps
* libm needed, and it keeps every regular definition of a name that a needed
* shared library also defines, hidden or not: the unit would add all of its
* code to every executable and save nothing. Its functions answer what
* glibc's do, so linking glibc's own instead is equivalent.
*/
export function inputsWithoutLld(platform: string, inputs: readonly string[]): string[] {
return platform === "linux"
? inputs.filter((input) => basename(input) !== "scr_glibc_math.o")
: [...inputs];
}