Files
scriptc/packages/compiler/test/source-debugging.test.ts
T
Chris Tateandtimokosonen e30d11219a fix(compiler): expose source variables in LLVM dev builds
- Preserve source bindings and lexical scopes for native debugger inspection.
- Verify variable values, module isolation, and cache restores in LLDB, and document native representations.

Fixes #483

Co-authored-by: timokosonen <325538238+timokosonen@users.noreply.github.com>
2026-09-27 08:19:49 -05:00

114 lines
5.0 KiB
TypeScript

import { execFile } from "node:child_process";
import { cp, mkdtemp, readFile, rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { expect, test } from "vitest";
import { compile } from "../src/index.js";
const exec = promisify(execFile);
const corpus = join(import.meta.dirname, "../../../tests/corpus/3066-source-debugging");
const sanitize = process.env["SCRIPTC_SAN"] === "1";
// Xcode ships this debugger. Resolve real breakpoints after temporary native
// objects have been deleted, including after the executable cache restores.
test.runIf(process.platform === "darwin").each(["llvm", "c"] as const)(
"dev %s builds preserve source breakpoints across cache hits",
async (backend) => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-debug-"));
try {
const source = join(dir, 'source space é "quoted"');
await cp(corpus, source, { recursive: true });
const entry = join(source, "main.ts");
const outPath = join(dir, "program");
const options = { outDir: dir, outPath, backend, optimization: "dev" as const, sanitize };
const oracle = await exec(process.execPath, [entry]);
for (let build = 0; build < 2; build++) {
const result = await compile(entry, options);
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics) : "").toBe(true);
if (!result.ok) return;
expect((await stat(`${outPath}.dSYM/Contents/Resources/DWARF/program`)).size).toBeGreaterThan(0);
const native = await exec(outPath);
expect(native.stdout).toBe(oracle.stdout);
expect(native.stderr).toBe(oracle.stderr);
const debug = await exec("lldb", ["--batch", outPath,
"-o", "breakpoint set --file main.ts --line 4 --move-to-nearest-code false",
"-o", "breakpoint set --file helper.ts --line 4 --move-to-nearest-code false",
"-o", "breakpoint list",
]);
expect(debug.stdout).not.toContain("pending");
expect(debug.stdout).toContain("main.ts:4");
expect(debug.stdout).toContain("helper.ts:4");
await rm(outPath);
await rm(`${outPath}.dSYM`, { recursive: true });
}
for (const mode of ["release", "dev", "strip"] as const) {
const result = await compile(entry, {
...options,
optimization: mode === "release" ? "release" : "dev",
strip: mode === "strip",
});
expect(result.ok).toBe(true);
if (mode === "dev") {
expect((await stat(`${outPath}.dSYM`)).isDirectory()).toBe(true);
} else {
await expect(stat(`${outPath}.dSYM`)).rejects.toMatchObject({ code: "ENOENT" });
}
}
} finally {
await rm(dir, { recursive: true, force: true });
}
},
);
test.runIf(process.platform === "darwin")("dev LLVM object and assembly outputs carry source locations", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-debug-object-"));
try {
for (const outputKind of ["obj", "asm"] as const) {
const outPath = join(dir, `program.${outputKind}`);
const result = await compile(join(corpus, "main.ts"), { outDir: dir, outPath, outputKind, optimization: "dev" });
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics) : "").toBe(true);
if (!result.ok) continue;
if (outputKind === "obj") {
const { stdout } = await exec("dwarfdump", ["--debug-line", outPath]);
expect(stdout).toContain("main.ts");
expect(stdout).toContain("helper.ts");
const info = await exec("dwarfdump", ["--debug-info", outPath]);
expect(info.stdout).toContain("DW_TAG_variable");
expect(info.stdout).toContain("DW_TAG_formal_parameter");
} else {
const assembly = await readFile(outPath, "utf8");
expect(assembly).toContain(".loc");
expect(assembly).toContain("helper.ts");
}
}
} finally {
await rm(dir, { recursive: true, force: true });
}
});
test.each([
"800-switch-basics.ts",
"1021-async-ordering.ts",
"1452-return-through-finally.ts",
"2013-generators-sent-values.ts",
"603-closures-rc-stress.ts",
])("dev code generation preserves control flow: %s", async (fixture) => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-debug-flow-"));
try {
const entry = join(corpus, "..", fixture);
const node = await exec(process.execPath, [entry]);
for (const backend of ["c", "llvm"] as const) {
const outPath = join(dir, process.platform === "win32" ? `${backend}.exe` : backend);
const built = await compile(entry, { outDir: dir, outPath, backend, optimization: "dev", sanitize });
expect(built.ok, !built.ok ? JSON.stringify(built.diagnostics) : "").toBe(true);
if (!built.ok) continue;
const native = await exec(outPath);
expect(native.stdout).toBe(node.stdout);
expect(native.stderr).toBe(node.stderr);
}
} finally {
await rm(dir, { recursive: true, force: true });
}
});