feat(debug): per-unit OPDF so incremental builds are fully debuggable

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 <funclabel> 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.
This commit is contained in:
Graeme Geldenhuys 2026-06-22 19:43:35 +01:00
parent 18b9cb273b
commit 1e92604599
6 changed files with 343 additions and 67 deletions

View file

@ -29,7 +29,7 @@ uses
blaise.codegen.driver, blaise.codegen.driver,
blaise.codegen.qbe.driver, blaise.codegen.qbe.driver,
blaise.codegen.native.driver, blaise.codegen.native.driver,
uUnitLoader, uDebugOPDF, uUnitInterface, uSemanticExport, uSemanticImport, uUnitLoader, uDebugOPDF, uDebugFacts, uUnitInterface, uSemanticExport, uSemanticImport,
uUnitInterfaceIO, uIfaceObject, uASTDump, uUnitInterfaceIO, uIfaceObject, uASTDump,
blaise.frontend.opts, uConfig; blaise.frontend.opts, uConfig;
@ -363,6 +363,8 @@ var
WIRFile: string; WIRFile: string;
WBifFile: string; WBifFile: string;
WSource: TStringList; WSource: TStringList;
WFacts: TDbgFacts;
WOPDF: TOPDFEmitter;
begin begin
Self.Error := ''; Self.Error := '';
try try
@ -378,6 +380,34 @@ begin
WCG.SetSymbolTable(Self.SymTable); WCG.SetSymbolTable(Self.SymTable);
WCG.AppendUnit(Self.WorkUnit); WCG.AppendUnit(Self.WorkUnit);
WIR := WCG.GetOutput(); 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 } WCG := nil; { release the ARC handle so codegen memory is freed before lowering }
WIRFile := Self.OPath + Self.Driver.IRFileExt() + '.tmp'; WIRFile := Self.OPath + Self.Driver.IRFileExt() + '.tmp';

View file

