Name the compiler's own shim files independently of the install path (#1941)

This commit is contained in:
Chris Tate
2026-10-22 22:51:57 -05:00
committed by GitHub
6 changed files with 111 additions and 17 deletions
@@ -34,6 +34,31 @@ export function packageShimRoot(): string {
: join(installedDeclarationRoot, "shims");
}
/** The directories of files scriptc itself ships, each with the prefix
* that names its files independently of where scriptc is installed
* (compilerOwnedFileName). IR artifacts spell source locations in them this
* way (serializeModuleParts). */
export function compilerOwnedFileRoots(): readonly { from: string; to: string }[] {
return [{ from: tsgoPath(packageShimRoot()) + "/", to: "scriptc:shims/" }];
}
/** The name of a file scriptc itself ships (a package shim or one of the
* shipped declaration files) that does not depend on where scriptc is
* installed: `scriptc:shims/<path below the shim root>` or
* `scriptc:<declaration file name>`. Null for every other file. Names and
* artifacts that would otherwise spell the installation's absolute path use
* it, so two installations of one compiler emit the same output. */
export function compilerOwnedFileName(file: string): string | null {
const path = tsgoPath(file);
for (const { from, to } of compilerOwnedFileRoots()) {
if (path.startsWith(from)) return to + path.slice(from.length);
}
for (const declaration of [ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]) {
if (path === declaration) return `scriptc:${path.slice(path.lastIndexOf("/") + 1)}`;
}
return null;
}
/** Path of the shipped ambient declarations — the always-shipped CORE
* (comptime, setTimeout). Part of EVERY program scriptc
* builds, the project-world preflight program included. */
@@ -185,7 +185,7 @@ export { newFnCtx, type FnCtx } from "./function-context.js";
*/
import { dirname, relative, resolve } from "node:path";
import { bindingSource } from "../binding-source.js";
import { tsgoPath } from "../dts-paths.js";
import { compilerOwnedFileName, tsgoPath } from "../dts-paths.js";
import * as ts from "../ts7/adapter.js";
import { isBundledShimForwarder } from "../package-shims.js";
import { erasedTypeOnlyImport } from "../type-only-imports.js";
@@ -3210,13 +3210,14 @@ export class Lowerer {
/** Structural source positions (structural-positions.ts) for names that
* stay the same when unrelated code moves. */
/** Files are named by their path relative to the entry's directory, so
* names do not depend on where the program sits, and default library
* files by their base name: two checkers install them in different
* directories. */
* names do not depend on where the program sits, default library files
* by their base name (two checkers install them in different
* directories), and the files scriptc ships by their name inside scriptc,
* so names do not depend on where scriptc is installed either. */
readonly positions = new StructuralPositions((sf) =>
this.program.isSourceFileDefaultLibrary(sf)
? `lib:${sf.fileName.slice(sf.fileName.lastIndexOf("/") + 1)}`
: relative(dirname(this.entry.fileName), sf.fileName),
: (compilerOwnedFileName(sf.fileName) ?? relative(dirname(this.entry.fileName), sf.fileName)),
);
/** The outermost node whose token span is exactly `loc`, or null. */
@@ -3248,7 +3249,10 @@ export class Lowerer {
* before it. */
closureName(loc: SrcLoc, suffix = "", prefix = "%fn"): string {
const node = this.nodeAtLoc(loc);
const where = node === null ? `${loc.file}:${loc.start}:${loc.end}` : this.positions.of(node);
const where =
node === null
? `${compilerOwnedFileName(loc.file) ?? loc.file}:${loc.start}:${loc.end}`
: this.positions.of(node);
const key = `${prefix}|${this.closureRoot}|${where}|${suffix}`;
const first = keyedHelperName(this, prefix, key);
const seen = this.closureNameCounts.get(first) ?? 0;
@@ -3389,8 +3393,18 @@ export class Lowerer {
if (interned.has(fn.name)) this.sharedHelpers.add(fn);
if (this.sharedHelpers.size === 0) return;
const shared = new Map([...this.sharedHelpers].map((fn) => [fn.name, fn]));
const earlier = (left: SrcLoc, right: SrcLoc): boolean =>
left.file < right.file || (left.file === right.file && left.start < right.start);
// Files scriptc ships compare by their name inside scriptc, so the
// choice does not depend on where scriptc is installed.
const fileKeys = new Map<string, string>();
const fileKey = (file: string): string => {
let key = fileKeys.get(file);
if (key === undefined) fileKeys.set(file, (key = compilerOwnedFileName(file) ?? file));
return key;
};
const earlier = (left: SrcLoc, right: SrcLoc): boolean => {
if (left.file === right.file) return left.start < right.start;
return fileKey(left.file) < fileKey(right.file);
};
// The earliest use of each shared helper outside the shared helpers, and
// which shared helpers each shared helper uses.
const uses = new Map<string, SrcLoc>();
+5 -4
View File
@@ -126,6 +126,7 @@ import { moduleRuntimeFeatures } from "./ir/runtime-features.js";
import { serializeModuleParts } from "./ir/serialize.js";
import { readTextParts, writeTextParts } from "./text-parts.js";
import { validateModule } from "./ir/validate.js";
import { compilerOwnedFileRoots } from "./frontend/dts-paths.js";
import { loadProgram } from "./frontend/program-node.js";
import { runFrontend, type FrontendFactory } from "./frontend/pipeline.js";
import {
@@ -785,7 +786,7 @@ async function prepareExecutableInput(
if (outputKind === "ir") {
await mkdir(dirname(opts.outPath), { recursive: true });
await writeTextParts(opts.outPath, serializeModuleParts(mod));
await writeTextParts(opts.outPath, serializeModuleParts(mod, compilerOwnedFileRoots()));
return { ok: true, artifact: { kind: "ir", path: opts.outPath } };
}
@@ -885,7 +886,7 @@ async function prepareExecutableInput(
if (opts.emitIr) {
irPath = defaultSourcePaths.ir;
irSourcePath = privateSiblingPath(irPath, "ir");
await writeTextParts(irSourcePath, serializeModuleParts(mod));
await writeTextParts(irSourcePath, serializeModuleParts(mod, compilerOwnedFileRoots()));
}
const nativeFeatures = executableNativeFeatures(mod, backend, opts.optimization ?? "release");
@@ -1986,7 +1987,7 @@ async function emitSemanticLibraryHit(
let irPath: string | undefined;
if (opts.emitIr) {
irPath = join(opts.outDir, `${stem}.lib.ir.json`);
await writeTextParts(irPath, serializeModuleParts(mod));
await writeTextParts(irPath, serializeModuleParts(mod, compilerOwnedFileRoots()));
}
await compileLibraryNative(profile, llvmPath, archivePath, opts.sanitize ?? false, hit.native);
timing("native-archive");
@@ -2272,7 +2273,7 @@ async function compileLibraryTracked(
let irPath: string | undefined;
if (opts.emitIr) {
irPath = join(opts.outDir, `${stem}.lib.ir.json`);
await writeTextParts(irPath, serializeModuleParts(mod));
await writeTextParts(irPath, serializeModuleParts(mod, compilerOwnedFileRoots()));
}
const nativeFeatures = libraryNativeFeatures(mod, profile.emission);
@@ -198,3 +198,24 @@ test("byte decoding keeps the whole-document errors and version refusal", () =>
const sentinel = text.replace('"nan"', '"future"');
expect(() => deserializeModuleBytes(Buffer.from(sentinel))).toThrow("Invalid IR number sentinel");
});
test("file roots respell the source locations under them and nothing else", () => {
const mod = numbers([1, 2]);
const shim = { file: '/opt/scriptc "a"/shims/pkg/index.ts', start: 3, end: 4 };
const other = { file: "/opt/elsewhere/shims/pkg/index.ts", start: 5, end: 6 };
const body = mod.functions[0]!.body as IrStmt[];
body.push(
{ kind: "exprStmt", expr: { kind: "numLit", value: 3, type: F64, loc: shim }, loc: shim },
{ kind: "exprStmt", expr: { kind: "numLit", value: 4, type: F64, loc: other }, loc: other },
);
const roots = [{ from: '/opt/scriptc "a"/shims/', to: "scriptc:shims/" }];
const relocated = deserializeModule(serializeModuleParts(mod, roots).join(""));
const files = (relocated.functions[0]!.body as IrStmt[]).map((stmt) => stmt.loc.file);
expect(files).toEqual([
"numbers.ts",
"numbers.ts",
"scriptc:shims/pkg/index.ts",
"/opt/elsewhere/shims/pkg/index.ts",
]);
expect(serializeModuleParts(mod).join("")).toBe(serializeModule(mod, true));
});
+33 -3
View File
@@ -46,11 +46,41 @@ function encodeFunction(fn: IrFunction): string | undefined {
return ordinaryNumbers(fn) ? JSON.stringify(fn) : JSON.stringify(fn, encodeNumber);
}
/** A directory whose source location files an IR artifact names by a
* prefix of its own: a location file `from + rest` serializes as
* `to + rest`. */
export interface SerializedFileRoot {
from: string;
to: string;
}
/** Respells the source location files under `roots` in serialized parts.
* A location's file is the first member of its JSON object, and the
* escaped spelling of a path prefix is the prefix of the escaped path. */
function relocateFiles(parts: string[], roots: readonly SerializedFileRoot[]): string[] {
const needles = roots.map(({ from, to }) => ({
from: `"file":${JSON.stringify(from).slice(0, -1)}`,
to: `"file":${JSON.stringify(to).slice(0, -1)}`,
}));
return parts.map((part) =>
needles.reduce(
(text, { from, to }) => (text.includes(from) ? text.replaceAll(from, to) : text),
part,
),
);
}
/** The compact encoding as ordered parts: the module header up to the
* function array, one part per function (with separate comma parts), and the
* header's remainder. Concatenating the parts yields exactly
* `serializeModule(mod, true)`; writers stream them to disk in batches. */
export function serializeModuleParts(mod: IrModule): string[] {
* `serializeModule(mod, true)`; writers stream them to disk in batches.
* `fileRoots` respells the source locations in those directories, so
* artifacts do not depend on where the compiler that wrote them is
* installed. */
export function serializeModuleParts(
mod: IrModule,
fileRoots: readonly SerializedFileRoot[] = [],
): string[] {
// A replacer reads through live native views. Serializing the complete
// function array through one view refreshes every function capsule for
// each element, making large compiler artifacts quadratic. The sentinel
@@ -65,7 +95,7 @@ export function serializeModuleParts(mod: IrModule): string[] {
parts.push(encodeFunction(fn) ?? "null");
}
parts.push("]" + header.slice(offset + slot.length));
return parts;
return fileRoots.length === 0 ? parts : relocateFiles(parts, fileRoots);
}
/** Compiler artifacts use compact JSON; API consumers can retain the
+5 -2
View File
@@ -27,6 +27,7 @@ import {
type FrontendInputExclusions,
trackedReadFile,
} from "../frontend/input-tracker.js";
import { compilerOwnedFileRoots } from "../frontend/dts-paths.js";
import { provenanceSources } from "../frontend/provenance-registry.js";
import { processFork } from "../frontend/process-fork.js";
import { tokenEquivalentSource } from "../frontend/token-equivalence.js";
@@ -377,7 +378,7 @@ export function prepareNativeExecutable(
if (irInput !== "") timing("ir-input");
const ir =
outputKind === "ir" || options.emitIr
? batchTextParts(serializeModuleParts(prepared.mod))
? batchTextParts(serializeModuleParts(prepared.mod, compilerOwnedFileRoots()))
: null;
timing("ir-serialize");
const llvm =
@@ -510,7 +511,9 @@ export function prepareNativeLibrary(
const input: NativeExecutableInput = {
ok: true,
llvm,
ir: options.emitIr ? batchTextParts(serializeModuleParts(prepared.mod)) : null,
ir: options.emitIr
? batchTextParts(serializeModuleParts(prepared.mod, compilerOwnedFileRoots()))
: null,
sidecarJson: prepared.sidecarJson,
features: executableLinkFeatures(prepared.mod),
sources: [...prepared.sourceTexts],