Files
scriptc/packages/compiler/test/debug-variables.test.ts
Chris Tateandtimokosonen 0af282af54 fix(compiler): expose source variables in LLVM dev builds (#485)
- 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:26:32 -05:00

106 lines
4.7 KiB
TypeScript

import { execFile } from "node:child_process";
import { cp, mkdtemp, readFile, rm } 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/3068-debug-variables");
const sanitize = process.env["SCRIPTC_SAN"] === "1";
test.runIf(process.platform === "darwin")("LLVM dev builds expose source variables and lexical scopes in LLDB", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-debug-variables-"));
try {
const source = join(dir, "source space é");
await cp(corpus, source, { recursive: true });
const entry = join(source, "main.ts");
const outPath = join(dir, "program");
const text = await readFile(entry, "utf8");
const line = (marker: string): number => text.split("\n").findIndex((l) => l.endsWith(`// debug: ${marker}`)) + 1;
const oracle = await exec(process.execPath, [entry]);
for (let build = 0; build < 2; build++) {
const result = await compile(entry, { outDir: dir, outPath, backend: "llvm", optimization: "dev", sanitize });
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics) : "").toBe(true);
if (!result.ok) return;
const native = await exec(outPath);
expect(native.stdout).toBe(oracle.stdout);
expect(native.stderr).toBe(oracle.stderr);
const commands = [
...["globals", "locals", "shadow", "capture", "restored", "loop", "item", "hoisted"].map((marker) =>
`breakpoint set --file main.ts --line ${line(marker)} --move-to-nearest-code false`),
"breakpoint set --file other.ts --line 4 --move-to-nearest-code false",
"run",
"frame variable moduleValue moduleText",
"continue",
"frame variable input enabled fallback value text",
"frame variable text->len",
"frame variable text->data",
"continue",
"frame variable value",
"continue",
"frame variable value",
"continue",
"frame variable value",
"continue",
"frame variable i",
"continue",
"frame variable i",
"continue",
"frame variable item->tag item->slot.arm0",
"continue",
"frame variable item->tag item->slot.arm0",
"continue",
"frame variable hoisted",
"continue",
"frame variable moduleValue",
"continue",
];
const debug = await exec("lldb", ["--batch", outPath, ...commands.flatMap((c) => ["-o", c])]).catch((error: { stdout: string; stderr: string }) => {
throw new Error(`${error.stdout}\n${error.stderr}`);
});
expect(debug.stderr).toBe("");
expect(debug.stdout).not.toContain("pending");
for (const value of ["moduleValue = 42", "moduleValue = 100", "input = 3", "enabled = true", "fallback = 9", "value = 4", "value = 99", "value = 6", "i = 0", "i = 1", "item->tag = f64", "item->slot.arm0 = 6", "item->slot.arm0 = 7", "hoisted = 13"]) {
expect(debug.stdout).toContain(value);
}
expect(debug.stdout).toContain("text->len = 8");
expect(debug.stdout).toContain('text->data = "hello é"');
const shadowValues = [...debug.stdout.matchAll(/\(lldb\) frame variable value\r?\n\(double\) value = (\d+)/g)].map((match) => Number(match[1]));
expect(shadowValues).toEqual([99, 6, 6]);
expect(debug.stdout).not.toContain("sc_l_");
// Exercise the executable cache's restored dSYM, with native objects
// and the first linked binary already gone.
await rm(outPath);
await rm(`${outPath}.dSYM`, { recursive: true });
}
} finally {
await rm(dir, { recursive: true, force: true });
}
});
test("release LLVM output omits dev variable metadata", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-debug-modes-"));
try {
for (const mode of ["dev", "release"] as const) {
const outPath = join(dir, `${mode}.ll`);
const result = await compile(join(corpus, "main.ts"), {
outDir: dir, outPath, outputKind: "llvm",
optimization: mode,
});
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics) : "").toBe(true);
const llvm = await readFile(outPath, "utf8");
if (mode === "dev") {
expect(llvm).toContain("emissionKind: FullDebug");
expect(llvm).toContain('!DILocalVariable(name: "input", arg: 1');
expect(llvm).not.toContain("LineTablesOnly");
} else {
expect(llvm).not.toMatch(/!DI|llvm\.dbg\./);
}
}
} finally {
await rm(dir, { recursive: true, force: true });
}
});