@ -2335,6 +2335,11 @@ begin
Exit Exit
else if StartsWithStr(Args, '.bss') then else if StartsWithStr(Args, '.bss') then
ASection := eskBss 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 <label> lines become
R_X86_64_64 relocations via the same data-section machinery below. }
ASection := eskOpdf
else else
raise EAssembler.Create('unsupported section: ' + Args); raise EAssembler.Create('unsupported section: ' + Args);
Exit; Exit;
@ -2390,23 +2395,44 @@ begin
Exit; Exit;
end; end;
{ .byte and .word accept a comma-separated list of values, e.g.
`.byte 79, 80, 68, 70` (the OPDF magic) or `.byte 0,0,0,...` (BuildID).
Emit each value in order. }
if Dir = '.byte' then if Dir = '.byte' then
begin begin
P := 0; P := 0;
Val := ParseInt(Args, P); Len := Length(Args);
AWriter.AppendByte(ASection, Integer(Val)); while P < Len do
begin
while (P < Len) and ((Args[P] = Ord(' ')) or (Args[P] = Ord(#9)) or
(Args[P] = Ord(','))) do
P := P + 1;
if P >= Len then break;
Val := ParseInt(Args, P);
AWriter.AppendByte(ASection, Integer(Val));
end;
Exit; Exit;
end; end;
if Dir = '.word' then if Dir = '.word' then
begin begin
P := 0; P := 0;
Val := ParseInt(Args, P); Len := Length(Args);
AWriter.AppendWord(ASection, Integer(Val)); while P < Len do
begin
while (P < Len) and ((Args[P] = Ord(' ')) or (Args[P] = Ord(#9)) or
(Args[P] = Ord(','))) do
P := P + 1;
if P >= Len then break;
Val := ParseInt(Args, P);
AWriter.AppendWord(ASection, Integer(Val));
end;
Exit; Exit;
end; end;
if Dir = '.long' then { .int is an alias for .long: GNU as treats both as a 32-bit value on
x86-64. The OPDF debug emitter uses .int, so accept it here. }
if (Dir = '.long') or (Dir = '.int') then
begin begin
DataOp := ParseOperand(Args, 0, OpEnd); DataOp := ParseOperand(Args, 0, OpEnd);
if DataOp.Kind = opLabel then if DataOp.Kind = opLabel then

View file

@ -33,7 +33,8 @@ type
eskData, { .data — read-write initialised data } eskData, { .data — read-write initialised data }
eskRodata, { .rodata — read-only data } eskRodata, { .rodata — read-only data }
eskBss, { .bss — zero-initialised read-write data } eskBss, { .bss — zero-initialised read-write data }
eskTbss { .tbss — thread-local zero-initialised data } eskTbss, { .tbss — thread-local zero-initialised data }
eskOpdf { .opdf OPDF debug info (alloc+write, progbits) }
); );
TElfSymBind = ( TElfSymBind = (
@ -490,8 +491,8 @@ var
I: Integer; I: Integer;
begin begin
inherited Create(); inherited Create();
SetLength(FSections, 5); SetLength(FSections, 6);
for I := 0 to 4 do for I := 0 to 5 do
FSections[I] := nil; FSections[I] := nil;
FSymbols := TList<TElfWriterSym>.Create(); FSymbols := TList<TElfWriterSym>.Create();
FSymMap := TDictionary<string, Integer>.Create(); FSymMap := TDictionary<string, Integer>.Create();
@ -525,6 +526,7 @@ begin
eskRodata: Result := '.rodata'; eskRodata: Result := '.rodata';
eskBss: Result := '.bss'; eskBss: Result := '.bss';
eskTbss: Result := '.tbss'; eskTbss: Result := '.tbss';
eskOpdf: Result := '.opdf';
else else
Result := '.text'; Result := '.text';
end; end;
@ -865,11 +867,11 @@ var
then .rela.text, .rela.data, .rela.rodata, then .rela.text, .rela.data, .rela.rodata,
then .symtab, .strtab, .shstrtab. then .symtab, .strtab, .shstrtab.
We track which sections exist and their SHT indices. } We track which sections exist and their SHT indices. }
SecOrder: array[0..4] of TElfSectionKind; SecOrder: array[0..5] of TElfSectionKind;
SecPresent: array[0..4] of Boolean; SecPresent: array[0..5] of Boolean;
SecShtIdx: array[0..4] of Integer; SecShtIdx: array[0..5] of Integer;
NumDataSecs: Integer; NumDataSecs: Integer;
RelaShtIdx: array[0..4] of Integer; RelaShtIdx: array[0..5] of Integer;
NumRelaSecs: Integer; NumRelaSecs: Integer;
SymtabIdx, StrtabIdx, ShstrtabIdx, NoteIdx: Integer; SymtabIdx, StrtabIdx, ShstrtabIdx, NoteIdx: Integer;
TotalShNum: Integer; TotalShNum: Integer;
@ -884,8 +886,8 @@ var
ShdrOff: Int64; ShdrOff: Int64;
CurOff: Int64; CurOff: Int64;
SecFileOff: array[0..4] of Int64; SecFileOff: array[0..5] of Int64;
RelaFileOff: array[0..4] of Int64; RelaFileOff: array[0..5] of Int64;
SymtabFileOff, StrtabFileOff, ShstrtabFileOff, NoteFileOff: Int64; SymtabFileOff, StrtabFileOff, ShstrtabFileOff, NoteFileOff: Int64;
SymtabBuf: TByteBuf; SymtabBuf: TByteBuf;
@ -893,8 +895,8 @@ var
SymSecIdx: Integer; SymSecIdx: Integer;
StInfo: Integer; StInfo: Integer;
RelaBuf: array[0..4] of TByteBuf; RelaBuf: array[0..5] of TByteBuf;
RelaCount: array[0..4] of Integer; RelaCount: array[0..5] of Integer;
SymRemapIdx: array of Integer; SymRemapIdx: array of Integer;
LocalSymCount, GlobalSymCount: Integer; LocalSymCount, GlobalSymCount: Integer;
LocalSyms, GlobalSyms: TList<Integer>; LocalSyms, GlobalSyms: TList<Integer>;
@ -902,8 +904,8 @@ var
ShFlags: Int64; ShFlags: Int64;
ShType: Integer; ShType: Integer;
SecNameIdx: array[0..4] of Integer; SecNameIdx: array[0..5] of Integer;
RelaNameIdx: array[0..4] of Integer; RelaNameIdx: array[0..5] of Integer;
SymtabNameIdx, StrtabNameIdx, ShstrtabNameIdx, NoteNameIdx: Integer; SymtabNameIdx, StrtabNameIdx, ShstrtabNameIdx, NoteNameIdx: Integer;
begin begin
@ -912,9 +914,10 @@ begin
SecOrder[2] := eskRodata; SecOrder[2] := eskRodata;
SecOrder[3] := eskBss; SecOrder[3] := eskBss;
SecOrder[4] := eskTbss; SecOrder[4] := eskTbss;
SecOrder[5] := eskOpdf;
NumDataSecs := 0; NumDataSecs := 0;
for I := 0 to 4 do for I := 0 to 5 do
begin begin
SecPresent[I] := FSections[Ord(SecOrder[I])] <> nil; SecPresent[I] := FSections[Ord(SecOrder[I])] <> nil;
if SecPresent[I] then if SecPresent[I] then
@ -927,7 +930,7 @@ begin
end; end;
NumRelaSecs := 0; NumRelaSecs := 0;
for I := 0 to 4 do for I := 0 to 5 do
begin begin
if SecPresent[I] and (GetSection(SecOrder[I]).RelocCount > 0) then if SecPresent[I] and (GetSection(SecOrder[I]).RelocCount > 0) then
begin begin
@ -952,7 +955,7 @@ begin
Strtab := TByteBuf.Create(); Strtab := TByteBuf.Create();
Shstrtab := TByteBuf.Create(); Shstrtab := TByteBuf.Create();
SymtabBuf := TByteBuf.Create(); SymtabBuf := TByteBuf.Create();
for I := 0 to 4 do for I := 0 to 5 do
RelaBuf[I] := nil; RelaBuf[I] := nil;
LocalSyms := TList<Integer>.Create(); LocalSyms := TList<Integer>.Create();
GlobalSyms := TList<Integer>.Create(); GlobalSyms := TList<Integer>.Create();
@ -963,7 +966,7 @@ begin
{ Build .shstrtab — section name string table } { Build .shstrtab — section name string table }
Shstrtab.PushByte(0); Shstrtab.PushByte(0);
for I := 0 to 4 do for I := 0 to 5 do
begin begin
SecNameIdx[I] := 0; SecNameIdx[I] := 0;
RelaNameIdx[I] := 0; RelaNameIdx[I] := 0;
@ -1007,7 +1010,7 @@ begin
else else
begin begin
SymSecIdx := 0; SymSecIdx := 0;
for J := 0 to 4 do for J := 0 to 5 do
if SecPresent[J] and (SecOrder[J] = Sym.Section) then if SecPresent[J] and (SecOrder[J] = Sym.Section) then
begin begin
SymSecIdx := SecShtIdx[J]; SymSecIdx := SecShtIdx[J];
@ -1027,7 +1030,7 @@ begin
else else
begin begin
SymSecIdx := 0; SymSecIdx := 0;
for J := 0 to 4 do for J := 0 to 5 do
if SecPresent[J] and (SecOrder[J] = Sym.Section) then if SecPresent[J] and (SecOrder[J] = Sym.Section) then
begin begin
SymSecIdx := SecShtIdx[J]; SymSecIdx := SecShtIdx[J];
@ -1040,7 +1043,7 @@ begin
end; end;
{ Build .rela.* sections } { Build .rela.* sections }
for I := 0 to 4 do for I := 0 to 5 do
begin begin
RelaBuf[I] := TByteBuf.Create(); RelaBuf[I] := TByteBuf.Create();
RelaCount[I] := 0; RelaCount[I] := 0;
@ -1060,7 +1063,7 @@ begin
then the section header table at the end. } then the section header table at the end. }
CurOff := Int64(ELF64_EHDR_SIZE); CurOff := Int64(ELF64_EHDR_SIZE);
for I := 0 to 4 do for I := 0 to 5 do
begin begin
SecFileOff[I] := Int64(0); SecFileOff[I] := Int64(0);
if not SecPresent[I] then Continue; if not SecPresent[I] then Continue;
@ -1072,7 +1075,7 @@ begin
CurOff := CurOff + Int64(Sec.Count); CurOff := CurOff + Int64(Sec.Count);
end; end;
for I := 0 to 4 do for I := 0 to 5 do
begin begin
RelaFileOff[I] := Int64(0); RelaFileOff[I] := Int64(0);
if RelaShtIdx[I] = 0 then Continue; if RelaShtIdx[I] = 0 then Continue;
@ -1132,7 +1135,7 @@ begin
OutBuf.AppendBytes(Buf); OutBuf.AppendBytes(Buf);
{ Emit section data bodies } { Emit section data bodies }
for I := 0 to 4 do for I := 0 to 5 do
begin begin
if not SecPresent[I] then Continue; if not SecPresent[I] then Continue;
Sec := GetSection(SecOrder[I]); Sec := GetSection(SecOrder[I]);
@ -1142,7 +1145,7 @@ begin
end; end;
{ Emit .rela.* bodies } { Emit .rela.* bodies }
for I := 0 to 4 do for I := 0 to 5 do
begin begin
if RelaShtIdx[I] = 0 then Continue; if RelaShtIdx[I] = 0 then Continue;
OutBuf.PadTo(Integer(RelaFileOff[I])); OutBuf.PadTo(Integer(RelaFileOff[I]));
@ -1170,7 +1173,7 @@ begin
ElfEmitShdr(Buf, 0, SHT_NULL, 0, 0, 0, 0, 0, 0, 0, 0); ElfEmitShdr(Buf, 0, SHT_NULL, 0, 0, 0, 0, 0, 0, 0, 0);
{ Data sections } { Data sections }
for I := 0 to 4 do for I := 0 to 5 do
begin begin
if not SecPresent[I] then Continue; if not SecPresent[I] then Continue;
Sec := GetSection(SecOrder[I]); Sec := GetSection(SecOrder[I]);
@ -1200,6 +1203,14 @@ begin
ShFlags := SHF_ALLOC or SHF_WRITE or SHF_TLS; ShFlags := SHF_ALLOC or SHF_WRITE or SHF_TLS;
ShType := SHT_NOBITS; ShType := SHT_NOBITS;
end; end;
eskOpdf:
begin
{ OPDF debug section: alloc+write progbits, matching GNU as output
for `.section .opdf, "aw", @progbits`. SHF_ALLOC makes it ride
into the loadable image so the debugger finds it by name. }
ShFlags := SHF_ALLOC or SHF_WRITE;
ShType := SHT_PROGBITS;
end;
end; end;
ElfEmitShdr(Buf, SecNameIdx[I], ShType, ShFlags, 0, ElfEmitShdr(Buf, SecNameIdx[I], ShType, ShFlags, 0,
SecFileOff[I], Int64(Sec.Size), 0, 0, SecFileOff[I], Int64(Sec.Size), 0, 0,
@ -1207,7 +1218,7 @@ begin
end; end;
{ .rela.* sections } { .rela.* sections }
for I := 0 to 4 do for I := 0 to 5 do
begin begin
if RelaShtIdx[I] = 0 then Continue; if RelaShtIdx[I] = 0 then Continue;
ElfEmitShdr(Buf, RelaNameIdx[I], SHT_RELA, ElfEmitShdr(Buf, RelaNameIdx[I], SHT_RELA,
@ -1247,7 +1258,7 @@ begin
Strtab.Free(); Strtab.Free();
Shstrtab.Free(); Shstrtab.Free();
SymtabBuf.Free(); SymtabBuf.Free();
for I := 0 to 4 do for I := 0 to 5 do
RelaBuf[I].Free(); RelaBuf[I].Free();
LocalSyms.Free(); LocalSyms.Free();
GlobalSyms.Free(); GlobalSyms.Free();

View file

@ -26,6 +26,8 @@ type
TOPDFEmitter = class TOPDFEmitter = class
private private
FProgram: TProgram; FProgram: TProgram;
FUnit: TUnit; { non-nil in unit mode; FProgram is nil then }
[Unretained] FUnitSymTable: TSymbolTable; { unit-mode symbol table (not owned) }
FSourceFile: string; FSourceFile: string;
[Unretained] FFacts: TDbgFacts; { non-owned — provided by the driver } [Unretained] FFacts: TDbgFacts; { non-owned — provided by the driver }
FOutput: TStringList; FOutput: TStringList;
@ -67,6 +69,9 @@ type
procedure EmitInterface(AType: TInterfaceTypeDesc); procedure EmitInterface(AType: TInterfaceTypeDesc);
procedure EmitProperties(AClass: TRecordTypeDesc); procedure EmitProperties(AClass: TRecordTypeDesc);
procedure EmitConstants; procedure EmitConstants;
procedure EmitConstantsFromList(AList: TObjectList);
procedure EmitTypesFromBlock(ABlock: TBlock);
procedure EmitGlobalVarsFromList(AList: TObjectList);
procedure EmitAllTypes; procedure EmitAllTypes;
procedure EmitGlobalVars; procedure EmitGlobalVars;
procedure EmitFunctionScopes; procedure EmitFunctionScopes;
@ -86,8 +91,15 @@ type
function CanonicalName(AType: TTypeDesc): string; function CanonicalName(AType: TTypeDesc): string;
function FieldsPayloadSize(ARec: TRecordTypeDesc): Integer; function FieldsPayloadSize(ARec: TRecordTypeDesc): Integer;
procedure EmitFields(ARec: TRecordTypeDesc); procedure EmitFields(ARec: TRecordTypeDesc);
function ActiveSymTable: TSymbolTable;
public public
constructor Create(AProgram: TProgram; const ASourceFile: string); constructor Create(AProgram: TProgram; const ASourceFile: string);
{ Unit-mode emitter: produces a complete, self-contained .opdf section for
a single unit's object file (no program directory, no 'main' scope).
The same section is concatenated by the linker with every other unit's
and the program's .opdf section. }
constructor CreateForUnit(AUnit: TUnit; ASymTable: TSymbolTable;
const ASourceFile: string);
{ Optional codegen facts: when set, scopes/locals/lines are emitted { Optional codegen facts: when set, scopes/locals/lines are emitted
from exact backend data instead of the approximate AST walk. } from exact backend data instead of the approximate AST walk. }
procedure SetFacts(AFacts: TDbgFacts); procedure SetFacts(AFacts: TDbgFacts);
@ -138,6 +150,8 @@ constructor TOPDFEmitter.Create(AProgram: TProgram; const ASourceFile: string);
begin begin
inherited Create(); inherited Create();
FProgram := AProgram; FProgram := AProgram;
FUnit := nil;
FUnitSymTable := nil;
FSourceFile := ASourceFile; FSourceFile := ASourceFile;
FOutput := TStringList.Create(); FOutput := TStringList.Create();
FTypeNames := TStringList.Create(); FTypeNames := TStringList.Create();
@ -152,6 +166,37 @@ begin
FDone := False; FDone := False;
end; end;
constructor TOPDFEmitter.CreateForUnit(AUnit: TUnit; ASymTable: TSymbolTable;
const ASourceFile: string);
begin
inherited Create();
FProgram := nil;
FUnit := AUnit;
FUnitSymTable := ASymTable;
FSourceFile := ASourceFile;
FOutput := TStringList.Create();
FTypeNames := TStringList.Create();
FTypeNames.Sorted := True;
FTypeNames.CaseSensitive := True;
FEmitted := TStringList.Create();
FEmitted.Sorted := True;
FEmitted.CaseSensitive := True;
FRecordCount := 0;
FTotRecIdx := -1;
FUnitDirRecCountIdx := -1;
FDone := False;
end;
function TOPDFEmitter.ActiveSymTable: TSymbolTable;
begin
if FUnit <> nil then
Result := FUnitSymTable
else if FProgram <> nil then
Result := FProgram.SymbolTable
else
Result := nil;
end;
destructor TOPDFEmitter.Destroy; destructor TOPDFEmitter.Destroy;
begin begin
FEmitted.Free(); FEmitted.Free();
@ -464,15 +509,20 @@ var
Ch: string; Ch: string;
Pfx: string; Pfx: string;
begin begin
{ Mirror the native backend's ClassSymName exactly so the .quad vtable_<sym>
label here matches the symbol the backend actually defined. In WHOLE-
PROGRAM mode the backend has a program name and drops the prefix for
program-owned classes; in UNIT mode there is no program name, so even the
unit's own classes carry their unit prefix (vtable_<Unit>_<Class>). }
Pfx := ''; Pfx := '';
if (FProgram <> nil) and (FProgram.SymbolTable <> nil) then if Self.ActiveSymTable() <> nil then
begin begin
Sym := FProgram.SymbolTable.Lookup(AClassName); Sym := Self.ActiveSymTable().Lookup(AClassName);
if Sym <> nil then if Sym <> nil then
begin begin
Owner := Sym.OwningUnit; Owner := Sym.OwningUnit;
if (Owner <> '') and if (Owner <> '') and
not SameText(Owner, FProgram.Name) and not ((FProgram <> nil) and SameText(Owner, FProgram.Name)) and
not SameText(Owner, 'System') and not SameText(Owner, 'System') and
not ((Length(Owner) >= 4) and SameText(Copy(Owner, 0, 4), 'rtl.')) and not ((Length(Owner) >= 4) and SameText(Copy(Owner, 0, 4), 'rtl.')) and
not ((Length(Owner) >= 7) and SameText(Copy(Owner, 0, 7), 'blaise_')) then not ((Length(Owner) >= 7) and SameText(Copy(Owner, 0, 7), 'blaise_')) then
@ -706,7 +756,11 @@ begin
L(' .byte 2 # TargetArch: archX86_64'); L(' .byte 2 # TargetArch: archX86_64');
L(' .byte 8 # PointerSize: 8'); L(' .byte 8 # PointerSize: 8');
FTotRecIdx := FOutput.Count; FTotRecIdx := FOutput.Count;
L(' .int 0 # TotalRecords (patched at end)'); { TotalRecords = 0 selects stream-terminated mode: the reader does not trust
a count, it reads records until section EOF and skips any further 32-byte
'OPDF' magic headers (the next unit's block). This is what makes per-unit
.opdf sections concatenate cleanly at link time. pdr ignores this field. }
L(' .int 0 # TotalRecords (0 = stream-terminated)');
L(' .int 3 # Flags: HAS_DIRECTORY or DYNARRAY_LEN32'); L(' .int 3 # Flags: HAS_DIRECTORY or DYNARRAY_LEN32');
end; end;
@ -991,16 +1045,16 @@ begin
end; end;
end; end;
procedure TOPDFEmitter.EmitConstants; procedure TOPDFEmitter.EmitConstantsFromList(AList: TObjectList);
var var
I: Integer; I: Integer;
C: TConstDecl; C: TConstDecl;
TypeID: Cardinal; TypeID: Cardinal;
RecSize: Integer; RecSize: Integer;
begin begin
for I := 0 to FProgram.Block.ConstDecls.Count - 1 do for I := 0 to AList.Count - 1 do
begin begin
C := TConstDecl(FProgram.Block.ConstDecls.Items[I]); C := TConstDecl(AList.Items[I]);
if C.IsFloat then if C.IsFloat then
begin begin
{ Floating-point constant: emit the 8-byte IEEE-754 Double via GAS { Floating-point constant: emit the 8-byte IEEE-754 Double via GAS
@ -1056,34 +1110,65 @@ begin
end; end;
end; end;
procedure TOPDFEmitter.EmitAllTypes; procedure TOPDFEmitter.EmitConstants;
begin
if FUnit <> nil then
begin
EmitConstantsFromList(FUnit.IntfBlock.ConstDecls);
EmitConstantsFromList(FUnit.ImplBlock.ConstDecls);
end
else if FProgram <> nil then
EmitConstantsFromList(FProgram.Block.ConstDecls);
end;
procedure TOPDFEmitter.EmitTypesFromBlock(ABlock: TBlock);
var var
I: Integer; I: Integer;
TD: TTypeDecl; TD: TTypeDecl;
TDesc: TTypeDesc; TDesc: TTypeDesc;
ST: TSymbolTable;
begin
ST := Self.ActiveSymTable();
if ST = nil then Exit;
for I := 0 to ABlock.TypeDecls.Count - 1 do
begin
TD := TTypeDecl(ABlock.TypeDecls.Items[I]);
TDesc := ST.FindType(TD.Name);
if TDesc <> nil then
EmitTypeDesc(TDesc);
end;
end;
procedure TOPDFEmitter.EmitAllTypes;
var
I: Integer;
GI: TGenericInstance; GI: TGenericInstance;
ST: TSymbolTable;
GIList: TObjectList;
begin begin
EmitUtf8Str(); EmitUtf8Str();
if FProgram.SymbolTable <> nil then ST := Self.ActiveSymTable();
if ST <> nil then
begin begin
EmitPrimitive(FProgram.SymbolTable.TypeInteger); EmitPrimitive(ST.TypeInteger);
EmitPrimitive(FProgram.SymbolTable.TypeInt64); EmitPrimitive(ST.TypeInt64);
EmitPrimitive(FProgram.SymbolTable.TypeByte); EmitPrimitive(ST.TypeByte);
EmitPrimitive(FProgram.SymbolTable.TypeBoolean); EmitPrimitive(ST.TypeBoolean);
end; end;
for I := 0 to FProgram.Block.TypeDecls.Count - 1 do if FUnit <> nil then
begin begin
TD := TTypeDecl(FProgram.Block.TypeDecls.Items[I]); EmitTypesFromBlock(FUnit.IntfBlock);
if FProgram.SymbolTable <> nil then EmitTypesFromBlock(FUnit.ImplBlock);
begin GIList := FUnit.GenericInstances;
TDesc := FProgram.SymbolTable.FindType(TD.Name); end
if TDesc <> nil then else
EmitTypeDesc(TDesc); begin
end; EmitTypesFromBlock(FProgram.Block);
GIList := FProgram.GenericInstances;
end; end;
for I := 0 to FProgram.GenericInstances.Count - 1 do for I := 0 to GIList.Count - 1 do
begin begin
GI := TGenericInstance(FProgram.GenericInstances.Items[I]); GI := TGenericInstance(GIList.Items[I]);
if (GI.TypeDesc <> nil) and (GI.TypeDesc.Kind = tyClass) then if (GI.TypeDesc <> nil) and (GI.TypeDesc.Kind = tyClass) then
EmitTypeDesc(GI.TypeDesc); EmitTypeDesc(GI.TypeDesc);
end; end;
@ -1113,20 +1198,31 @@ begin
end; end;
end; end;
procedure TOPDFEmitter.EmitGlobalVars; procedure TOPDFEmitter.EmitGlobalVarsFromList(AList: TObjectList);
var var
I, J: Integer; I, J: Integer;
V: TVarDecl; V: TVarDecl;
begin begin
for I := 0 to FProgram.Block.Decls.Count - 1 do for I := 0 to AList.Count - 1 do
begin begin
V := TVarDecl(FProgram.Block.Decls.Items[I]); V := TVarDecl(AList.Items[I]);
if not V.IsGlobal then Continue; if not V.IsGlobal then Continue;
for J := 0 to V.Names.Count - 1 do for J := 0 to V.Names.Count - 1 do
EmitGlobalVar(V.Names.Strings[J], V.ResolvedType); EmitGlobalVar(V.Names.Strings[J], V.ResolvedType);
end; end;
end; end;
procedure TOPDFEmitter.EmitGlobalVars;
begin
if FUnit <> nil then
begin
EmitGlobalVarsFromList(FUnit.IntfBlock.Decls);
EmitGlobalVarsFromList(FUnit.ImplBlock.Decls);
end
else if FProgram <> nil then
EmitGlobalVarsFromList(FProgram.Block.Decls);
end;
procedure TOPDFEmitter.SetFacts(AFacts: TDbgFacts); procedure TOPDFEmitter.SetFacts(AFacts: TDbgFacts);
begin begin
FFacts := AFacts; FFacts := AFacts;
@ -1289,6 +1385,12 @@ begin
EmitFunctionScopesFromFacts(); EmitFunctionScopesFromFacts();
Exit; Exit;
end; end;
{ Non-facts (approximate AST-walk) path is whole-program only. Per-unit
incremental compilation only embeds OPDF when the backend produced exact
debug facts (native); the QBE backend produces none, so a unit-mode
emitter without facts emits no function scopes (acceptable: native is the
debug backend, see CLAUDE.md). }
if FProgram = nil then Exit;
Decls := TObjectList.Create(False); Decls := TObjectList.Create(False);
Labels := TStringList.Create(); Labels := TStringList.Create();
try try
@ -1447,9 +1549,11 @@ end;
procedure TOPDFEmitter.PatchTotalRecords; procedure TOPDFEmitter.PatchTotalRecords;
begin begin
if FTotRecIdx >= 0 then { Stream-terminated mode: TotalRecords stays 0. Readers (pdr) ignore the
FOutput.Strings[FTotRecIdx] := ' .int ' + IntToStr(FRecordCount) + field and read records until section EOF, skipping any subsequent 32-byte
' # TotalRecords'; 'OPDF' headers from concatenated per-unit blocks. Patching a real count
here would be wrong once the linker concatenates sections, so this is a
deliberate no-op. }
end; end;
procedure TOPDFEmitter.PatchUnitDirRecordCount; procedure TOPDFEmitter.PatchUnitDirRecordCount;
@ -1466,7 +1570,10 @@ begin
FDone := True; FDone := True;
EmitSection(); EmitSection();
EmitHeader(); EmitHeader();
EmitUnitDirectory(); { recUnitDirectory is emitted ONLY by the program object (pdr ignores it).
Units omit it; their records are read in stream-terminated mode. }
if FProgram <> nil then
EmitUnitDirectory();
EmitAllTypes(); EmitAllTypes();
EmitGlobalVars(); EmitGlobalVars();
EmitConstants(); EmitConstants();

View file

@ -85,6 +85,7 @@ type
object's fields stayed nil. This crashed the stdlib JSON test suite object's fields stayed nil. This crashed the stdlib JSON test suite
when its TTestCase base came from target/units. } when its TTestCase base came from target/units. }
procedure TestIncrementalRebuild_VirtualCtorVtableSlot; procedure TestIncrementalRebuild_VirtualCtorVtableSlot;
procedure TestDebugOpdf_PerUnitSection_InDependencyObject;
end; end;
implementation implementation
@ -949,6 +950,104 @@ begin
AssertEquals('build2 stdout (cached ctor slot)', '7' + #10, Captured) AssertEquals('build2 stdout (cached ctor slot)', '7' + #10, Captured)
end; end;
procedure TSepCompileTests.TestDebugOpdf_PerUnitSection_InDependencyObject;
const
DepSrc =
'''
unit OpdfDep;
interface
procedure Greet(const AName: string);
implementation
procedure Greet(const AName: string);
var
Total: Integer;
Msg: string;
begin
Total := 0;
Msg := 'Hello, ';
Msg := Msg + AName;
Total := Total + 1;
WriteLn(Msg);
Total := Total + 1;
WriteLn('Count is ', Total)
end;
end.
''';
ProgSrc =
'''
program UseOpdfDep;
uses OpdfDep;
begin
Greet('World')
end.
''';
var
DepPas, ProgPas, ProgBin, CacheDir, DepObj: string;
Captured, ObjOut: string;
Rc: Integer;
Proc: TProcess;
Chunk: string;
begin
if not ToolchainAvailable() then
begin
Fail('toolchain missing — qbe or RTL not found');
Exit
end;
if not FileExists(BlaisePath()) then
begin
Fail('blaise binary missing at ' + BlaisePath());
Exit
end;
DepPas := FScratch + '/OpdfDep.pas';
ProgPas := FScratch + '/use_opdfdep.pas';
ProgBin := FScratch + '/use_opdfdep';
CacheDir := FScratch + '/units-opdf';
DepObj := CacheDir + '/opdfdep.o';
WriteFile(DepPas, DepSrc);
WriteFile(ProgPas, ProgSrc);
ForceDirectories(CacheDir);
{ Default (incremental) native build with --debug-opdf: the dependency
unit is compiled to its own .o in the cache dir. With per-unit OPDF the
worker embeds a self-contained .opdf section into that .o so pdr can break
inside OpdfDep.Greet. Before this feature only the program object carried
OPDF, so the dependency .o had no .opdf section. }
Rc := RunBlaise(['--source', ProgPas, '--output', ProgBin,
'--backend', 'native', '--debug-opdf',
'--unit-cache', CacheDir,
'--unit-path', FScratch], Captured);
AssertEquals('blaise(--debug-opdf) exit code (out: ' + Captured + ')', 0, Rc);
AssertTrue('use_opdfdep exists', FileExists(ProgBin));
AssertTrue('dependency object exists at ' + DepObj, FileExists(DepObj));
{ The unit .o must carry an .opdf section (objdump -h shows it). }
Proc := TProcess.Create(nil);
try
Proc.Executable := 'objdump';
Proc.Parameters.Add('-h');
Proc.Parameters.Add(DepObj);
Proc.Execute();
ObjOut := '';
repeat
Chunk := Proc.ReadOutput();
ObjOut := ObjOut + Chunk
until (Chunk = '') and not Proc.Running;
Proc.WaitOnExit()
finally
Proc.Free()
end;
AssertTrue('dependency .o has an .opdf section (objdump -h)',
Pos('.opdf', ObjOut) >= 0);
{ The program still runs correctly. }
Rc := RunBinary(ProgBin, Captured);
AssertEquals('use_opdfdep exit code', 0, Rc);
AssertEquals('use_opdfdep stdout', 'Hello, World' + #10 + 'Count is 2' + #10,
Captured)
end;
initialization initialization
RegisterTest(TSepCompileTests); RegisterTest(TSepCompileTests);

View file

@ -23,7 +23,7 @@ type
procedure TestOPDF_SectionDeclaration; procedure TestOPDF_SectionDeclaration;
procedure TestOPDF_HeaderMagic; procedure TestOPDF_HeaderMagic;
procedure TestOPDF_HeaderVersion; procedure TestOPDF_HeaderVersion;
procedure TestOPDF_TotalRecordsPatched; procedure TestOPDF_TotalRecordsStreamTerminated;
procedure TestOPDF_Primitive_Integer; procedure TestOPDF_Primitive_Integer;
procedure TestOPDF_Primitive_Boolean; procedure TestOPDF_Primitive_Boolean;
procedure TestOPDF_Primitive_Int64; procedure TestOPDF_Primitive_Int64;
@ -132,13 +132,16 @@ begin
AssertTrue('version word present', Contains(IR, '.word 1')); AssertTrue('version word present', Contains(IR, '.word 1'));
end; end;
procedure TOPDFTests.TestOPDF_TotalRecordsPatched; procedure TOPDFTests.TestOPDF_TotalRecordsStreamTerminated;
var var
IR: string; IR: string;
begin begin
{ Stream-terminated mode: TotalRecords stays 0 so per-unit .opdf sections
concatenate at link time (the reader reads to section EOF, skipping any
further 'OPDF' magic headers). pdr ignores the count entirely. }
IR := GenOPDF('program P; var X: Integer; begin end.'); IR := GenOPDF('program P; var X: Integer; begin end.');
AssertFalse('TotalRecords not still zero placeholder', AssertTrue('TotalRecords is the stream-terminated zero',
Contains(IR, '.int 0 # TotalRecords')); Contains(IR, '.int 0 # TotalRecords (0 = stream-terminated)'));
end; end;
procedure TOPDFTests.TestOPDF_Primitive_Integer; procedure TOPDFTests.TestOPDF_Primitive_Integer;