mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
fix(compiler): support source breakpoints in dev builds (#481)
- Emit source locations for native TypeScript and JavaScript debugging across C and LLVM backends. - Preserve macOS debug symbols through linking and cache reuse, with regression coverage and Xcode guidance. Fixes #461 Co-authored-by: timokosonen <325538238+timokosonen@users.noreply.github.com>
This commit is contained in:
co-authored by
timokosonen
parent
8272aa83c2
commit
0e6a13d041
@@ -119,6 +119,9 @@ Prebuilds release runtime objects and native TLS/dynamic-engine archives for tar
|
||||
<dt><code>--strip</code></dt>
|
||||
<dd>Remove symbol and debug payload when linking an executable, including cross-compiled targets. This opt-in flag works with either backend and <code>--from-c</code> without changing code optimization.</dd>
|
||||
|
||||
<dt><code>--optimization <release|dev></code></dt>
|
||||
<dd>Select native optimization for executable, assembly, or object output. The default is <code>release</code> (<code>-O2</code>). <code>dev</code> uses <code>-O0</code> and emits source locations for TypeScript and JavaScript breakpoints and stack frames. macOS executable builds also produce an adjacent <code>.dSYM</code> bundle.</dd>
|
||||
|
||||
<dt><code>--windows-subsystem <console|gui></code></dt>
|
||||
<dd>Select the subsystem of a Windows executable. <code>console</code> is the default; <code>gui</code> prevents Windows from opening a console window for the app. Valid only for executable builds targeting Windows, including <code>--from-c</code>.</dd>
|
||||
|
||||
@@ -179,6 +182,8 @@ fib-linux: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically
|
||||
|
||||
The default backend emits textual LLVM IR. On supported targets it is lowered to an object by the bundled helper and linked with the matching precompiled runtime pack; <code>SCRIPTC_LINKER</code> controls the platform linker driver, separately from C compilation. A program outside the LLVM tier is never miscompiled—the native build falls back to the C backend transparently and says so in one stderr line. The production <code>wasm32-wasi</code> target uses LLVM's 32-bit ABI path and never falls back; an LLVM coverage gap is a build diagnostic. Dynamic npm embedding is LLVM surface on every target.
|
||||
|
||||
For source breakpoints in Xcode or LLDB, build with <code>--optimization=dev</code>. In an Xcode custom build rule, add this option to the <code>scriptc build</code> invocation and list both the executable and its <code>.dSYM</code> bundle as outputs. Keep the bundle next to the executable and retain the original source files at their build-time paths. Both the LLVM and C backends preserve source locations across imported modules.
|
||||
|
||||
The C backend is a debugging aid: deliberately readable, source-line-annotated output with differential tests against LLVM wherever the two overlap. Pin it when you want to inspect what your program became:
|
||||
|
||||
```console
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
Honesty is the product. The static surface is large but not total, and a few behaviors diverge from Node **by design**. This page is the plain-language version; `scriptc coverage` on your program is the real answer for your code, and every blocker it reports is specific and coded. Nothing on this page is silent: everything here is either a compile error or a documented, numbered divergence.
|
||||
|
||||
## Native debugging
|
||||
|
||||
Executable builds with `--optimization=dev` support source breakpoints and native stack frames for statically compiled TypeScript and JavaScript. Debuggers see native representations; TypeScript variable inspection, expression evaluation, and stepping through code executed by the dynamic engine are not implemented. Release builds and builds with `--strip` omit these source mappings. macOS uses an adjacent `.dSYM` bundle generated with the Command Line Tools' `dsymutil`.
|
||||
|
||||
## What doesn't compile (yet)
|
||||
|
||||
These are rejected at compile time with an `SC` code, a code frame, and usually a rewrite hint. A non-exhaustive tour of the ones you're most likely to meet:
|
||||
|
||||
@@ -33,8 +33,8 @@ Options:
|
||||
its C inspection lane accepts async-free programs only
|
||||
--optimization <release|dev>
|
||||
native optimization posture (default: release/-O2). dev
|
||||
uses -O0 and stable cached LLVM object shards for faster
|
||||
edits of large programs
|
||||
uses -O0, source breakpoints, and cached LLVM object shards;
|
||||
macOS executable builds also produce an adjacent .dSYM
|
||||
--strip remove symbol/debug payload from the linked executable
|
||||
for smaller builds (opt in; --emit=exe only)
|
||||
--windows-subsystem <console|gui>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { SourceLocations } from "../source-locations.js";
|
||||
/* IR → C. Three-address style: every IR expression lands in a fresh C temp.
|
||||
* Verbose (clang -O2 erases it) but buys three things: short-circuit
|
||||
* emission is trivially correct, reference counting has one mechanical
|
||||
@@ -70,6 +71,8 @@ import { emitExpr, liveDynRefAdapter as buildLiveDynRefAdapter, type StreamTyped
|
||||
import { emitLibraryIdentityLines } from "../library-identity-markers.js";
|
||||
|
||||
export interface CEmitOptions {
|
||||
/** Exact frontend sources for dev-build #line directives. */
|
||||
debugSources?: ReadonlyMap<string, string>;
|
||||
/** Library archive assembly may move the volatile identity getters into a
|
||||
* separate translation unit. Public/direct emission keeps them by default. */
|
||||
emitLibraryIdentity?: boolean;
|
||||
@@ -156,6 +159,8 @@ function ffiCallbackDummyC(callback: IrFfiCallbackParam["callback"]): string {
|
||||
}
|
||||
|
||||
export class CEmitter {
|
||||
private readonly debug: SourceLocations | null;
|
||||
sourceLoc: SrcLoc | null = null;
|
||||
readonly lines: string[] = [];
|
||||
indent = 0;
|
||||
tempCounter = 0;
|
||||
@@ -440,6 +445,7 @@ export class CEmitter {
|
||||
sourceText?: string,
|
||||
private readonly options: CEmitOptions = {},
|
||||
) {
|
||||
this.debug = options.debugSources === undefined ? null : new SourceLocations(options.debugSources);
|
||||
this.constantNumericTables = findConstantNumericTables(mod);
|
||||
this.ffiCallbackAdapters = allocateFfiCallbackAdapters(mod.ffiImports ?? []);
|
||||
this.ffiHasRetainedCallback = hasRetainedFfiCallback(mod.ffiImports ?? []);
|
||||
@@ -1580,9 +1586,20 @@ export class CEmitter {
|
||||
}
|
||||
|
||||
line(text: string): void {
|
||||
const pos = this.sourceLoc === null ? null : this.debug?.position(this.sourceLoc);
|
||||
if (pos !== null && pos !== undefined && text.trim() !== "") {
|
||||
// Repeat the directive for every generated line: one TS statement can
|
||||
// expand into many native instructions, all belonging to that line.
|
||||
this.lines.push(`#line ${pos.line} ${JSON.stringify(pos.file)}`);
|
||||
}
|
||||
this.lines.push(" ".repeat(this.indent) + text);
|
||||
}
|
||||
|
||||
endSourceFunction(): void {
|
||||
this.sourceLoc = null;
|
||||
if (this.debug !== null) this.lines.push('#line 1 "<scriptc>"');
|
||||
}
|
||||
|
||||
srcComment(loc: SrcLoc): string {
|
||||
if (!this.lineStarts) return "";
|
||||
let lo = 0, hi = this.lineStarts.length - 1;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { endsWithJump, matchStringSelfConcat } from "../../ir/analysis.js";
|
||||
|
||||
|
||||
export function emitFunction(emitter: CEmitter, fn: IrFunction): void {
|
||||
emitter.sourceLoc = fn.loc;
|
||||
emitter.tempCounter = 0;
|
||||
emitter.frames = [];
|
||||
emitter.scopes = [];
|
||||
@@ -93,6 +94,8 @@ export function emitFunction(emitter: CEmitter, fn: IrFunction): void {
|
||||
|
||||
emitter.indent--;
|
||||
emitter.line(`}`);
|
||||
// Keep generated runtime scaffolding out of the user's source file.
|
||||
emitter.endSourceFunction();
|
||||
emitter.line(``);
|
||||
}
|
||||
|
||||
@@ -166,6 +169,16 @@ export function emitStmts(emitter: CEmitter, stmts: IrStmt[]): void {
|
||||
}
|
||||
|
||||
export function emitStmt(emitter: CEmitter, s: IrStmt): void {
|
||||
const previous = emitter.sourceLoc;
|
||||
emitter.sourceLoc = s.loc;
|
||||
try {
|
||||
emitStmtBody(emitter, s);
|
||||
} finally {
|
||||
emitter.sourceLoc = previous;
|
||||
}
|
||||
}
|
||||
|
||||
function emitStmtBody(emitter: CEmitter, s: IrStmt): void {
|
||||
emitter.frames.push([]);
|
||||
switch (s.kind) {
|
||||
case "varDecl": {
|
||||
@@ -1034,8 +1047,9 @@ export function emitStmt(emitter: CEmitter, s: IrStmt): void {
|
||||
/** Emits `if (cond) ` followed by a block on the same line for readability. */
|
||||
export function mergeBrace(emitter: CEmitter, emitBlockFn: () => void): void {
|
||||
const head = emitter.lines.pop()!;
|
||||
const before = emitter.lines.length;
|
||||
let before = emitter.lines.length;
|
||||
emitBlockFn();
|
||||
while (emitter.lines[before]?.startsWith("#line ")) before++;
|
||||
emitter.lines[before] = head + emitter.lines[before]!.trimStart();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
|
||||
import { basename, dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const HEADER = Buffer.from("SCDSYM01");
|
||||
|
||||
export function needsDarwinDebugSymbols(platform: string, optimization?: string, strip = false): boolean {
|
||||
return platform === "darwin" && optimization === "dev" && !strip;
|
||||
}
|
||||
|
||||
/** Run before deleting link inputs: Mach-O executables retain a debug map,
|
||||
* while the actual line tables live in those temporary object files. */
|
||||
export async function createDarwinDebugSymbols(binary: string): Promise<void> {
|
||||
await exec("dsymutil", [binary, "-o", `${binary}.dSYM`]);
|
||||
}
|
||||
|
||||
/** Keep the two dSYM payloads together as one integrity-checked cache file.
|
||||
* Names are fixed by the output, never accepted from serialized cache data. */
|
||||
export async function readDarwinDebugSymbols(binary: string): Promise<Buffer> {
|
||||
const contents = join(`${binary}.dSYM`, "Contents");
|
||||
const [plist, dwarf] = await Promise.all([
|
||||
readFile(join(contents, "Info.plist")),
|
||||
readFile(join(contents, "Resources", "DWARF", basename(binary))),
|
||||
]);
|
||||
const sizes = Buffer.alloc(4);
|
||||
sizes.writeUInt32LE(plist.length);
|
||||
return Buffer.concat([HEADER, sizes, plist, dwarf]);
|
||||
}
|
||||
|
||||
export async function installDarwinDebugSymbols(bytes: Buffer, binary: string): Promise<void> {
|
||||
if (bytes.length < 12 || !bytes.subarray(0, 8).equals(HEADER)) throw new Error("invalid cached dSYM");
|
||||
const plistEnd = 12 + bytes.readUInt32LE(8);
|
||||
if (plistEnd <= 12 || plistEnd >= bytes.length) throw new Error("invalid cached dSYM sizes");
|
||||
const current = await readDarwinDebugSymbols(binary).catch(() => null);
|
||||
if (current?.equals(bytes)) return;
|
||||
const stage = await mkdtemp(join(dirname(binary), ".scriptc-dsym-"));
|
||||
try {
|
||||
const contents = join(stage, "Contents");
|
||||
const dwarfDir = join(contents, "Resources", "DWARF");
|
||||
await mkdir(dwarfDir, { recursive: true });
|
||||
await Promise.all([
|
||||
writeFile(join(contents, "Info.plist"), bytes.subarray(12, plistEnd)),
|
||||
writeFile(join(dwarfDir, basename(binary)), bytes.subarray(plistEnd)),
|
||||
]);
|
||||
await rm(`${binary}.dSYM`, { recursive: true, force: true });
|
||||
await rename(stage, `${binary}.dSYM`);
|
||||
} finally {
|
||||
await rm(stage, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
import type { FfiProfile } from "../ffi/ffi-manifest.js";
|
||||
import type { NativeLinkFeatures } from "./native-link-info.js";
|
||||
import type { NativeArtifactDependency } from "./native-toolchain.js";
|
||||
import { needsDarwinDebugSymbols } from "./debug-symbols.js";
|
||||
import { loadRuntimePack, type RuntimePackSelection } from "./runtime-pack.js";
|
||||
import {
|
||||
executableOptimizationLinkerArgs,
|
||||
@@ -30,6 +31,7 @@ export interface NativeLinkPlan {
|
||||
/** Inputs already snapshotted by the program-object emitter. */
|
||||
programObjectDependencies: NativeArtifactDependency[];
|
||||
runtimePack: RuntimePackSelection;
|
||||
darwinDebugSymbols?: boolean;
|
||||
}
|
||||
|
||||
export async function createNativeLinkPlan(options: {
|
||||
@@ -48,6 +50,7 @@ export async function createNativeLinkPlan(options: {
|
||||
const runtimePack = await loadRuntimePack(options);
|
||||
return {
|
||||
target: options.target,
|
||||
darwinDebugSymbols: needsDarwinDebugSymbols(options.target.platform, options.optimization, options.strip),
|
||||
outputPath: options.outPath,
|
||||
inputs: [
|
||||
options.programObject,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "./native-toolchain.js";
|
||||
import { RuntimePackError, stageRuntimePackArtifacts } from "./runtime-pack.js";
|
||||
import type { NativeLinkPlan } from "./link-plan.js";
|
||||
import { createDarwinDebugSymbols, installDarwinDebugSymbols, readDarwinDebugSymbols } from "./debug-symbols.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -254,10 +255,17 @@ export async function linkNativeExecutable(
|
||||
await execFileAsync(linker, args);
|
||||
const output = await stat(privateOut);
|
||||
if (!output.isFile() || output.size === 0) throw new Error("linker produced no executable");
|
||||
if (plan.darwinDebugSymbols) {
|
||||
await createDarwinDebugSymbols(privateOut);
|
||||
await installDarwinDebugSymbols(await readDarwinDebugSymbols(privateOut), plan.outputPath);
|
||||
}
|
||||
await rename(privateOut, plan.outputPath).catch(async () => {
|
||||
await rm(plan.outputPath, { force: true });
|
||||
await rename(privateOut, plan.outputPath);
|
||||
});
|
||||
if (plan.target.platform === "darwin" && !plan.darwinDebugSymbols) {
|
||||
await rm(`${plan.outputPath}.dSYM`, { recursive: true, force: true });
|
||||
}
|
||||
if (
|
||||
options.onArtifactReady !== undefined && preLinkDependencies !== null &&
|
||||
await nativeArtifactDependenciesStillMatch(preLinkDependencies).catch(() => false)
|
||||
|
||||
@@ -19,6 +19,7 @@ export class BlockBuilder {
|
||||
readonly entryAllocas: string[] = [];
|
||||
private tempCounter = 0;
|
||||
private labelCounter = 0;
|
||||
debugLocation: string | null = null;
|
||||
|
||||
constructor() {
|
||||
this.cur = { label: "entry", lines: [], term: null };
|
||||
@@ -35,7 +36,21 @@ export class BlockBuilder {
|
||||
}
|
||||
|
||||
line(s: string): void {
|
||||
if (this.cur.term === null) this.cur.lines.push(` ${s}`);
|
||||
if (this.cur.term === null) this.cur.lines.push(` ${this.withDebugLocation(s)}`);
|
||||
}
|
||||
|
||||
private withDebugLocation(s: string): string {
|
||||
if (this.debugLocation === null || s.trimStart().startsWith(";")) return s;
|
||||
// Instruction comments must follow the attachment. Quoted LLVM operands
|
||||
// can themselves contain semicolons; those are not comment delimiters.
|
||||
let quoted = false;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
if (s[i] === '"') quoted = !quoted;
|
||||
if (s[i] === ";" && !quoted) {
|
||||
return `${s.slice(0, i).trimEnd()}, !dbg ${this.debugLocation} ${s.slice(i)}`;
|
||||
}
|
||||
}
|
||||
return `${s}, !dbg ${this.debugLocation}`;
|
||||
}
|
||||
|
||||
tmp(): string {
|
||||
@@ -51,7 +66,7 @@ export class BlockBuilder {
|
||||
}
|
||||
|
||||
terminate(s: string): void {
|
||||
if (this.cur.term === null) this.cur.term = ` ${s}`;
|
||||
if (this.cur.term === null) this.cur.term = ` ${this.withDebugLocation(s)}`;
|
||||
}
|
||||
|
||||
br(label: string): void {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { basename, dirname } from "node:path";
|
||||
import type { IrFunction, SrcLoc } from "../../ir/ir.js";
|
||||
import { SourceLocations } from "../source-locations.js";
|
||||
import { mangleFunction } from "../mangle.js";
|
||||
|
||||
/** LLVM metadata strings use UTF-8 byte escapes, including for paths. */
|
||||
function quoted(text: string): string {
|
||||
return '"' + [...Buffer.from(text)].map((byte) =>
|
||||
byte >= 0x20 && byte < 0x7f && byte !== 0x22 && byte !== 0x5c
|
||||
? String.fromCharCode(byte)
|
||||
: `\\${byte.toString(16).padStart(2, "0").toUpperCase()}`,
|
||||
).join("") + '"';
|
||||
}
|
||||
|
||||
/** Source breakpoints and stack frames; no claim about TypeScript locals. */
|
||||
export class LlvmDebugInfo {
|
||||
private readonly nodes: string[] = [];
|
||||
private readonly files = new Map<string, string>();
|
||||
private readonly locations = new Map<string, string>();
|
||||
private readonly scopes = new Map<string, string>();
|
||||
private readonly source: SourceLocations;
|
||||
private readonly unit: string;
|
||||
private readonly signature: string;
|
||||
private readonly version: string;
|
||||
private readonly dwarf: string;
|
||||
|
||||
constructor(sourceFile: string, sources: ReadonlyMap<string, string>) {
|
||||
this.source = new SourceLocations(sources);
|
||||
const file = this.file(sourceFile);
|
||||
this.unit = this.add(`distinct !DICompileUnit(language: DW_LANG_C11, file: ${file}, producer: "scriptc", isOptimized: false, runtimeVersion: 0, emissionKind: LineTablesOnly)`);
|
||||
this.signature = this.add(`!DISubroutineType(types: ${this.add("!{}")})`);
|
||||
this.version = this.add('!{i32 2, !"Debug Info Version", i32 3}');
|
||||
this.dwarf = this.add('!{i32 2, !"Dwarf Version", i32 4}');
|
||||
}
|
||||
|
||||
private add(node: string): string {
|
||||
const id = `!${this.nodes.length}`;
|
||||
this.nodes.push(`${id} = ${node}`);
|
||||
return id;
|
||||
}
|
||||
|
||||
private file(path: string): string {
|
||||
let file = this.files.get(path);
|
||||
if (file === undefined) {
|
||||
file = this.add(`!DIFile(filename: ${quoted(basename(path))}, directory: ${quoted(dirname(path))})`);
|
||||
this.files.set(path, file);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
function(fn: IrFunction): string | null {
|
||||
const pos = this.source.position(fn.loc);
|
||||
if (pos === null) return null;
|
||||
const file = this.file(pos.file);
|
||||
return this.add(`distinct !DISubprogram(name: ${quoted(fn.name)}, linkageName: ${quoted(mangleFunction(fn.name))}, scope: ${file}, file: ${file}, line: ${pos.line}, type: ${this.signature}, scopeLine: ${pos.line}, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition, unit: ${this.unit})`);
|
||||
}
|
||||
|
||||
location(loc: SrcLoc, fn: string | null): string | null {
|
||||
if (fn === null) return null;
|
||||
const pos = this.source.position(loc);
|
||||
if (pos === null) return null;
|
||||
// Initializers and compiler-generated entry wrappers can span files.
|
||||
const scopeKey = `${fn}:${pos.file}`;
|
||||
let scope = this.scopes.get(scopeKey);
|
||||
if (scope === undefined) {
|
||||
scope = this.add(`!DILexicalBlockFile(scope: ${fn}, file: ${this.file(pos.file)}, discriminator: 0)`);
|
||||
this.scopes.set(scopeKey, scope);
|
||||
}
|
||||
const key = `${scope}:${pos.line}:${pos.column}`;
|
||||
let id = this.locations.get(key);
|
||||
if (id === undefined) {
|
||||
id = this.add(`!DILocation(line: ${pos.line}, column: ${Math.min(pos.column, 65535)}, scope: ${scope})`);
|
||||
this.locations.set(key, id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
render(): string {
|
||||
return [`!llvm.dbg.cu = !{${this.unit}}`, `!llvm.module.flags = !{${this.version}, ${this.dwarf}}`, ...this.nodes].join("\n");
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,7 @@ import { RUNTIME_ABI_MARKER } from "../runtime-abi.js";
|
||||
import { computeMayThrow } from "../c/may-throw.js";
|
||||
import { mangleArgPack, mangleAsyncSpawn, mangleClassObj, mangleFnClosure, mangleFunction, mangleGenDrop, mangleGenSpawn, mangleGlobal, mangleLocal, mangleRecordStruct, mangleTrampoline, mangleWrapper } from "../mangle.js";
|
||||
import { BlockBuilder } from "./blocks.js";
|
||||
import { LlvmDebugInfo } from "./debug-info.js";
|
||||
import { f64Lit, ffiNativeTypeLl, llvmCommentText } from "./common.js";
|
||||
import { emitLiteralExpr, emitOperatorExpr, emitStringExpr, emitContainerExpr, emitRecordExpr } from "./expr-primitives.js";
|
||||
import { emitControlExpr } from "./expr-control.js";
|
||||
@@ -170,6 +171,8 @@ interface LlScopeEntry {
|
||||
}
|
||||
|
||||
export interface LlvmTargetOptions {
|
||||
/** Exact frontend sources for dev-build line tables and stack frames. */
|
||||
debugSources?: ReadonlyMap<string, string>;
|
||||
/** Pointer width of the target C ABI. Native targets are 64-bit today. */
|
||||
pointerBits?: 32 | 64;
|
||||
/** Select the WASI libc entry-point convention. */
|
||||
@@ -204,6 +207,8 @@ function llStrBytes(text: string): string {
|
||||
}
|
||||
|
||||
class LlEmitter {
|
||||
private readonly debug: LlvmDebugInfo | null;
|
||||
private debugScope: string | null = null;
|
||||
readonly sizeType: "i32" | "i64";
|
||||
readonly cycleColorOffset: number;
|
||||
private readonly wasi: boolean;
|
||||
@@ -377,6 +382,7 @@ class LlEmitter {
|
||||
private logArgSlots = 0;
|
||||
|
||||
constructor(private readonly mod: IrModule, options: LlvmTargetOptions) {
|
||||
this.debug = options.debugSources === undefined ? null : new LlvmDebugInfo(mod.sourceFile, options.debugSources);
|
||||
this.constantNumericTables = findConstantNumericTables(mod);
|
||||
this.sizeType = options.pointerBits === 32 ? "i32" : "i64";
|
||||
this.wasi = options.wasi === true;
|
||||
@@ -1321,6 +1327,7 @@ class LlEmitter {
|
||||
if (this.wasi) out.push(`attributes #1 = { sanitize_address presplitcoroutine }`);
|
||||
if (hasNoInlineRecordClone) out.push(`attributes #2 = { noinline sanitize_address }`);
|
||||
out.push(``);
|
||||
if (this.debug !== null) out.push(this.debug.render());
|
||||
return out.join("\n");
|
||||
}
|
||||
out.push(
|
||||
@@ -1449,6 +1456,7 @@ class LlEmitter {
|
||||
...(hasNoInlineRecordClone ? [`attributes #2 = { noinline sanitize_address }`] : []),
|
||||
``,
|
||||
);
|
||||
if (this.debug !== null) out.push(this.debug.render());
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
@@ -2968,6 +2976,8 @@ class LlEmitter {
|
||||
private emitFunction(fn: IrFunction): string {
|
||||
const B = new BlockBuilder();
|
||||
this.B = B;
|
||||
this.debugScope = this.debug?.function(fn) ?? null;
|
||||
B.debugLocation = this.debug?.location(fn.loc, this.debugScope) ?? null;
|
||||
this.frames = [];
|
||||
this.scopes = [];
|
||||
this.jumpTargets = [];
|
||||
@@ -3122,7 +3132,8 @@ class LlEmitter {
|
||||
if (fn.captures !== undefined) params.unshift("ptr %sc_env");
|
||||
const ret = this.llType(fn.returnType);
|
||||
const attrs = coro !== null ? "#1" : FN_ATTRS;
|
||||
return `define internal ${ret} @${mangleFunction(fn.name)}(${params.join(", ")}) ${attrs} { ; ${fn.name}\n${B.render()}\n}`;
|
||||
const debug = this.debugScope === null ? "" : ` !dbg ${this.debugScope}`;
|
||||
return `define internal ${ret} @${mangleFunction(fn.name)}(${params.join(", ")}) ${attrs}${debug} { ; ${fn.name}\n${B.render()}\n}`;
|
||||
}
|
||||
|
||||
// ── statements ──────────────────────────────────────────────────────────
|
||||
@@ -3153,6 +3164,16 @@ class LlEmitter {
|
||||
}
|
||||
|
||||
private emitStmt(s: IrStmt): void {
|
||||
const previous = this.B.debugLocation;
|
||||
this.B.debugLocation = this.debug?.location(s.loc, this.debugScope) ?? null;
|
||||
try {
|
||||
this.emitStmtBody(s);
|
||||
} finally {
|
||||
this.B.debugLocation = previous;
|
||||
}
|
||||
}
|
||||
|
||||
private emitStmtBody(s: IrStmt): void {
|
||||
const B = this.B;
|
||||
this.frames.push([]);
|
||||
switch (s.kind) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import { splitLlvmLibraryProgram, splitLlvmProgram } from "./split.js";
|
||||
import { LlvmDebugInfo } from "./debug-info.js";
|
||||
import { VOID } from "../../ir/ir.js";
|
||||
|
||||
const scratch: string[] = [];
|
||||
afterAll(async () => {
|
||||
@@ -77,6 +79,32 @@ test("thread-local program state conservatively keeps the single-TU path", () =>
|
||||
expect(splitLlvmProgram(tls, { minimumBytes: 0, targetBytes: 64 * 1024 })).toBeNull();
|
||||
});
|
||||
|
||||
test("debug metadata stays on definitions when splitting LLVM modules", async () => {
|
||||
const debug = new LlvmDebugInfo("/source/main.ts", new Map([["/source/main.ts", "console.log(1);\n"]]));
|
||||
let source = SAMPLE;
|
||||
for (const name of ["left", "right", "public_entry"]) {
|
||||
const loc = { file: "/source/main.ts", start: 0, end: 1 };
|
||||
const scope = debug.function({ name, loc, params: [], locals: [], body: [], returnType: VOID });
|
||||
const location = debug.location(loc, scope);
|
||||
source = source.replace(new RegExp(`(@${name}\\(\\) #0) \\{([\\s\\S]*?)\\n\\}`), (_, header: string, body: string) =>
|
||||
`${header} !dbg ${scope} {${body.split("\n").map((line) => line.startsWith(" ") ? `${line}, !dbg ${location}` : line).join("\n")}\n}`,
|
||||
);
|
||||
}
|
||||
source += debug.render();
|
||||
const split = splitLlvmProgram(source, { minimumBytes: 0 });
|
||||
expect(split).not.toBeNull();
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-debug-split-"));
|
||||
scratch.push(dir);
|
||||
for (const shard of split!.shards) {
|
||||
expect(shard.source).not.toMatch(/^declare.*!dbg/m);
|
||||
const path = join(dir, shard.name);
|
||||
await writeFile(path, shard.source);
|
||||
const result = spawnSync("clang", ["-Wno-override-module", "-c", path, "-o", `${path}.o`], { encoding: "utf8" });
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.stderr).toBe("");
|
||||
}
|
||||
});
|
||||
|
||||
test("dev libraries split at the measured 2MB crossover while executables retain 4MB", () => {
|
||||
const body = Array.from({ length: 700 }, (_, i) =>
|
||||
`define internal i64 @library_pad_${i}() #0 {\nentry:\n ; ${"x".repeat(3072)}\n ret i64 ${i}\n}\n`,
|
||||
|
||||
@@ -87,7 +87,10 @@ function functionDefAt(lines: readonly string[], start: number): { def: Function
|
||||
const source = [definitionHeader, ...lines.slice(start + 1, end + 1)].join("\n");
|
||||
// Parameter names are legal on declarations. Preserve the emitted text so
|
||||
// attributes, zeroext, and varargs stay byte-exact.
|
||||
const declaration = `declare ${linkage}${match[2]} ${match[3]}(${match[4]})${match[5]}`.trimEnd();
|
||||
// A definition's DISubprogram cannot be attached to a declaration in a
|
||||
// different shard. Its owning definition retains the original metadata.
|
||||
const attrs = match[5]!.replace(/\s+!dbg !\d+/g, "");
|
||||
const declaration = `declare ${linkage}${match[2]} ${match[3]}(${match[4]})${attrs}`.trimEnd();
|
||||
return { def: { source, declaration, symbol, promoted }, end };
|
||||
}
|
||||
|
||||
|
||||
@@ -3845,6 +3845,16 @@ entry:
|
||||
// The completed executable hit returns before shard lookup/merge.
|
||||
expect((await stat(join(cacheRoot, "program-shard", name))).mtimeMs).toBe(old.getTime());
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
// A missing bundle is restored with its executable without revisiting
|
||||
// any program shards, including on case-insensitive macOS volumes.
|
||||
await rm(`${outPath}.dSYM`, { recursive: true });
|
||||
await build(9);
|
||||
expect((await stat(join(`${outPath}.dSYM`, "Contents/Resources/DWARF/program"))).size).toBeGreaterThan(0);
|
||||
for (const name of after) {
|
||||
expect((await stat(join(cacheRoot, "program-shard", name))).mtimeMs).toBe(old.getTime());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (oldCacheDir === undefined) delete process.env["SCRIPTC_CACHE_DIR"];
|
||||
else process.env["SCRIPTC_CACHE_DIR"] = oldCacheDir;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { localizeElfObject, mergeAndLocalizeCoffObjects } from "./object-localize.js";
|
||||
import { executableOptimizationLinkerArgs, executableStripLinkerArgs, windowsSubsystemLinkerArgs, type WindowsSubsystem } from "./targets.js";
|
||||
import { createDarwinDebugSymbols, installDarwinDebugSymbols, needsDarwinDebugSymbols, readDarwinDebugSymbols } from "./debug-symbols.js";
|
||||
import {
|
||||
createVendorArchives,
|
||||
MBEDTLS_VERSION,
|
||||
@@ -3407,6 +3408,7 @@ interface LocalArtifactStamp {
|
||||
version: 2;
|
||||
key: string;
|
||||
digest: string;
|
||||
debugSymbolsDigest?: string;
|
||||
dependencies: NativeArtifactDependency[];
|
||||
integrity: string;
|
||||
}
|
||||
@@ -3756,7 +3758,7 @@ function localArtifactStampPath(root: string, outPath: string): string {
|
||||
}
|
||||
|
||||
function localArtifactStampIntegrity(
|
||||
stamp: Pick<LocalArtifactStamp, "version" | "key" | "digest" | "dependencies">,
|
||||
stamp: Pick<LocalArtifactStamp, "version" | "key" | "digest" | "dependencies" | "debugSymbolsDigest">,
|
||||
): string {
|
||||
return createHash("sha256")
|
||||
.update("local-artifact-stamp-v2\0")
|
||||
@@ -3768,6 +3770,7 @@ async function localArtifactHit(
|
||||
stampPath: string,
|
||||
outPath: string,
|
||||
key: string,
|
||||
darwinDebugSymbols = false,
|
||||
): Promise<LocalArtifactStamp | null> {
|
||||
try {
|
||||
const stamp = JSON.parse(await readFile(stampPath, "utf8")) as Partial<LocalArtifactStamp>;
|
||||
@@ -3777,6 +3780,8 @@ async function localArtifactHit(
|
||||
stamp.version !== 2 ||
|
||||
stamp.key !== key ||
|
||||
!/^[0-9a-f]{64}$/.test(stamp.digest ?? "") ||
|
||||
(darwinDebugSymbols !== (stamp.debugSymbolsDigest !== undefined)) ||
|
||||
(stamp.debugSymbolsDigest !== undefined && !/^[0-9a-f]{64}$/.test(stamp.debugSymbolsDigest)) ||
|
||||
!Array.isArray(stamp.dependencies) ||
|
||||
!/^[0-9a-f]{64}$/.test(stamp.integrity ?? "") ||
|
||||
localArtifactStampIntegrity({
|
||||
@@ -3784,6 +3789,7 @@ async function localArtifactHit(
|
||||
key: stamp.key,
|
||||
digest: stamp.digest!,
|
||||
dependencies: stamp.dependencies,
|
||||
...(stamp.debugSymbolsDigest === undefined ? {} : { debugSymbolsDigest: stamp.debugSymbolsDigest }),
|
||||
}) !== stamp.integrity ||
|
||||
!output.isFile() ||
|
||||
(output.mode & 0o777) !== expectedMode ||
|
||||
@@ -3811,7 +3817,8 @@ async function localArtifactHit(
|
||||
)
|
||||
) ||
|
||||
!(await nativeArtifactDependenciesStillMatch(stamp.dependencies)) ||
|
||||
await fileDigest(outPath) !== stamp.digest
|
||||
await fileDigest(outPath) !== stamp.digest ||
|
||||
darwinDebugSymbols && createHash("sha256").update(await readDarwinDebugSymbols(outPath)).digest("hex") !== stamp.debugSymbolsDigest
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -3830,6 +3837,7 @@ async function publishLocalArtifactStamp(
|
||||
dependencyPaths: readonly string[],
|
||||
recursiveDirectories: readonly string[] = [],
|
||||
recursiveExclusions: readonly string[] = [],
|
||||
darwinDebugSymbols = false,
|
||||
): Promise<LocalArtifactStamp> {
|
||||
await mkdir(dirname(stampPath), { recursive: true });
|
||||
const tmp = `${stampPath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
||||
@@ -3844,6 +3852,9 @@ async function publishLocalArtifactStamp(
|
||||
key,
|
||||
digest: await fileDigest(outPath),
|
||||
dependencies,
|
||||
...(darwinDebugSymbols
|
||||
? { debugSymbolsDigest: createHash("sha256").update(await readDarwinDebugSymbols(outPath)).digest("hex") }
|
||||
: {}),
|
||||
} as const;
|
||||
const stamp: LocalArtifactStamp = {
|
||||
...unsigned,
|
||||
@@ -4023,6 +4034,10 @@ async function compileCInternal(
|
||||
const tls = (opts.tls ?? false) || nativeFetch || netIsland;
|
||||
const tlsCa = (opts.tlsCa ?? false) || tls;
|
||||
const driver = resolveCc();
|
||||
const darwinDebugSymbols = needsDarwinDebugSymbols(targetPlatform(driver), optimization, opts.strip);
|
||||
const debugFlags = optimization === "dev" && !opts.strip
|
||||
? ["-gline-tables-only", ...(opts.cPath.endsWith(".ll") ? [] : ["-gno-column-info"])]
|
||||
: [];
|
||||
const shardNames = new Set<string>();
|
||||
const programShardsValid = opts.programShards?.every((shard) => {
|
||||
if (
|
||||
@@ -4192,7 +4207,7 @@ async function compileCInternal(
|
||||
programBytes,
|
||||
compilerPath: effectiveCompiler.canonicalPath,
|
||||
};
|
||||
const hit = await localArtifactHit(stampPath, opts.outPath, key);
|
||||
const hit = await localArtifactHit(stampPath, opts.outPath, key, darwinDebugSymbols);
|
||||
if (hit !== null) {
|
||||
await opts.onArtifactReady?.({ dependencies: hit.dependencies }).catch(() => undefined);
|
||||
return;
|
||||
@@ -4424,10 +4439,11 @@ async function compileCInternal(
|
||||
} = {},
|
||||
): string[] => [
|
||||
"-std=c11",
|
||||
...debugFlags,
|
||||
...driver.targetArgs,
|
||||
...threadArgs,
|
||||
...(sanitize
|
||||
? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
|
||||
? [optimization === "dev" ? "-O0" : "-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
|
||||
: [optimization === "dev" ? "-O0" : "-O2"]),
|
||||
...executableSectionFlags.compile,
|
||||
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
|
||||
@@ -4617,10 +4633,11 @@ async function compileCInternal(
|
||||
// historical single clang invocation.
|
||||
const cflags = [
|
||||
"-std=c11",
|
||||
...debugFlags,
|
||||
...driver.targetArgs,
|
||||
...threadArgs,
|
||||
...(sanitize
|
||||
? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
|
||||
? [optimization === "dev" ? "-O0" : "-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
|
||||
: [optimization === "dev" ? "-O0" : "-O2"]),
|
||||
...executableSectionFlags.compile,
|
||||
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
|
||||
@@ -5026,6 +5043,20 @@ async function compileCInternal(
|
||||
if (!(await copyValidCachedFile(cachedBin, tmpOut))) {
|
||||
throw new Error("invalid cached executable");
|
||||
}
|
||||
if (darwinDebugSymbols) {
|
||||
const cachedSymbols = `${cachedBin}.dsym`;
|
||||
const symbolsCopy = privateSiblingPath(opts.outPath, "dsym-hit");
|
||||
try {
|
||||
if (!(await copyValidCachedFile(cachedSymbols, symbolsCopy))) throw new Error("invalid cached dSYM");
|
||||
const bytes = await readFile(symbolsCopy);
|
||||
// A concurrent publisher may replace either payload. The sidecar
|
||||
// also names its binary digest so a mixed pair always misses.
|
||||
if (bytes.subarray(0, 32).toString("hex") !== await fileDigest(tmpOut)) throw new Error("mismatched cached dSYM");
|
||||
await installDarwinDebugSymbols(bytes.subarray(32), opts.outPath);
|
||||
} finally {
|
||||
await rm(symbolsCopy, { force: true });
|
||||
}
|
||||
}
|
||||
// Match a fresh linker output under the caller's current umask. Reusing a
|
||||
// cache entry populated by a less restrictive shell must not widen access.
|
||||
await chmod(tmpOut, 0o777 & ~process.umask());
|
||||
@@ -5038,6 +5069,7 @@ async function compileCInternal(
|
||||
localArtifactDependencyPaths,
|
||||
[dirname(resolve(opts.cPath))],
|
||||
[persistentCache.root],
|
||||
darwinDebugSymbols,
|
||||
).catch(() => null);
|
||||
if (stamp !== null) {
|
||||
await opts.onArtifactReady?.({ dependencies: stamp.dependencies }).catch(() => undefined);
|
||||
@@ -5258,6 +5290,13 @@ async function compileCInternal(
|
||||
: { programPath: shardedProgramObject, outPath: privateOut },
|
||||
),
|
||||
);
|
||||
if (darwinDebugSymbols) {
|
||||
// clang invokes dsymutil when compiling a source TU while linking.
|
||||
// A sharded program is already an object, so run it explicitly while
|
||||
// the merged object and staged runtime inputs still exist.
|
||||
if (shardedProgramObject !== null) await createDarwinDebugSymbols(privateOut);
|
||||
await installDarwinDebugSymbols(await readDarwinDebugSymbols(privateOut), opts.outPath);
|
||||
}
|
||||
await installArtifact(privateOut, opts.outPath);
|
||||
|
||||
if (cachedBin !== null && keyHex !== null) {
|
||||
@@ -5317,6 +5356,14 @@ async function compileCInternal(
|
||||
// generated code and embedded literals private; the hit path reapplies
|
||||
// the caller's current executable mode to its destination copy.
|
||||
await publishCachedFile(privateOut, cachedBin);
|
||||
if (darwinDebugSymbols) {
|
||||
const symbols = privateSiblingPath(privateOut, "debug-symbols");
|
||||
await writeFile(symbols, Buffer.concat([
|
||||
Buffer.from(await fileDigest(privateOut), "hex"),
|
||||
await readDarwinDebugSymbols(privateOut),
|
||||
]));
|
||||
await publishCachedFile(symbols, `${cachedBin}.dsym`);
|
||||
}
|
||||
} catch {
|
||||
/* publishing is best-effort */
|
||||
}
|
||||
@@ -5334,6 +5381,7 @@ async function compileCInternal(
|
||||
localArtifactDependencyPaths,
|
||||
[dirname(resolve(opts.cPath))],
|
||||
[persistentCache.root],
|
||||
darwinDebugSymbols,
|
||||
).catch(() => null);
|
||||
if (stamp !== null) {
|
||||
await opts.onArtifactReady?.({ dependencies: stamp.dependencies }).catch(() => undefined);
|
||||
@@ -5350,6 +5398,9 @@ async function compileCInternal(
|
||||
export async function compileC(opts: CcOptions): Promise<void> {
|
||||
clearCcCaches();
|
||||
await compileCInternal(opts, false);
|
||||
if (targetPlatform(resolveCc()) === "darwin" && !needsDarwinDebugSymbols("darwin", opts.optimization, opts.strip)) {
|
||||
await rm(`${opts.outPath}.dSYM`, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
export type NativeCacheWarmProfile = "runtime" | "tls" | "dynamic";
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { SourceLocations } from "./source-locations.js";
|
||||
|
||||
test("source offsets use their own file, UTF-16 columns, and JS line separators", () => {
|
||||
const text = "one\r\ntwo\rthree\nfour\u2028five\u2029😀six";
|
||||
const sources = new SourceLocations(new Map([["main.ts", "short"], ["other.ts", text]]));
|
||||
for (const [index, word] of ["one", "two", "three", "four", "five", "😀six"].entries()) {
|
||||
const offset = text.indexOf(word);
|
||||
expect(sources.position({ file: "other.ts", start: offset, end: offset + word.length })).toEqual({
|
||||
file: "other.ts", line: index + 1, column: 1,
|
||||
});
|
||||
}
|
||||
expect(sources.position({ file: "other.ts", start: text.indexOf("six"), end: text.length })?.column).toBe(3);
|
||||
expect(sources.position({ file: "missing.ts", start: 0, end: 0 })).toBeNull();
|
||||
expect(sources.position({ file: "main.ts", start: -1, end: -1 })).toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { SrcLoc } from "../ir/ir.js";
|
||||
|
||||
export interface SourcePosition {
|
||||
file: string;
|
||||
line: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
/** Resolve IR's UTF-16 offsets against the exact sources used by the frontend. */
|
||||
export class SourceLocations {
|
||||
private readonly starts = new Map<string, number[]>();
|
||||
|
||||
constructor(sources: ReadonlyMap<string, string>) {
|
||||
for (const [file, text] of sources) {
|
||||
const starts = [0];
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text.charCodeAt(i);
|
||||
if (ch === 13 && text.charCodeAt(i + 1) === 10) i++;
|
||||
if (ch === 10 || ch === 13 || ch === 0x2028 || ch === 0x2029) starts.push(i + 1);
|
||||
}
|
||||
this.starts.set(file, starts);
|
||||
}
|
||||
}
|
||||
|
||||
position(loc: SrcLoc): SourcePosition | null {
|
||||
const starts = this.starts.get(loc.file);
|
||||
if (starts === undefined || loc.start < 0) return null;
|
||||
let lo = 0, hi = starts.length - 1;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi + 1) >>> 1;
|
||||
if (starts[mid]! <= loc.start) lo = mid;
|
||||
else hi = mid - 1;
|
||||
}
|
||||
return { file: loc.file, line: lo + 1, column: loc.start - starts[lo]! + 1 };
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { FrontendInputTracker, trackedFileExists, trackedReadFile } from "../frontend/input-tracker.js";
|
||||
import { nativeArtifactDependenciesStillMatch } from "../backend/native-toolchain.js";
|
||||
import { installDarwinDebugSymbols, readDarwinDebugSymbols } from "../backend/debug-symbols.js";
|
||||
import {
|
||||
publishEarlyExecutableCache,
|
||||
readEarlyExecutableCache,
|
||||
@@ -122,6 +123,29 @@ test("early executable cache restores the emitted TU, IR, and native feature gat
|
||||
expect(await readFile(alternateC, "utf8")).toBe("/* saved C backend */\n");
|
||||
});
|
||||
|
||||
test("early dev cache restores the matching dSYM and rejects a missing payload", async () => {
|
||||
const f = await fixture();
|
||||
f.options.optimization = "dev";
|
||||
f.options.target = "native:darwin:arm64:runtime-pack";
|
||||
const tracker = new FrontendInputTracker();
|
||||
tracker.run(() => trackedReadFile(f.source));
|
||||
await writeFile(f.options.outPath, "native binary");
|
||||
const symbols = Buffer.concat([Buffer.from("SCDSYM01"), Buffer.from([5, 0, 0, 0]), Buffer.from("plistDWARF")]);
|
||||
await installDarwinDebugSymbols(symbols, f.options.outPath);
|
||||
await publishEarlyExecutableCache(f.root, f.options, {
|
||||
cPath: f.cPath, irPath: f.irPath, native: { ...native, optimization: "dev" },
|
||||
executableRestored: true, nativeDependencies: [], frontend: tracker.snapshot(),
|
||||
});
|
||||
await rm(`${f.options.outPath}.dSYM`, { recursive: true });
|
||||
expect((await readEarlyExecutableCache(f.root, f.options))?.executableRestored).toBe(true);
|
||||
expect(await readDarwinDebugSymbols(f.options.outPath)).toEqual(symbols);
|
||||
const files = await readdir(f.root, { recursive: true });
|
||||
const cachedSymbols = files.find((file) => file.endsWith("program.dsym"));
|
||||
expect(cachedSymbols).toBeDefined();
|
||||
await rm(join(f.root, cachedSymbols!));
|
||||
expect(await readEarlyExecutableCache(f.root, f.options)).toBeNull();
|
||||
});
|
||||
|
||||
test("early executable cache misses on source and resolution changes", async () => {
|
||||
const f = await fixture();
|
||||
const tracker = new FrontendInputTracker();
|
||||
|
||||
@@ -6,6 +6,7 @@ import { frontendInputsStillMatch, validFrontendInputSnapshot } from "../fronten
|
||||
import { compilerReleaseVersion } from "../library/sidecar.js";
|
||||
import { nativeArtifactDependenciesStillMatch, type NativeArtifactDependency } from "../backend/native-toolchain.js";
|
||||
import type { CompilerImplementationDependency } from "../library/compiler-self-identity.js";
|
||||
import { installDarwinDebugSymbols, needsDarwinDebugSymbols, readDarwinDebugSymbols } from "../backend/debug-symbols.js";
|
||||
import { compilerImplementationDependenciesStillMatch, compilerImplementationRoot } from "../library/compiler-self-identity.js";
|
||||
import {
|
||||
cacheKey as sharedCacheKey,
|
||||
@@ -69,6 +70,7 @@ interface EarlyExecutableCacheStamp {
|
||||
translationUnit: CachedExecutableFile;
|
||||
ir: CachedExecutableFile | null;
|
||||
executable: CachedExecutableFile | null;
|
||||
debugSymbols?: CachedExecutableFile;
|
||||
};
|
||||
nativeDependencies: NativeArtifactDependency[] | null;
|
||||
native: EarlyExecutableNativeFeatures;
|
||||
@@ -291,7 +293,7 @@ function executableFrontendOutputExclusions(
|
||||
options: EarlyExecutableCacheOptions,
|
||||
backend: "c" | "llvm",
|
||||
): ReturnType<typeof frontendOutputExclusions> {
|
||||
return frontendOutputExclusions(options, backend, "", [options.outPath]);
|
||||
return frontendOutputExclusions(options, backend, "", [options.outPath, `${options.outPath}.dSYM`]);
|
||||
}
|
||||
|
||||
async function fileMatches(
|
||||
@@ -347,6 +349,13 @@ export async function readEarlyExecutableCache(
|
||||
stamp.files.executable?.name !== "program.bin" ||
|
||||
!/^[0-9a-f]{64}$/.test(stamp.files.executable.digest)
|
||||
)) ||
|
||||
(stamp.files.debugSymbols !== undefined && (
|
||||
stamp.files.executable === null || stamp.files.debugSymbols.name !== "program.dsym" ||
|
||||
!/^[0-9a-f]{64}$/.test(stamp.files.debugSymbols.digest)
|
||||
)) ||
|
||||
(stamp.files.executable !== null &&
|
||||
needsDarwinDebugSymbols(options.target.split(":")[1] ?? "", options.optimization, options.strip) !==
|
||||
(stamp.files.debugSymbols !== undefined)) ||
|
||||
(stamp.files.executable === null) !== (stamp.nativeDependencies === null) ||
|
||||
(stamp.nativeDependencies !== null && !Array.isArray(stamp.nativeDependencies)) ||
|
||||
(stamp.files.ir !== null) !== options.emitIr ||
|
||||
@@ -358,7 +367,7 @@ export async function readEarlyExecutableCache(
|
||||
) return null;
|
||||
|
||||
const directory = dirname(path);
|
||||
const [translationUnit, ir, executable] = await Promise.all([
|
||||
const [translationUnit, ir, executable, debugSymbols] = await Promise.all([
|
||||
readCachedFile(
|
||||
join(directory, stamp.files.translationUnit.name),
|
||||
stamp.files.translationUnit.digest,
|
||||
@@ -372,11 +381,15 @@ export async function readEarlyExecutableCache(
|
||||
join(directory, stamp.files.executable.name),
|
||||
stamp.files.executable.digest,
|
||||
),
|
||||
stamp.files.debugSymbols === undefined
|
||||
? Promise.resolve(null)
|
||||
: readCachedFile(join(directory, stamp.files.debugSymbols.name), stamp.files.debugSymbols.digest),
|
||||
]);
|
||||
if (
|
||||
translationUnit === null ||
|
||||
stamp.files.ir !== null && ir === null ||
|
||||
stamp.files.executable !== null && executable === null
|
||||
stamp.files.executable !== null && executable === null ||
|
||||
stamp.files.debugSymbols !== undefined && debugSymbols === null
|
||||
) return null;
|
||||
|
||||
// Validate the native proof before restoring frontend artifacts: replacing
|
||||
@@ -398,10 +411,14 @@ export async function readEarlyExecutableCache(
|
||||
) await installBytes(ir, paths.irPath);
|
||||
if (executableRestored) {
|
||||
try {
|
||||
if (debugSymbols !== null) await installDarwinDebugSymbols(debugSymbols, options.outPath);
|
||||
const expectedMode = 0o777 & ~process.umask();
|
||||
if (!(await fileMatches(options.outPath, stamp.files.executable!.digest, expectedMode))) {
|
||||
await installExecutable(executable!, options.outPath);
|
||||
}
|
||||
if (debugSymbols === null && options.target.split(":")[1] === "darwin") {
|
||||
await rm(`${options.outPath}.dSYM`, { recursive: true, force: true });
|
||||
}
|
||||
} catch {
|
||||
executableRestored = false;
|
||||
}
|
||||
@@ -412,6 +429,7 @@ export async function readEarlyExecutableCache(
|
||||
join(directory, stamp.files.translationUnit.name),
|
||||
...(stamp.files.ir === null ? [] : [join(directory, stamp.files.ir.name)]),
|
||||
...(stamp.files.executable === null ? [] : [join(directory, stamp.files.executable.name)]),
|
||||
...(stamp.files.debugSymbols === undefined ? [] : [join(directory, stamp.files.debugSymbols.name)]),
|
||||
].map((cachePath) => utimes(cachePath, now, now).catch(() => undefined)));
|
||||
return {
|
||||
cPath: paths.cPath,
|
||||
@@ -553,6 +571,12 @@ export async function publishEarlyExecutableCache(
|
||||
: publishFile(result.irPath, "program.ir.json"),
|
||||
publishExecutable,
|
||||
]);
|
||||
let debugSymbols: CachedExecutableFile | undefined;
|
||||
if (executable !== null && needsDarwinDebugSymbols(options.target.split(":")[1] ?? "", options.optimization, options.strip)) {
|
||||
const bytes = await readDarwinDebugSymbols(options.outPath);
|
||||
await writeFile(join(stage, "program.dsym"), bytes, { mode: 0o600 });
|
||||
debugSymbols = { name: "program.dsym", digest: digest(bytes) };
|
||||
}
|
||||
if (!frontendInputsStillMatch(
|
||||
result.frontend,
|
||||
executableFrontendOutputExclusions(options, result.native.backend),
|
||||
@@ -561,7 +585,7 @@ export async function publishEarlyExecutableCache(
|
||||
version: 1,
|
||||
key: cacheKey(options),
|
||||
frontend: result.frontend,
|
||||
files: { translationUnit, ir, executable },
|
||||
files: { translationUnit, ir, executable, ...(debugSymbols === undefined ? {} : { debugSymbols }) },
|
||||
nativeDependencies: executable === null ? null : result.nativeDependencies!,
|
||||
native: result.native,
|
||||
};
|
||||
@@ -579,6 +603,7 @@ export async function publishEarlyExecutableCache(
|
||||
await install(translationUnit.name);
|
||||
if (ir !== null) await install(ir.name);
|
||||
if (executable !== null) await install(executable.name);
|
||||
if (debugSymbols !== undefined) await install(debugSymbols.name);
|
||||
await install("stamp.json");
|
||||
await publishEarlyExecutableRoute(root, options);
|
||||
} finally {
|
||||
|
||||
@@ -221,7 +221,8 @@ export interface CompileBaseOptions {
|
||||
* C fallback; a missing LLVM lowering there is SC3001. */
|
||||
backend?: "c" | "llvm";
|
||||
/** Native optimization posture. Release is the shipped -O2 default; dev
|
||||
* uses -O0 and stable multi-TU object caching for large LLVM programs. */
|
||||
* uses -O0, source line tables, and stable multi-TU object caching for
|
||||
* large LLVM programs. Darwin executables include an adjacent .dSYM. */
|
||||
optimization?: "release" | "dev";
|
||||
/** Remove symbol/debug payload from an executable at link time. */
|
||||
strip?: boolean;
|
||||
@@ -1624,6 +1625,9 @@ async function compileTracked(
|
||||
c: join(opts.outDir, `${stem}.c`),
|
||||
llvm: join(opts.outDir, `${stem}.ll`),
|
||||
} as const;
|
||||
const debugOptions = opts.optimization === "dev" && !opts.strip
|
||||
? { debugSources: sourceTexts }
|
||||
: {};
|
||||
|
||||
if (outputKind === "ir") {
|
||||
await mkdir(dirname(opts.outPath), { recursive: true });
|
||||
@@ -1633,7 +1637,7 @@ async function compileTracked(
|
||||
|
||||
if (outputKind === "c") {
|
||||
await mkdir(dirname(opts.outPath), { recursive: true });
|
||||
await writeFile(opts.outPath, emitCModule(lowered.module, entryText));
|
||||
await writeFile(opts.outPath, emitCModule(lowered.module, entryText, debugOptions));
|
||||
return { ok: true, artifact: { kind: "c", path: opts.outPath } };
|
||||
}
|
||||
|
||||
@@ -1641,6 +1645,7 @@ async function compileTracked(
|
||||
let llvm: string;
|
||||
try {
|
||||
llvm = emitLlvmModule(lowered.module, {
|
||||
...debugOptions,
|
||||
pointerBits: buildPlatform === "wasi" ? 32 : 64,
|
||||
wasi: buildPlatform === "wasi",
|
||||
runtimeAbiMarker: outputKind === "obj",
|
||||
@@ -1712,6 +1717,7 @@ async function compileTracked(
|
||||
if (opts.backend !== "c") {
|
||||
try {
|
||||
const ll = emitLlvmModule(lowered.module!, {
|
||||
...debugOptions,
|
||||
pointerBits: buildPlatform === "wasi" ? 32 : 64,
|
||||
wasi: buildPlatform === "wasi",
|
||||
runtimeAbiMarker:
|
||||
@@ -1733,7 +1739,7 @@ async function compileTracked(
|
||||
}
|
||||
}
|
||||
if (backend === "c") {
|
||||
await writeFile(cPath, emitCModule(lowered.module!, entryText));
|
||||
await writeFile(cPath, emitCModule(lowered.module!, entryText, debugOptions));
|
||||
}
|
||||
let irPath: string | undefined;
|
||||
if (opts.emitIr) {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
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");
|
||||
} 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 });
|
||||
}
|
||||
});
|
||||
@@ -7230,6 +7230,13 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/3066-source-debugging/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/3066-source-debugging/helper.ts",
|
||||
"<repo>/tests/corpus/3066-source-debugging/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/400-fib.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/400-fib.ts"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export function calculate(count: number): number {
|
||||
let value = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
value += i;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { calculate } from "./helper.ts";
|
||||
|
||||
function report(): void {
|
||||
const value = calculate(3);
|
||||
console.log("result", value);
|
||||
}
|
||||
|
||||
report();
|
||||
Reference in New Issue
Block a user