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:
parent
18b9cb273b
commit
1e92604599
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue