mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-18 05:19:29 +08:00
- 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>
106 lines
4.7 KiB
TypeScript
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 });
|
|
}
|
|
});
|