fix(bif): serialise the overload directive across the unit-interface boundary

Imported methods always had IsOverload=False: the .bif method layouts
round-tripped IsVirtual/IsOverride/IsStatic but not IsOverload, and the
importers never set it. ResolveMethodOverload's hiding walk stops at the
first non-overload candidate, so an overload set split across an imported
class and an imported ancestor would be truncated to the more-derived
level (latent today because the known overload sets live on a single
class).

Add IsOverload to both method-encoding paths and bump BLAISE-IFACE 5 -> 6:
  * TRoutineSig (class methods): field added in uUnitInterface, set in
    BuildRoutineSig, encoded/decoded in EncodeMethodSig/ReadMethodSig,
    propagated in SynthesiseMethodDecl.
  * TMethodDecl (interface and generic-template methods, the
    EncodeMethodDecl/ReadMethodDecl path): the same flag was dropped there
    too, so an overloaded interface method lost its directive across the
    .bif.

Round-trip tests for both paths: TestRoundTrip_Class_WithOverloadedMethods
and TestRoundTrip_Interface_WithOverloadedMethods. bif-coverage status
gains TRoutineSig.IsOverload.
This commit is contained in:
Graeme Geldenhuys 2026-06-29 12:08:08 +01:00
parent fdf99b26c0
commit a934d73566
6 changed files with 135 additions and 8 deletions

View file

@ -234,6 +234,10 @@ begin
VTableSlot alone cannot distinguish a static method from a final
non-virtual instance method (both are -1). }
Result.IsStatic := ASrc.IsStatic;
{ Carry the `overload` directive so a cross-unit overload set whose members
are split across an imported class and its ancestor is not truncated by
ResolveMethodOverload's hiding walk (which stops at a non-overload). }
Result.IsOverload := ASrc.IsOverload;
{ Carry visibility so cross-unit member access enforces private/protected. }
Result.Visibility := ASrc.Visibility;
Result.ReturnType := ResolveTypeRef(ASrc.ReturnTypeName, AIface, ADeps);

View file

@ -981,6 +981,10 @@ begin
{ Carry static-ness so the semantic pass's TypeName.StaticMethod() resolution
(which checks MDecl.IsStatic) succeeds for a method imported from a .bif. }
Result.IsStatic := ASig.IsStatic;
{ Carry the `overload` directive so ResolveMethodOverload's hiding walk treats
an imported overload as overloadable otherwise an overload set split
across an imported class and its ancestor is truncated to the derived level. }
Result.IsOverload := ASig.IsOverload;
{ Carry visibility so cross-unit private/protected method access is enforced. }
Result.Visibility := ASig.Visibility;
for J := 0 to ASig.Params.Count - 1 do

View file

@ -94,6 +94,12 @@ type
static and final non-virtual instance
methods, so this is a distinct flag rather
than being inferred from the slot. }
IsOverload: Boolean; { declared with the `overload` directive.
ResolveMethodOverload's hiding walk stops at
the first non-overload candidate, so this must
survive the .bif or an overload set split
across an imported class and its ancestor is
truncated to the more-derived level. }
Visibility: TMemberVisibility; { member access scope; default mvPublic }
constructor Create;
destructor Destroy; override;

View file

