From ef8ee2b3939b342b83be6a169ac256b7ba77cf84 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 13:03:33 -0500 Subject: [PATCH] Emit a debug build's function bodies in one emitter Debug metadata is numbered per emitter (debug-info.ts) and the range journal does not carry it, so a debug build emitted in ranges or on workers referred to metadata the merged module did not define. Debug builds now emit every function body in the emitter itself. --- .../compiler/src/backend/llvm/emit-module.ts | 7 ++- packages/compiler/src/backend/llvm/emitter.ts | 10 ++-- .../compiler/test/chunked-emission.test.ts | 53 +++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts index 75e6ad50f..1f66e566f 100644 --- a/packages/compiler/src/backend/llvm/emit-module.ts +++ b/packages/compiler/src/backend/llvm/emit-module.ts @@ -192,10 +192,13 @@ function readRange(dir: string, range: number): { result: ChunkResult; texts: st export function emitModuleParts(emitter: LlEmitter): string[] { // Function bodies first (the literal/unit/fn-value tables fill as they // emit), then the file assembles around them — the runtime ABI’s order. + // Debug metadata is numbered per emitter and not journaled, so a debug + // build emits every body here. + const ranges = emitter.debug === null; const chunks = emitter.targetOptions.functionChunks ?? 1; const fnDefs = - (chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ?? - emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) ?? + (ranges && chunks > 1 ? emitFunctionsInChunks(emitter, chunks) : null) ?? + (ranges ? emitFunctionsOnWorkers(emitter, emitter.targetOptions.functionWorkers ?? 1) : null) ?? []; if (fnDefs.length === 0) for (const fn of emitter.mod.functions) { diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 1ca9313b7..2daabdb92 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -414,11 +414,13 @@ export interface LlvmTargetOptions { targetTriple?: string; /** Emit the function bodies in this many contiguous ranges, each by its * own emitter, and merge them in function order (emit-journal.ts). The - * output is the same as one emitter's. */ + * output is the same as one emitter's. Debug builds (`debugSources`) + * emit in one emitter. */ functionChunks?: number; /** Emit the function bodies of a large module on this many forked worker * processes at a time (emit-module.ts), forked with `forkPrimitives`. The - * output is the same as one emitter's. */ + * output is the same as one emitter's. Debug builds (`debugSources`) + * emit in one emitter. */ functionWorkers?: number; /** The host's process primitives, where it can fork (the native * compiler's). */ @@ -1273,7 +1275,9 @@ export class LlEmitter { this.journal = journal; this.journaled = journal !== null || - ((options.functionWorkers ?? 1) > 1 && options.forkPrimitives !== undefined); + ((options.functionWorkers ?? 1) > 1 && + options.forkPrimitives !== undefined && + options.debugSources === undefined); this.stackTraces = observesStacks(mod); this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined); this.mockTimers = this.runtimeFeatures.mockTimers; diff --git a/packages/compiler/test/chunked-emission.test.ts b/packages/compiler/test/chunked-emission.test.ts index 9902569f7..b4912c9f6 100644 --- a/packages/compiler/test/chunked-emission.test.ts +++ b/packages/compiler/test/chunked-emission.test.ts @@ -67,3 +67,56 @@ test("function bodies emitted in chunks merge into the text one emitter gives", await rm(dir, { recursive: true, force: true }); } }, 600_000); + +// Debug metadata is numbered per emitter (debug-info.ts) and not journaled: +// a debug build emits its function bodies in one emitter. +test("debug builds emit function bodies serially, with every metadata reference defined", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-chunked-debug-")); + const minFunctions = process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"]; + try { + const corpus = join(import.meta.dirname, "../../../tests/corpus/3066-source-debugging"); + const entry = join(corpus, "main.ts"); + const outPath = join(dir, "main.ir.json"); + const result = await compile(entry, { + outDir: dir, + outPath, + outputKind: "ir", + optimization: "dev", + }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const ir = await readFile(outPath, "utf8"); + const debugSources = new Map(); + for (const name of ["main.ts", "helper.ts"]) + debugSources.set(join(corpus, name), await readFile(join(corpus, name), "utf8")); + const options: LlvmTargetOptions = { debugSources }; + const serial = text(emitLlvmModuleSource(deserializeModule(ir), options)); + expect(serial).toContain("!DILocation"); + + let forks = 0; + process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = "1"; + const forkPrimitives = { + fork: (): number => { + forks++; + return -1; + }, + exit: (): void => {}, + wait: (): number => -1, + }; + for (const variant of [ + { functionChunks: 3 }, + { functionWorkers: 4, forkPrimitives }, + ] satisfies LlvmTargetOptions[]) { + const emitted = text(emitLlvmModuleSource(deserializeModule(ir), { ...options, ...variant })); + expect(emitted === serial, JSON.stringify(Object.keys(variant))).toBe(true); + const defined = new Set([...emitted.matchAll(/^(![0-9]+) = /gm)].map((m) => m[1])); + const referenced = new Set([...emitted.matchAll(/(![0-9]+)\b/g)].map((m) => m[1])); + expect([...referenced].filter((id) => !defined.has(id))).toEqual([]); + } + expect(forks).toBe(0); + } finally { + if (minFunctions === undefined) delete process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"]; + else process.env["SCRIPTC_EMIT_WORKER_FUNCTIONS"] = minFunctions; + await rm(dir, { recursive: true, force: true }); + } +}, 120_000);