feat(opdf): emit open-array debug info (ArrayKind=2 + companion slot)

Open-array parameters were emitted to the OPDF companion file as dynamic
arrays (IsDynamic=1), so the debugger read their length from the heap
header at data-4. An open array has no header — it is a (data ptr, high
index) pair on the stack — so this read returned garbage (Length=32765).

Model open arrays as a distinct kind:

- TDbgVar gains IsOpenArray + HighRbpOffset; DbgMarkParams records the
  '_high' companion slot offset from the frame map.
- The recArray byte previously named IsDynamic now carries ArrayKind
  (0=static, 1=dynamic, 2=open-array); open arrays emit ArrayKind=2.
- recLocalVar for an open array uses LocationExpr=5 and appends the
  companion '_high' slot offset as a trailing SmallInt.
- Open arrays get a distinct canonical name ('open array of T') so they
  no longer alias a same-element dynamic array's TypeID.
- Parameter and facts-path variable type records are now emitted; the
  anonymous open-array type previously had no recArray written at all.

The debugger side (length read from the companion slot) lives in the
opdebugger repo. FPC's dbgopdf.pas has no open-array concept; this is a
Blaise extension to the OPDF format.
This commit is contained in:
Graeme Geldenhuys 2026-06-13 01:06:16 +01:00
parent 6ba1bc9b78
commit 7b8116310f
4 changed files with 144 additions and 9 deletions

View file