@ -53,7 +53,17 @@ uses
const
IFACE_MAGIC = 'BLAISE-IFACE';
IFACE_VERSION = 5; { v5 (this cycle): member Visibility (private/protected/
IFACE_VERSION = 6; { v6 (this cycle): the `overload` directive now round-trips.
TRoutineSig.IsOverload added to EncodeMethodSig/
ReadMethodSig (one extra byte after IsStatic) and
TMethodDecl.IsOverload added to EncodeMethodDecl/
ReadMethodDecl (after IsOverride; this is the
interface- and generic-template-method path). Without
it a cross-unit overload set split across an imported
class and its ancestor is truncated by the importer's
hiding walk. Both method layouts grew, so v5 readers
must reject these .bif and recompile.
v5: member Visibility (private/protected/
strict) now serialised field, method, and property
payloads each carry one extra byte for the visibility
ordinal, so v4 readers must reject these .bif and
@ -342,6 +352,9 @@ begin
{ static-method flag distinct from VTableSlot (both static and final
non-virtual instance methods carry slot -1). }
EncodeBool (AR.IsStatic) +
{ `overload` directive must round-trip so a split overload set is not
truncated by the importer's hiding walk. }
EncodeBool (AR.IsOverload) +
{ member visibility ordinal }
EncodeLpstr(IntToStr(Ord(AR.Visibility))) +
EncodeCount(AR.Params.Count);
@ -437,6 +450,9 @@ begin
resolved cross-unit refs }
EncodeBool (AM.IsVirtual) +
EncodeBool (AM.IsOverride) +
EncodeBool (AM.IsOverload) + { `overload` directive generic-template
and interface methods share this path
and may be overloaded }
EncodeCount(AM.Params.Count);
for J := 0 to AM.Params.Count - 1 do
begin
@ -1913,9 +1929,10 @@ begin
Result.IsOverride := DecodeBool(AText, APos);
Result.ResolvedQbeName := ReadLpstrAt(AText, APos);
Result.VTableSlot := StrToInt(ReadLpstrAt(AText, APos));
{ Order must mirror EncodeMethodSig: IsStatic follows VTableSlot, then the
visibility ordinal. }
{ Order must mirror EncodeMethodSig: IsStatic follows VTableSlot, then
IsOverload, then the visibility ordinal. }
Result.IsStatic := DecodeBool(AText, APos);
Result.IsOverload := DecodeBool(AText, APos);
Result.Visibility := TMemberVisibility(StrToInt(ReadLpstrAt(AText, APos)));
Pc := DecodeCount(AText, APos);
for J := 1 to Pc do
@ -1989,6 +2006,7 @@ begin
if not HasReturn then Result.ReturnTypeName := '';
Result.IsVirtual := DecodeBool(AText, APos);
Result.IsOverride := DecodeBool(AText, APos);
Result.IsOverload := DecodeBool(AText, APos);
Pc := DecodeCount(AText, APos);
for J := 1 to Pc do
begin

View file

@ -245,6 +245,8 @@ type
procedure TestRoundTrip_ViaFile;
procedure TestRoundTrip_Record;
procedure TestRoundTrip_Class_WithVirtualMethod;
procedure TestRoundTrip_Class_WithOverloadedMethods;
procedure TestRoundTrip_Interface_WithOverloadedMethods;
procedure TestRoundTrip_Interface;
procedure TestRoundTrip_Interface_WithProperty;
procedure TestRoundTrip_ProceduralType;
@ -2691,12 +2693,12 @@ begin
Iface := TUnitInterface.Create('U');
try
Buf := WriteUnitInterface(Iface);
{ Blaise Pos is 0-based; match-at-start returns 0. Version is 5 since
member Visibility (private/protected/strict) was added to the field,
method, and property encoded layouts (on top of v4's TRoutineSig.IsStatic
and v3's static-member facts). }
{ Blaise Pos is 0-based; match-at-start returns 0. Version is 6 since the
`overload` directive (TRoutineSig.IsOverload + TMethodDecl.IsOverload) was
added to the method encoded layouts (on top of v5's member Visibility,
v4's TRoutineSig.IsStatic, and v3's static-member facts). }
AssertTrue('starts with magic',
Pos('BLAISE-IFACE 5', Buf) = 0);
Pos('BLAISE-IFACE 6', Buf) = 0);
finally
Iface.Free();
end;
@ -2964,6 +2966,98 @@ begin
end;
end;
procedure TIfaceIOTests.TestRoundTrip_Class_WithOverloadedMethods;
{ Regression (bugs.txt: imported methods lose IsOverload). A class with an
overloaded method set must round-trip the `overload` directive through the
.bif: ResolveMethodOverload's hiding walk stops at the first NON-overload
candidate, so a method imported with IsOverload=False would wrongly truncate
an overload set that is split across an imported class and its ancestor. }
const
SRC =
'unit U;' + #10 +
'interface' + #10 +
'type TFoo = class' + #10 +
' procedure Add(A: Integer); overload;' + #10 +
' procedure Add(A: string); overload;' + #10 +
'end;' + #10 +
'implementation' + #10 +
'procedure TFoo.Add(A: Integer); begin end;' + #10 +
'procedure TFoo.Add(A: string); begin end;' + #10 +
'end.' + #10;
var
Iface, Round: TUnitInterface;
Buf: string;
E: TTypeEntry;
M: TRoutineSig;
I: Integer;
begin
Iface := ParseAnalyseAndExport(SRC);
try
Buf := WriteUnitInterface(Iface);
Round := ReadUnitInterface(Buf);
try
E := Round.FindType('TFoo');
AssertTrue('TFoo present', E <> nil);
AssertEquals('2 methods', 2, E.Methods.Count);
{ Every Add overload must carry IsOverload across the .bif boundary. }
for I := 0 to E.Methods.Count - 1 do
begin
M := TRoutineSig(E.Methods.Items[I]);
AssertEquals('overload method name', 'Add', M.Name);
AssertTrue('IsOverload preserved for ' + M.ResolvedQbeName, M.IsOverload);
end;
finally
Round.Free();
end;
finally
Iface.Free();
end;
end;
procedure TIfaceIOTests.TestRoundTrip_Interface_WithOverloadedMethods;
{ Interface methods serialise via EncodeMethodDecl/ReadMethodDecl (the AST
TMethodDecl path, distinct from the class TRoutineSig path). That path also
dropped IsOverload, so an overloaded interface method imported from a .bif
would lose its `overload` directive. }
const
SRC =
'unit U;' + #10 +
'interface' + #10 +
'type IShape = interface' + #10 +
' procedure Draw(X: Integer); overload;' + #10 +
' procedure Draw(X: string); overload;' + #10 +
'end;' + #10 +
'implementation end.' + #10;
var
Iface, Round: TUnitInterface;
Buf: string;
E: TTypeEntry;
M: TMethodDecl;
I: Integer;
begin
Iface := ParseAnalyseAndExport(SRC);
try
Buf := WriteUnitInterface(Iface);
Round := ReadUnitInterface(Buf);
try
E := Round.FindType('IShape');
AssertTrue('IShape present', E <> nil);
AssertTrue('is interface', E.Def is TInterfaceTypeDef);
AssertEquals('2 methods', 2, TInterfaceTypeDef(E.Def).Methods.Count);
for I := 0 to TInterfaceTypeDef(E.Def).Methods.Count - 1 do
begin
M := TMethodDecl(TInterfaceTypeDef(E.Def).Methods.Items[I]);
AssertEquals('overload method name', 'Draw', M.Name);
AssertTrue('IsOverload preserved', M.IsOverload);
end;
finally
Round.Free();
end;
finally
Iface.Free();
end;
end;
procedure TIfaceIOTests.TestRoundTrip_Interface;
const
SRC =

View file

@ -294,6 +294,7 @@ TRoutineSig.ResolvedQbeName serialise
TRoutineSig.IsVirtual serialise
TRoutineSig.IsOverride serialise
TRoutineSig.IsStatic serialise
TRoutineSig.IsOverload serialise
TRoutineSig.Visibility serialise
# TConstEntry (uUnitInterface.pas:102)