feat(opdf): field drilldown — unit-class symbols, Self typing, record-param shadows

Three gaps between the OPDF data and a realistic multi-unit program:

- Type records for unit-defined classes referenced bare symbols
  (vtable_TStringList, TStringList_SetText) while the emitted symbols
  carry the owning-unit prefix — any program using TStringList failed
  to LINK under --debug-opdf.  New MangledClassSym mirrors the
  codegen's ClassSymName allowlist (System / rtl.* / blaise_* /
  program-scope stay bare) for VMTAddress and property accessor
  addresses.

- Self was recorded untyped (its slot is allocated as a raw pointer),
  so 'print Self.FField' failed.  DbgMarkParams now types Self as the
  owning class — pdr dereferences class-typed variables, so instance
  field drilldown works.  The unit-less driver path now passes the
  program's symbol table to the backend (the analyser, which the
  table's uses-chain provider points at, is still alive there; the
  in-test fallback variant dangled and was reverted).

- Value record parameters present their '_data' shadow slot (the
  callee's inline copy) as the parameter instead of the raw ABI
  pointer slot — record fields are inline at known offsets, which is
  exactly what field access needs.

Verified with pdr against a program using classes+sysutils units:
print P.X / P.Y (record param fields), Self.FCount / Self.FName
(instance fields, string rendering), Msg (local string) all correct.

Suite: 2955 OK; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
This commit is contained in:
Graeme Geldenhuys 2026-06-11 14:12:55 +01:00
parent d2d5a8de3d
commit 525037a4db
4 changed files with 138 additions and 5 deletions

View file

@ -962,7 +962,13 @@ begin
CG.AppendProgram(Prog);
end
else
begin
{ Unit-less program: the backend still needs the symbol table for
class symbol prefixes and OPDF Self typing; the analyser (the
table's uses-chain provider) outlives codegen here. }
CG.SetSymbolTable(Prog.SymbolTable);
CG.Generate(Prog);
end;
IR := CG.GetOutput();
{ CG (ICodeGen) is released by ARC at program scope exit. We avoid an
explicit `CG := nil` here: the stage-1 release binary mis-compiles an

View file

@ -3221,11 +3221,12 @@ procedure TX86_64Backend.DbgRecordSlot(const AName: string; AType: TTypeDesc;
begin
if FDbgCur = nil then Exit;
{ Skip internal companion/bookkeeping slots: capture pointers, exception
frames, open-array highs, record-param data shadows. }
frames, open-array highs. '_data' record-param shadows are kept —
DbgMarkParams presents them AS the parameter (the shadow holds the
callee's inline record copy, which is what a debugger should show). }
if AName = '' then Exit;
if AName[0] = '_' then Exit;
if (Length(AName) > 5) and (Copy(AName, Length(AName) - 5, 5) = '_high') then Exit;
if (Length(AName) > 5) and (Copy(AName, Length(AName) - 5, 5) = '_data') then Exit;
FDbgCur.AddVar(AName, AType, AOffset);
end;
@ -3234,11 +3235,24 @@ var
I: Integer;
P: TMethodParam;
V: TDbgVar;
DV: TDbgVar;
Sym: TSymbol;
begin
if (FDbgCur = nil) or (ADecl = nil) then Exit;
V := FDbgCur.FindVar('Self');
if V <> nil then
begin
V.IsParam := True;
{ Type Self as the owning class so the debugger can drill its fields
(the slot holds the instance pointer; pdr derefs class types). }
if (V.TypeDesc = nil) and (ADecl.OwnerTypeName <> '') and
(FSymTable <> nil) then
begin
Sym := FSymTable.Lookup(ADecl.OwnerTypeName);
if (Sym <> nil) and (Sym.TypeDesc <> nil) then
V.TypeDesc := Sym.TypeDesc;
end;
end;
for I := 0 to ADecl.Params.Count - 1 do
begin
P := TMethodParam(ADecl.Params.Items[I]);
@ -3247,6 +3261,22 @@ begin
V.IsParam := True;
V.IsVarParam := P.IsVarParam;
V.IsConstParam := P.IsConstParam;
{ Value record/static-array params: the named slot holds an ABI pointer;
the callee's inline copy lives in the '_data' shadow slot. Present
the shadow AS the parameter — its fields are inline at known offsets,
which is what field drilldown needs. }
DV := FDbgCur.FindVar(P.ParamName + '_data');
if DV <> nil then
begin
FDbgCur.Vars.Delete(FDbgCur.Vars.IndexOf(V)); { drop the raw pointer slot }
DV.Name := P.ParamName;
DV.IsParam := True;
DV.IsVarParam := P.IsVarParam;
DV.IsConstParam := P.IsConstParam;
end
else if (V.TypeDesc = nil) and (P.ResolvedType <> nil) and
not P.IsVarParam then
V.TypeDesc := P.ResolvedType;
end;
end;

View file

@ -70,6 +70,7 @@ type
procedure EmitAllTypes;
procedure EmitGlobalVars;
procedure EmitFunctionScopes;
function MangledClassSym(const AClassName: string): string;
procedure EmitFunctionScopesFromFacts;
procedure EmitFactParameter(AVar: TDbgVar; var ADeclIdx: Integer);
procedure EmitFactLocalVar(AVar: TDbgVar; AScopeID: Integer;
@ -447,6 +448,40 @@ begin
EmitFields(AType);
end;
function TOPDFEmitter.MangledClassSym(const AClassName: string): string;
var
Sym: TSymbol;
Owner: string;
I: Integer;
Ch: string;
Pfx: string;
begin
Pfx := '';
if (FProgram <> nil) and (FProgram.SymbolTable <> nil) then
begin
Sym := FProgram.SymbolTable.Lookup(AClassName);
if Sym <> nil then
begin
Owner := Sym.OwningUnit;
if (Owner <> '') and
not 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
begin
for I := 0 to Length(Owner) - 1 do
begin
Ch := Copy(Owner, I, 1);
if Ch = '.' then Pfx := Pfx + '_'
else Pfx := Pfx + Ch;
end;
Pfx := Pfx + '_';
end;
end;
end;
Result := Pfx + AClassName;
end;
procedure TOPDFEmitter.EmitClass(AType: TRecordTypeDesc);
var
CName, ParentName: string;
@ -485,7 +520,7 @@ begin
L(' .int ' + IntToStr(GetOrAllocTypeID(CName)) + ' # TypeID');
L(' .int ' + IntToStr(ParentID) + ' # ParentTypeID');
if AType.HasVTable() then
L(' .quad vtable_' + CName + ' # VMTAddress')
L(' .quad vtable_' + MangledClassSym(CName) + ' # VMTAddress')
else
L(' .quad 0 # VMTAddress (no vtable)');
L(' .int ' + IntToStr(AType.TotalSize()) + ' # InstanceSize');
@ -901,7 +936,7 @@ begin
L(' .quad 0 # ReadAddr (field not found)');
end
else if ReadType = PAT_METHOD then
L(' .quad ' + AClass.Name + '_' + PI.ReadMethod + ' # ReadAddr (getter)')
L(' .quad ' + MangledClassSym(AClass.Name) + '_' + PI.ReadMethod + ' # ReadAddr (getter)')
else
L(' .quad 0 # ReadAddr (none)');
@ -915,7 +950,7 @@ begin
L(' .quad 0 # WriteAddr (field not found)');
end
else if WriteType = PAT_METHOD then
L(' .quad ' + AClass.Name + '_' + PI.WriteMethod + ' # WriteAddr (setter)')
L(' .quad ' + MangledClassSym(AClass.Name) + '_' + PI.WriteMethod + ' # WriteAddr (setter)')
else
L(' .quad 0 # WriteAddr (none)');

View file

@ -58,6 +58,9 @@ type
records exact frame offsets; normal builds stay label-free. }
procedure TestOpdf_StatementLabels_AndFacts;
procedure TestOpdf_Off_NoDbgLabels;
{ Self is typed as the owning class; value record params present the
'_data' shadow slot (the inline copy) so field drilldown works. }
procedure TestOpdf_Facts_SelfAndRecordParamTyping;
end;
implementation
@ -564,6 +567,65 @@ begin
AssertTrue('no debug labels in a normal build', Pos('.Ldbg_', Asm_) < 0);
end;
procedure TNativeConstArgTests.TestOpdf_Facts_SelfAndRecordParamTyping;
var
L: TLexer;
P: TParser;
Prog: TProgram;
A: TSemanticAnalyser;
CG: TCodeGenNative;
F: TDbgFunc;
V: TDbgVar;
I: Integer;
begin
L := TLexer.Create('''
program P;
type
TPt = record X, Y: Integer; end;
TW = class
N: Integer;
procedure Track(Q: TPt);
end;
procedure TW.Track(Q: TPt);
begin
N := N + Q.X;
end;
var W: TW; G: TPt;
begin
W := TW.Create();
W.Track(G);
end.
''');
P := TParser.Create(L);
Prog := P.Parse();
P.Free(); L.Free();
A := TSemanticAnalyser.Create();
A.Analyse(Prog);
CG := TCodeGenNative.Create();
CG.SetTarget(HostTarget());
CG.SetOpdfMode(True);
CG.SetSymbolTable(Prog.SymbolTable);
CG.Generate(Prog);
A.Free();
F := nil;
for I := 0 to CG.GetDebugFacts().Funcs.Count - 1 do
if TDbgFunc(CG.GetDebugFacts().Funcs.Items[I]).SymbolName = 'TW_Track' then
F := TDbgFunc(CG.GetDebugFacts().Funcs.Items[I]);
AssertNotNil('Track scope recorded', F);
V := F.FindVar('Self');
AssertNotNil('Self recorded', V);
AssertNotNil('Self typed as the owning class', V.TypeDesc);
AssertEquals('Self type', 'TW', V.TypeDesc.Name);
V := F.FindVar('Q');
AssertNotNil('record param recorded', V);
AssertTrue('record param is a param', V.IsParam);
AssertNotNil('record param typed', V.TypeDesc);
AssertEquals('record param presents the inline copy type', 'TPt', V.TypeDesc.Name);
AssertTrue('inline copy below the raw pointer slot', V.RbpOffset < -16);
Prog.Free();
CG.Free();
end;
initialization
RegisterTest(TNativeConstArgTests);