From 1e926045999a38e1c1c5e56421085b8d993e256c Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Mon, 22 Jun 2026 19:43:35 +0100 Subject: [PATCH] feat(debug): per-unit OPDF so incremental builds are fully debuggable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Incremental compilation is the default, but each dependency unit was compiled to its own .o with NO OPDF debug info — only the top --source unit got an .opdf section. pdr could therefore not set breakpoints, show callstacks, or inspect locals inside any dependency (RTL, stdlib, or a user's own units), which makes the default pipeline effectively undebuggable. Mirror FPC's multi-unit OPDF model (which pdr already reads): every object — each unit AND the program — carries a complete, self-contained .opdf section [32-byte header][records], and the linker concatenates the same-named sections. The header's TotalRecords is written as 0 (stream-terminated): readers consume records to section EOF and skip the embedded per-unit headers (and linker zero padding). pdr needs NO changes — it already handles exactly this layout. Layers: - uDebugOPDF.pas: EmitHeader writes TotalRecords=0 (PatchTotalRecords now a no-op); new CreateForUnit + unit-mode DoEmit that emits a unit's types / globals / constants / function scopes / line info from its IntfBlock+ImplBlock and the unit's symbol table, with NO program main scope and NO unit directory (directory is program-only; pdr ignores it). Whole-program output is unchanged apart from TotalRecords=0. MangledClassSym matches the native backend's class-symbol naming so vtable labels resolve in unit mode. - Blaise.pas: TCompileWorker.Execute appends a unit-mode OPDF section to each unit's IR when --debug-opdf and the native codegen produced debug facts, so the per-unit .o embeds its own .opdf. (QBE has no facts → no per-unit OPDF; native is the debug backend.) - blaise.assembler.x86_64.pas: accept `.section .opdf, "aw", @progbits` (new eskOpdf); `.int` alias for `.long`; `.byte`/`.word` now parse comma-separated value lists (needed for the OPDF magic + BuildID bytes). - blaise.elfwriter.pas: emit the .opdf section as SHF_ALLOC|SHF_WRITE PROGBITS so it lands in the loadable image where pdr finds it; the internal linker's section merger already concatenates .opdf by name and resolves its .quad relocations. Verified: pdr breaks inside a dependency unit, shows the cross-unit callstack, and inspects locals — under BOTH the internal toolchain (default) and external gas/ld, incremental AND --no-incremental. Full suite OK (3742) on both build paths; all four fixpoints pass (normal builds unaffected). Regression test TestDebugOpdf_PerUnitSection_InDependencyObject asserts the dependency .o carries an .opdf section. --- compiler/src/main/pascal/Blaise.pas | 32 +++- .../main/pascal/blaise.assembler.x86_64.pas | 36 +++- compiler/src/main/pascal/blaise.elfwriter.pas | 65 ++++--- compiler/src/main/pascal/uDebugOPDF.pas | 167 ++++++++++++++---- .../test/pascal/cp.test.e2e.sepcompile.pas | 99 +++++++++++ compiler/src/test/pascal/cp.test.opdf.pas | 11 +- 6 files changed, 343 insertions(+), 67 deletions(-) diff --git a/compiler/src/main/pascal/Blaise.pas b/compiler/src/main/pascal/Blaise.pas index 1c92c78..c27aea5 100644 --- a/compiler/src/main/pascal/Blaise.pas +++ b/compiler/src/main/pascal/Blaise.pas @@ -29,7 +29,7 @@ uses blaise.codegen.driver, blaise.codegen.qbe.driver, blaise.codegen.native.driver, - uUnitLoader, uDebugOPDF, uUnitInterface, uSemanticExport, uSemanticImport, + uUnitLoader, uDebugOPDF, uDebugFacts, uUnitInterface, uSemanticExport, uSemanticImport, uUnitInterfaceIO, uIfaceObject, uASTDump, blaise.frontend.opts, uConfig; @@ -363,6 +363,8 @@ var WIRFile: string; WBifFile: string; WSource: TStringList; + WFacts: TDbgFacts; + WOPDF: TOPDFEmitter; begin Self.Error := ''; try @@ -378,6 +380,34 @@ begin WCG.SetSymbolTable(Self.SymTable); WCG.AppendUnit(Self.WorkUnit); WIR := WCG.GetOutput(); + + { Per-unit OPDF debug info: when --debug-opdf is on AND this worker's + codegen produced exact debug facts (native backend), build a unit-mode + OPDF emitter for THIS unit and append its self-contained .opdf section + to the unit's IR. At link time the linker concatenates every unit's + and the program's .opdf section into one, so pdr can break inside any + unit. Mirrors the whole-program path in the main driver. + + QBE backend: GetDebugFacts returns nil (QBE assigns frames/addresses + itself), so per-unit OPDF is skipped here. A per-unit .opdf.s sidecar + is impractical in the incremental pipeline; native is the debug backend + per CLAUDE.md, so QBE incremental units carry no per-unit OPDF. } + if Self.Opts.OPDFEnabled then + begin + WFacts := WCG.GetDebugFacts(); + if WFacts <> nil then + begin + WOPDF := TOPDFEmitter.CreateForUnit(Self.WorkUnit, Self.SymTable, + Self.WorkUnit.SourceFile); + try + WOPDF.SetFacts(WFacts); + WIR := WIR + LineEnding + WOPDF.GetOutput(); + finally + WOPDF.Free(); + end; + end; + end; + WCG := nil; { release the ARC handle so codegen memory is freed before lowering } WIRFile := Self.OPath + Self.Driver.IRFileExt() + '.tmp'; diff --git a/compiler/src/main/pascal/blaise.assembler.x86_64.pas b/compiler/src/main/pascal/blaise.assembler.x86_64.pas index b824aec..6b59bf7 100644 --- a/compiler/src/main/pascal/blaise.assembler.x86_64.pas +++ b/compiler/src/main/pascal/blaise.assembler.x86_64.pas @@ -2335,6 +2335,11 @@ begin Exit else if StartsWithStr(Args, '.bss') then ASection := eskBss + else if StartsWithStr(Args, '.opdf') then + { OPDF debug section: alloc+write progbits data. Holds .byte/.word/.int/ + .quad/.ascii records emitted by uDebugOPDF; .quad