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.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';

View file

@ -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 <label> lines become
R_X86_64_64 relocations via the same data-section machinery below. }
ASection := eskOpdf
else
raise EAssembler.Create('unsupported section: ' + Args);
Exit;
@ -2390,23 +2395,44 @@ begin
Exit;
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
begin
P := 0;
Val := ParseInt(Args, P);
AWriter.AppendByte(ASection, Integer(Val));
Len := Length(Args);
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;
end;
if Dir = '.word' then
begin
P := 0;
Val := ParseInt(Args, P);
AWriter.AppendWord(ASection, Integer(Val));
Len := Length(Args);
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;
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
DataOp := ParseOperand(Args, 0, OpEnd);
if DataOp.Kind = opLabel then

View file

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

View file

@ -26,6 +26,8 @@ type
TOPDFEmitter = class
private
FProgram: TProgram;
FUnit: TUnit; { non-nil in unit mode; FProgram is nil then }
[Unretained] FUnitSymTable: TSymbolTable; { unit-mode symbol table (not owned) }
FSourceFile: string;
[Unretained] FFacts: TDbgFacts; { non-owned — provided by the driver }
FOutput: TStringList;
@ -67,6 +69,9 @@ type
procedure EmitInterface(AType: TInterfaceTypeDesc);
procedure EmitProperties(AClass: TRecordTypeDesc);
procedure EmitConstants;
procedure EmitConstantsFromList(AList: TObjectList);
procedure EmitTypesFromBlock(ABlock: TBlock);
procedure EmitGlobalVarsFromList(AList: TObjectList);
procedure EmitAllTypes;
procedure EmitGlobalVars;
procedure EmitFunctionScopes;
@ -86,8 +91,15 @@ type
function CanonicalName(AType: TTypeDesc): string;
function FieldsPayloadSize(ARec: TRecordTypeDesc): Integer;
procedure EmitFields(ARec: TRecordTypeDesc);
function ActiveSymTable: TSymbolTable;
public
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
from exact backend data instead of the approximate AST walk. }
procedure SetFacts(AFacts: TDbgFacts);
@ -138,6 +150,8 @@ constructor TOPDFEmitter.Create(AProgram: TProgram; const ASourceFile: string);
begin
inherited Create();
FProgram := AProgram;
FUnit := nil;
FUnitSymTable := nil;
FSourceFile := ASourceFile;
FOutput := TStringList.Create();
FTypeNames := TStringList.Create();
@ -152,6 +166,37 @@ begin
FDone := False;
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;
begin
FEmitted.Free();
@ -464,15 +509,20 @@ var
Ch: string;
Pfx: string;
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 := '';
if (FProgram <> nil) and (FProgram.SymbolTable <> nil) then
if Self.ActiveSymTable() <> nil then
begin
Sym := FProgram.SymbolTable.Lookup(AClassName);
Sym := Self.ActiveSymTable().Lookup(AClassName);
if Sym <> nil then
begin
Owner := Sym.OwningUnit;
if (Owner <> '') and
not SameText(Owner, FProgram.Name) and
not ((FProgram <> nil) and SameText(Owner, FProgram.Name)) and
not SameText(Owner, 'System') 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
@ -706,7 +756,11 @@ begin
L(' .byte 2 # TargetArch: archX86_64');
L(' .byte 8 # PointerSize: 8');
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');
end;
@ -991,16 +1045,16 @@ begin
end;
end;
procedure TOPDFEmitter.EmitConstants;
procedure TOPDFEmitter.EmitConstantsFromList(AList: TObjectList);
var
I: Integer;
C: TConstDecl;
TypeID: Cardinal;
RecSize: Integer;
begin
for I := 0 to FProgram.Block.ConstDecls.Count - 1 do
for I := 0 to AList.Count - 1 do
begin
C := TConstDecl(FProgram.Block.ConstDecls.Items[I]);
C := TConstDecl(AList.Items[I]);
if C.IsFloat then
begin
{ Floating-point constant: emit the 8-byte IEEE-754 Double via GAS
@ -1056,34 +1110,65 @@ begin
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
I: Integer;
TD: TTypeDecl;
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;
ST: TSymbolTable;
GIList: TObjectList;
begin
EmitUtf8Str();
if FProgram.SymbolTable <> nil then
ST := Self.ActiveSymTable();
if ST <> nil then
begin
EmitPrimitive(FProgram.SymbolTable.TypeInteger);
EmitPrimitive(FProgram.SymbolTable.TypeInt64);
EmitPrimitive(FProgram.SymbolTable.TypeByte);
EmitPrimitive(FProgram.SymbolTable.TypeBoolean);
EmitPrimitive(ST.TypeInteger);
EmitPrimitive(ST.TypeInt64);
EmitPrimitive(ST.TypeByte);
EmitPrimitive(ST.TypeBoolean);
end;
for I := 0 to FProgram.Block.TypeDecls.Count - 1 do
if FUnit <> nil then
begin
TD := TTypeDecl(FProgram.Block.TypeDecls.Items[I]);
if FProgram.SymbolTable <> nil then
begin
TDesc := FProgram.SymbolTable.FindType(TD.Name);
if TDesc <> nil then
EmitTypeDesc(TDesc);
end;
EmitTypesFromBlock(FUnit.IntfBlock);
EmitTypesFromBlock(FUnit.ImplBlock);
GIList := FUnit.GenericInstances;
end
else
begin
EmitTypesFromBlock(FProgram.Block);
GIList := FProgram.GenericInstances;
end;
for I := 0 to FProgram.GenericInstances.Count - 1 do
for I := 0 to GIList.Count - 1 do
begin
GI := TGenericInstance(FProgram.GenericInstances.Items[I]);
GI := TGenericInstance(GIList.Items[I]);
if (GI.TypeDesc <> nil) and (GI.TypeDesc.Kind = tyClass) then
EmitTypeDesc(GI.TypeDesc);
end;
@ -1113,20 +1198,31 @@ begin
end;
end;
procedure TOPDFEmitter.EmitGlobalVars;
procedure TOPDFEmitter.EmitGlobalVarsFromList(AList: TObjectList);
var
I, J: Integer;
V: TVarDecl;
begin
for I := 0 to FProgram.Block.Decls.Count - 1 do
for I := 0 to AList.Count - 1 do
begin
V := TVarDecl(FProgram.Block.Decls.Items[I]);
V := TVarDecl(AList.Items[I]);
if not V.IsGlobal then Continue;
for J := 0 to V.Names.Count - 1 do
EmitGlobalVar(V.Names.Strings[J], V.ResolvedType);
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);
begin
FFacts := AFacts;
@ -1289,6 +1385,12 @@ begin
EmitFunctionScopesFromFacts();
Exit;
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);
Labels := TStringList.Create();
try
@ -1447,9 +1549,11 @@ end;
procedure TOPDFEmitter.PatchTotalRecords;
begin
if FTotRecIdx >= 0 then
FOutput.Strings[FTotRecIdx] := ' .int ' + IntToStr(FRecordCount) +
' # TotalRecords';
{ Stream-terminated mode: TotalRecords stays 0. Readers (pdr) ignore the
field and read records until section EOF, skipping any subsequent 32-byte
'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;
procedure TOPDFEmitter.PatchUnitDirRecordCount;
@ -1466,7 +1570,10 @@ begin
FDone := True;
EmitSection();
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();
EmitGlobalVars();
EmitConstants();

View file

@ -85,6 +85,7 @@ type
object's fields stayed nil. This crashed the stdlib JSON test suite
when its TTestCase base came from target/units. }
procedure TestIncrementalRebuild_VirtualCtorVtableSlot;
procedure TestDebugOpdf_PerUnitSection_InDependencyObject;
end;
implementation
@ -949,6 +950,104 @@ begin
AssertEquals('build2 stdout (cached ctor slot)', '7' + #10, Captured)
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
RegisterTest(TSepCompileTests);

View file

@ -23,7 +23,7 @@ type
procedure TestOPDF_SectionDeclaration;
procedure TestOPDF_HeaderMagic;
procedure TestOPDF_HeaderVersion;
procedure TestOPDF_TotalRecordsPatched;
procedure TestOPDF_TotalRecordsStreamTerminated;
procedure TestOPDF_Primitive_Integer;
procedure TestOPDF_Primitive_Boolean;
procedure TestOPDF_Primitive_Int64;
@ -132,13 +132,16 @@ begin
AssertTrue('version word present', Contains(IR, '.word 1'));
end;
procedure TOPDFTests.TestOPDF_TotalRecordsPatched;
procedure TOPDFTests.TestOPDF_TotalRecordsStreamTerminated;
var
IR: string;
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.');
AssertFalse('TotalRecords not still zero placeholder',
Contains(IR, '.int 0 # TotalRecords'));
AssertTrue('TotalRecords is the stream-terminated zero',
Contains(IR, '.int 0 # TotalRecords (0 = stream-terminated)'));
end;
procedure TOPDFTests.TestOPDF_Primitive_Integer;