@ -3587,6 +3587,19 @@ begin
DV.IsVarParam := P.IsVarParam;
DV.IsConstParam := P.IsConstParam;
end
else if P.IsOpenArray then
begin
{ Open-array parameter: the named slot holds the data pointer; the
element count is High+1, where High lives in the companion
'<name>_high' slot. Record the companion offset so the OPDF
emitter can locate the length — the debugger has no heap header
to read it from. }
V.IsOpenArray := True;
if not FFrame.TryGetValue(P.ParamName + '_high', V.HighRbpOffset) then
V.HighRbpOffset := 0;
if (V.TypeDesc = nil) and (P.ResolvedType <> nil) then
V.TypeDesc := P.ResolvedType;
end
else if P.IsVarParam then
begin
{ var/out parameter: the slot holds the ADDRESS of the caller's

View file

@ -47,6 +47,12 @@ type
(var/out params, by-reference aggregates, captured outer locals) —
emitted as LocationExpr=3 'RBP-relative indirect'. }
Indirect: Boolean;
{ Open-array parameter: the slot holds the data pointer (no heap header);
the element count is High+1, where High lives in a separate companion
stack slot at HighRbpOffset. Emitted as LocationExpr=5 'open-array'
with the companion offset trailing the LocationData. }
IsOpenArray: Boolean;
HighRbpOffset: Integer;
end;
{ One statement-level line marker: the backend emitted LabelName ('.Ldbg_N')

View file

@ -71,6 +71,7 @@ type
procedure EmitGlobalVars;
procedure EmitFunctionScopes;
function MangledClassSym(const AClassName: string): string;
procedure EmitFactsTypes;
procedure EmitFunctionScopesFromFacts;
procedure EmitFactParameter(AVar: TDbgVar; var ADeclIdx: Integer);
procedure EmitFactLocalVar(AVar: TDbgVar; AScopeID: Integer;
@ -122,6 +123,8 @@ const
LOC_RBP = 1;
LOC_RBP_INDIRECT = 3; { frame slot holds the value's address }
LOC_OPENARRAY = 5; { open-array: data ptr at LocationData, element count
from companion _high slot (trailing SmallInt) }
PAT_FIELD = 0; { property accessor: direct field offset }
PAT_METHOD = 1; { property accessor: method call }
@ -255,10 +258,13 @@ begin
CanonicalName(SA.ElementType);
end;
tyOpenArray:
if AType.Name <> '' then
Result := AType.Name
else
Result := 'array of ' + CanonicalName(TOpenArrayTypeDesc(AType).ElementType);
{ An open array is a distinct kind from a dynamic array (no heap header;
length in a companion slot). Give it a distinct canonical name so it
gets its own TypeID and recArray(ArrayKind=2) — otherwise it would
alias a same-element 'array of T' dynamic-array record and inherit
ArrayKind=1. }
Result := 'open array of ' +
CanonicalName(TOpenArrayTypeDesc(AType).ElementType);
else
if AType.Name = '' then
Result := 'Pointer'
@ -607,7 +613,13 @@ begin
begin
P := TMethodParam(AMethod.Params.Items[I]);
if P.ResolvedType <> nil then
CName := CanonicalName(P.ResolvedType)
begin
CName := CanonicalName(P.ResolvedType);
{ Ensure the parameter's type record is emitted (e.g. an open-array
param's recArray with ArrayKind=2) — otherwise the TypeID below
references a record that was never written. }
EmitTypeDesc(P.ResolvedType);
end
else
CName := 'Pointer';
RecSize := 9 + Length(P.ParamName);
@ -809,18 +821,21 @@ begin
end
else
begin
{ Open array: a (data ptr, high) pair with no heap header. ArrayKind=2
tells the debugger to read the length from the variable's companion
slot, not from data-4. (FPC's dbgopdf.pas has no open-array concept;
this is a Blaise extension to the OPDF format.) }
EmitTypeDesc(TOpenArrayTypeDesc(AType).ElementType);
ElemName := CanonicalName(TOpenArrayTypeDesc(AType).ElementType);
ElemID := GetOrAllocTypeID(ElemName);
IsDyn := 1;
RecSize := 12 + Length(CName);
L('');
L(' # recArray (dynamic): ' + CName);
L(' # recArray (open): ' + CName);
EmitRecHdr(REC_ARRAY, RecSize);
L(' .int ' + IntToStr(GetOrAllocTypeID(CName)) + ' # TypeID');
L(' .int ' + IntToStr(ElemID) + ' # ElementTypeID');
L(' .byte 1 # Dimensions');
L(' .byte ' + IntToStr(IsDyn) + ' # IsDynamic');
L(' .byte 2 # ArrayKind');
EmitStrField(CName);
end;
end;
@ -1048,6 +1063,30 @@ begin
if (GI.TypeDesc <> nil) and (GI.TypeDesc.Kind = tyClass) then
EmitTypeDesc(GI.TypeDesc);
end;
{ Facts path: parameter/local types are often anonymous (e.g. an
open-array param 'array of Integer') and never appear in TypeDecls,
so emit every frame slot's type here. EmitTypeDesc dedupes against
types already written above. }
EmitFactsTypes();
end;
procedure TOPDFEmitter.EmitFactsTypes;
var
I, J: Integer;
F: TDbgFunc;
V: TDbgVar;
begin
if FFacts = nil then Exit;
for I := 0 to FFacts.Funcs.Count - 1 do
begin
F := TDbgFunc(FFacts.Funcs.Items[I]);
for J := 0 to F.Vars.Count - 1 do
begin
V := TDbgVar(F.Vars.Items[J]);
if V.TypeDesc <> nil then
EmitTypeDesc(V.TypeDesc);
end;
end;
end;
procedure TOPDFEmitter.EmitGlobalVars;
@ -1108,13 +1147,19 @@ begin
else
CName := 'Pointer';
RecSize := 15 + Length(AVar.Name);
{ Open-array locals carry an extra trailing SmallInt: the companion
'_high' slot offset. Account for it in the record size. }
if AVar.IsOpenArray then
RecSize := RecSize + 2;
L('');
L(' # recLocalVar: ' + AVar.Name + ' (rbp' +
IntToStr(AVar.RbpOffset) + ')');
EmitRecHdr(REC_LOCALVAR, RecSize);
L(' .int ' + IntToStr(GetOrAllocTypeID(CName)) + ' # TypeID');
L(' .int ' + IntToStr(AScopeID) + ' # ScopeID');
if AVar.Indirect then
if AVar.IsOpenArray then
L(' .byte ' + IntToStr(LOC_OPENARRAY) + ' # LocationExpr (open-array)')
else if AVar.Indirect then
L(' .byte ' + IntToStr(LOC_RBP_INDIRECT) +
' # LocationExpr (RBP-relative indirect)')
else
@ -1122,6 +1167,9 @@ begin
L(' .word ' + IntToStr(ADeclIdx) + ' # DeclIndex');
EmitNameLen(AVar.Name);
L(' .word ' + IntToStr(AVar.RbpOffset) + ' # LocationData (RBP offset)');
if AVar.IsOpenArray then
L(' .word ' + IntToStr(AVar.HighRbpOffset) +
' # CompanionData (_high RBP offset)');
EmitNameData(AVar.Name);
ADeclIdx := ADeclIdx + 1;
end;

View file

@ -55,6 +55,8 @@ type
procedure TestOPDF_Pointer_RecType;
procedure TestOPDF_Array_Static_RecType;
procedure TestOPDF_Array_Static_IsDynamic;
procedure TestOPDF_Array_OpenArray_ArrayKind;
procedure TestOPDF_OpenArray_CompanionLocation;
procedure TestOPDF_Set_RecType;
procedure TestOPDF_Set_SizeInBytes;
procedure TestOPDF_Property_RecType;
@ -469,6 +471,66 @@ begin
AssertTrue('IsDynamic=0 for static array', Contains(IR, '.byte 0 # IsDynamic'));
end;
procedure TOPDFTests.TestOPDF_Array_OpenArray_ArrayKind;
var
IR: string;
begin
{ An open-array parameter is neither static nor dynamic: it is a (ptr, high)
pair with no heap header. Its recArray must carry ArrayKind=2, NOT
IsDynamic=1 (which would make the debugger read length from data-4). }
IR := GenOPDF(
'''
program P;
procedure Bar(a: array of Integer);
begin end;
begin end.
''');
AssertTrue('ArrayKind=2 for open array', Contains(IR, '.byte 2 # ArrayKind'));
AssertTrue('open array is not flagged dynamic',
not Contains(IR, 'array of Integer' + #10 + ' .byte 1 # ArrayKind'));
end;
procedure TOPDFTests.TestOPDF_OpenArray_CompanionLocation;
var
L: TLexer;
P: TParser;
Pr: TProgram;
A: TSemanticAnalyser;
E: TOPDFEmitter;
Facts: TDbgFacts;
O: string;
begin
L := TLexer.Create('program P; begin end.');
P := TParser.Create(L);
Pr := P.Parse();
A := TSemanticAnalyser.Create();
Facts := BuildSampleFacts();
try
A.Analyse(Pr);
E := TOPDFEmitter.Create(Pr, 'test.pas');
try
E.SetFacts(Facts);
O := E.GetOutput();
finally
E.Free();
end;
{ The open-array local 'Oa' must use LocationExpr=5 (open-array) and carry
both its data-slot offset (-32) and the companion _high offset (-40). }
AssertTrue('open-array LocationExpr=5',
Pos('.byte 5 # LocationExpr (open-array)', O) > 0);
AssertTrue('open-array data offset -32',
Pos('.word -32 # LocationData', O) > 0);
AssertTrue('open-array companion _high offset -40',
Pos('.word -40 # CompanionData (_high RBP offset)', O) > 0);
finally
Facts.Free();
A.Free();
Pr.Free();
P.Free();
L.Free();
end;
end;
procedure TOPDFTests.TestOPDF_Set_RecType;
var
IR: string;
@ -649,6 +711,12 @@ begin
V := F.AddVar('By', nil, -16);
V.IsParam := True;
F.AddVar('Tmp', nil, -24);
{ Open-array parameter: data slot at -32, companion _high slot at -40.
The OPDF emitter must locate the length via the companion offset. }
V := F.AddVar('Oa', nil, -32);
V.IsParam := True;
V.IsOpenArray := True;
V.HighRbpOffset := -40;
LineRec := TDbgLine.Create();
LineRec.LabelName := '.Ldbg_3';
LineRec.Line := 10;