14.5 Debug Information and Source Maps
Machine code executes addresses, registers, and memory; developers reason about files, lines, functions, variables, and types. Debug information preserves a mapping between these worlds. Native toolchains commonly use DWARF with ELF or Mach-O and CodeView/PDB with PE/COFF. JavaScript and other generated-text ecosystems commonly use source maps. Although their formats differ, both record provenance that ordinary instructions no longer contain.
Debug metadata is not one reverse lookup table. A debugger must answer several independent questions: Which source location corresponds to this program counter? Which function and lexical scope contain it? Was the function inlined? Where is variable x now? How can the caller's frame be reconstructed? What type and layout should be used to display a value?
Address ranges, line programs, and variable locations
A native compiler emits a line table that maps address ranges to source file, line, and often column. DWARF compresses this mapping as a small state-machine program rather than repeating a full record per instruction. Several machine instructions may correspond to one line, one instruction may represent code combined from several expressions, and optimized instructions may move away from their original source order.
Variables need location descriptions, not only names. Before register allocation, total may be an SSA value; later it occupies rax, then a stack slot, then no recoverable location at all because it was folded or eliminated. A location list associates different PC ranges with expressions such as “register 3,” “CFA minus 16,” or “constant 7.” Saying <optimized out> is more honest than displaying stale data.
Inlining creates another dimension: a PC physically inside sum may logically represent a call to add declared in a different file. Debug metadata records an abstract function and one or more inlined call-site instances, allowing a debugger to show a virtual stack. Unwind information such as .eh_frame describes how to recover the caller's canonical frame address, saved registers, and return address even when a frame pointer is omitted.
Optimization changes what can be observed
At -O0, compilers often preserve straightforward statement order and stable stack homes, producing intuitive stepping but slower code. At higher optimization, common-subexpression elimination, instruction scheduling, tail duplication, loop transforms, and inlining break the illusion that execution visits each source statement once in textual order. A variable can have different values along merged paths or exist only as pieces in several locations.
Good optimized-debug support is therefore designed through the pipeline. IR operations carry source locations; transformations choose whether to preserve, merge, or drop them according to documented rules; value-tracking follows important variables through copies and substitutions; the backend emits range-accurate locations after register allocation. Incorrect metadata is worse than missing metadata because it confidently misleads a diagnosis. Compilers test this with debugger scripts: set breakpoints, step, inspect variables, print inline stacks, unwind through exceptions, and compare expected source positions.
Source maps for transformed text
A source map maps positions in generated text back to original sources. A typical version 3 map contains sources, optional embedded sourcesContent, a names table, and compact mappings. Mappings are organized by generated line and encode deltas using Base64 VLQ. A segment can connect generated column to source index, original line and column, and optional name index.
Suppose TypeScript is transpiled to JavaScript, then bundled, then minified. Each stage can emit a map from its output to its input; the build pipeline must compose those maps so a production stack at bundle.js:1:8392 reaches parser.ts:74:11, not merely the intermediate file. Column mappings matter after minification because an entire bundle may occupy one line. Name tables help restore parseExpression after it becomes a, though they increase payload.
Maps can be inline, published as adjacent .map files, or kept hidden from users and uploaded to an error-reporting service. The last option preserves production symbolication without advertising source content, but access control and retention still matter: maps may include original paths, names, or full source. Native deployments make a similar split by storing stripped binaries in production and indexed debug companions on a protected symbol server. In both cases, use a build ID or content hash to pair code with exactly matching symbols. A near match is dangerous: every address can resolve to a plausible but wrong line.
The end-to-end rule is simple: treat provenance as a compiler output with a schema, compatibility requirements, privacy policy, and tests. Verify deterministic IDs, path remapping, split-debug packaging, map composition, optimized stepping, crash symbolication, and behavior when metadata is intentionally absent.