From a934d73566d0fd856bf9be99cc3ca45efc1ac47f Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Mon, 29 Jun 2026 12:08:08 +0100 Subject: [PATCH] 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. --- compiler/src/main/pascal/uSemanticExport.pas | 4 + compiler/src/main/pascal/uSemanticImport.pas | 4 + compiler/src/main/pascal/uUnitInterface.pas | 6 + compiler/src/main/pascal/uUnitInterfaceIO.pas | 24 +++- .../src/test/pascal/cp.test.unitinterface.pas | 104 +++++++++++++++++- tools/bif-coverage/bif-coverage.status | 1 + 6 files changed, 135 insertions(+), 8 deletions(-) diff --git a/compiler/src/main/pascal/uSemanticExport.pas b/compiler/src/main/pascal/uSemanticExport.pas index c8fabc2..5c9297d 100644 --- a/compiler/src/main/pascal/uSemanticExport.pas +++ b/compiler/src/main/pascal/uSemanticExport.pas @@ -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); diff --git a/compiler/src/main/pascal/uSemanticImport.pas b/compiler/src/main/pascal/uSemanticImport.pas index ba4e4f9..1587cc6 100644 --- a/compiler/src/main/pascal/uSemanticImport.pas +++ b/compiler/src/main/pascal/uSemanticImport.pas @@ -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 diff --git a/compiler/src/main/pascal/uUnitInterface.pas b/compiler/src/main/pascal/uUnitInterface.pas index ffa6f79..7ac82e3 100644 --- a/compiler/src/main/pascal/uUnitInterface.pas +++ b/compiler/src/main/pascal/uUnitInterface.pas @@ -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; diff --git a/compiler/src/main/pascal/uUnitInterfaceIO.pas b/compiler/src/main/pascal/uUnitInterfaceIO.pas index 38a2247..655846d 100644 --- a/compiler/src/main/pascal/uUnitInterfaceIO.pas +++ b/compiler/src/main/pascal/uUnitInterfaceIO.pas @@ -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 diff --git a/compiler/src/test/pascal/cp.test.unitinterface.pas b/compiler/src/test/pascal/cp.test.unitinterface.pas index 7798d37..80c6306 100644 --- a/compiler/src/test/pascal/cp.test.unitinterface.pas +++ b/compiler/src/test/pascal/cp.test.unitinterface.pas @@ -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 = diff --git a/tools/bif-coverage/bif-coverage.status b/tools/bif-coverage/bif-coverage.status index dc8f016..a47dd9d 100644 --- a/tools/bif-coverage/bif-coverage.status +++ b/tools/bif-coverage/bif-coverage.status @@ -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)