fix(opdf): export class vtables under --debug-opdf so .opdf links

The OPDF class record stores each class's VMTAddress (the debugger reads an
object's VMT pointer at runtime and matches it to identify the dynamic type).
The QBE backend emits user-class and generic-instance vtables as LOCAL symbols
(`data $vtable_X`), while the .opdf debug section — assembled as a separate
object file — references `vtable_X`.  A local symbol is not visible across
object files, so every `--debug-opdf` build of a program containing a class
failed to link with "undefined reference to vtable_X".

Add an OPDF-mode flag to the codegen (ICodeGen.SetOpdfMode, wired from the
driver's OPDFEnabled).  When on, the QBE backend emits user/generic vtables as
`export data` so the .opdf object can resolve them; when off (every normal
build) the output is byte-for-byte unchanged — verified: non-OPDF IR still emits
`data $vtable_TThing`, --debug-opdf emits `export data $vtable_TThing`.  The
native backend already emits its vtables `.globl`, so its SetOpdfMode is a no-op.

Scope is deliberately gated on OPDF mode to avoid duplicate-symbol errors in the
separate-compilation (per-unit) path, where the same generic vtable can appear
in multiple objects; OPDF is only used with whole-program --source compiles.

Result: `--debug-opdf` now links and runs class programs, and the OPDF reference
debugger loads them.  Full suite OK (2679), FIXPOINT_OK (output unchanged for
non-OPDF builds).
This commit is contained in:
Graeme Geldenhuys 2026-06-09 16:53:23 +01:00
parent d3777ffcfa
commit 971897a411
4 changed files with 40 additions and 3 deletions

View file

@ -941,6 +941,7 @@ begin
else
CG := TCodeGenQBE.Create();
CG.SetDebugMode(DebugMode);
CG.SetOpdfMode(OPDFEnabled);
if IsUnitMode then
begin
{ Unit-as-top-level: emit just the unit's bodies, no program wrapping, no @main. }

View file

@ -57,6 +57,7 @@ type
procedure GenerateUnit(AUnit: TUnit);
procedure SetSymbolTable(ASymTable: TSymbolTable);
procedure SetDebugMode(AEnabled: Boolean);
procedure SetOpdfMode(AEnabled: Boolean);
procedure AppendUnit(AUnit: TUnit);
procedure AppendProgram(AProg: TProgram);
function GetOutput: string;
@ -125,6 +126,13 @@ begin
FDebugMode := AEnabled;
end;
procedure TCodeGenNative.SetOpdfMode(AEnabled: Boolean);
begin
{ No-op: OPDF debug info is not yet wired through the native backend. Its
own vtables are already emitted .globl (see EmitClassSection), so there is
nothing to toggle here. }
end;
procedure TCodeGenNative.Generate(AProg: TProgram);
begin
Self.EnsureBackend();

View file

@ -44,6 +44,13 @@ type
{ Enable backend debug/leak-tracking behaviour. }
procedure SetDebugMode(AEnabled: Boolean);
{ Enable OPDF-debug code shaping. When on, the backend emits class vtables
as exported (global) symbols so the separately-assembled .opdf section can
reference them across object files (the OPDF class record stores each
class's VMTAddress for runtime dynamic-type resolution). Off by default,
so normal builds are byte-for-byte unchanged. }
procedure SetOpdfMode(AEnabled: Boolean);
{ Multi-unit compilation: append unit IR to existing output without
resetting the output buffer or string-literal table. }
procedure AppendUnit(AUnit: TUnit);

View file

@ -154,6 +154,9 @@ type
procedure EmitMainHeader;
procedure EmitMainFooter;
procedure EmitTypeInfoDefs(AProg: TProgram);
{ 'export data ' when OPDF-debug mode is on (so the .opdf section can
reference the vtable across object files), else plain 'data '. }
function VTableDataPrefix: string;
procedure EmitVTableDefs(AProg: TProgram);
procedure EmitMethodDefs(AProg: TProgram);
procedure EmitInterfaceDefs(AProg: TProgram);
@ -373,6 +376,7 @@ type
public
FDebugMode: Boolean;
FExportAll: Boolean;
FOpdfMode: Boolean;
constructor Create;
destructor Destroy; override;
procedure Generate(AProg: TProgram);
@ -386,6 +390,7 @@ type
the units contain class type definitions. Prog.SymbolTable is correct. }
procedure SetSymbolTable(ASymTable: TSymbolTable);
procedure SetDebugMode(AEnabled: Boolean);
procedure SetOpdfMode(AEnabled: Boolean);
procedure SetExportAll(AEnabled: Boolean);
procedure AppendUnit(AUnit: TUnit);
{ Append program IR to existing output (companion to AppendUnit).
@ -6098,8 +6103,11 @@ begin
if (TDesc = nil) or not (TDesc is TRecordTypeDesc) then Continue;
RT := TRecordTypeDesc(TDesc);
if not RT.HasVTable() then Continue;
{ TypeInfo pointer is always the first vtable entry }
Line := 'data $vtable_' + ClassSymName(TD.Name) +
{ TypeInfo pointer is always the first vtable entry. In OPDF-debug mode the
vtable is exported so the separately-assembled .opdf section can reference
it (the OPDF class record stores each class's VMTAddress); otherwise it
stays a local symbol, keeping non-debug output unchanged. }
Line := VTableDataPrefix() + '$vtable_' + ClassSymName(TD.Name) +
' = { l $typeinfo_' + ClassSymName(TD.Name);
for S := 0 to RT.VTableCount() - 1 do
begin
@ -6123,7 +6131,7 @@ begin
RT := TRecordTypeDesc(GI.TypeDesc);
if not RT.HasVTable() then Continue;
MName := QBEMangle(GI.TypeName);
Line := 'data $vtable_' + MName + ' = { l $typeinfo_' + MName;
Line := VTableDataPrefix() + '$vtable_' + MName + ' = { l $typeinfo_' + MName;
for S := 0 to RT.VTableCount() - 1 do
begin
E := RT.VTableEntryAt(S);
@ -11191,6 +11199,19 @@ begin
FDebugMode := AEnabled;
end;
procedure TCodeGenQBE.SetOpdfMode(AEnabled: Boolean);
begin
FOpdfMode := AEnabled;
end;
function TCodeGenQBE.VTableDataPrefix: string;
begin
if FOpdfMode then
Result := 'export data '
else
Result := 'data ';
end;
procedure TCodeGenQBE.SetExportAll(AEnabled: Boolean);
begin
FExportAll := AEnabled;