Skip to content
Merged
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
* Fix parallel compilation of scripts ([PR #19649](https://github.com/dotnet/fsharp/pull/19649))
* Fix parser recovery, name resolution, and code completion for unfinished enum patterns ([PR #19708](https://github.com/dotnet/fsharp/pull/19708))
* Parser: fix unexpected diagnostics in debug builds, improve error messages ([PR #19730](https://github.com/dotnet/fsharp/pull/19730))
* Fix signature conformance: overloaded member with unit parameter `M(())` now matches sig `member M: unit -> unit`. ([Issue #19596](https://github.com/dotnet/fsharp/issues/19596), [PR #19615](https://github.com/dotnet/fsharp/pull/19615))

### Added

Expand Down
60 changes: 54 additions & 6 deletions src/Compiler/Checking/SignatureConformance.fs
Original file line number Diff line number Diff line change
Expand Up @@ -295,19 +295,41 @@ type Checker(g, amap, denv, remapInfo: SignatureRepackageInfo, checkingSig) =
err(fun(x, y, z) -> FSComp.SR.ValueNotContainedMutabilityGenericParametersDiffer(x, y, z, string mtps, string ntps))
elif implValInfo.KindsOfTypars <> sigValInfo.KindsOfTypars then
err(FSComp.SR.ValueNotContainedMutabilityGenericParametersAreDifferentKinds)
elif not (nSigArgInfos <= implArgInfos.Length && List.forall2 (fun x y -> List.length x <= List.length y) sigArgInfos (fst (List.splitAt nSigArgInfos implArgInfos))) then
else
// Check arg group arities. An empty impl group [] is compatible with
// a singleton sig group [_] when the member takes unit (e.g. member M(()) vs member M: unit -> unit)
let argGroupsCompatible =
nSigArgInfos <= implArgInfos.Length &&
List.forall2
(fun (sigGroup: ArgReprInfo list) (implGroup: ArgReprInfo list) ->
List.length sigGroup <= List.length implGroup
|| (implGroup.IsEmpty && sigGroup.Length = 1))
sigArgInfos
(fst (List.splitAt nSigArgInfos implArgInfos))

if not argGroupsCompatible then
err(fun(x, y, z) -> FSComp.SR.ValueNotContainedMutabilityAritiesDiffer(x, y, z, id.idText, string nSigArgInfos, id.idText, id.idText))
else
let implArgInfos = implArgInfos |> List.truncate nSigArgInfos
let implArgInfos = (implArgInfos, sigArgInfos) ||> List.map2 (fun l1 l2 -> l1 |> List.take l2.Length)
// When impl has empty group [] (unit param like member M(())), synthesize
// ArgReprInfo from the sig so SetValReprInfo reflects the signature contract.
let implArgInfos =
(implArgInfos, sigArgInfos)
||> List.map2 (fun implGroup sigGroup ->
if implGroup.IsEmpty && sigGroup.Length = 1 then
sigGroup |> List.map (fun sigArg ->
({ Attribs = sigArg.Attribs; Name = sigArg.Name; OtherRange = None }: ArgReprInfo))
else
implGroup |> List.take (min implGroup.Length sigGroup.Length))
// Propagate some information signature to implementation.

// Check the attributes on each argument, and update the ValReprInfo for
// the value to reflect the information in the signature.
// This ensures that the compiled form of the value matches the signature rather than
// the implementation. This also propagates argument names from signature to implementation
let res =
(implArgInfos, sigArgInfos) ||> List.forall2 (List.forall2 (fun implArgInfo sigArgInfo ->
(implArgInfos, sigArgInfos) ||> List.forall2 (fun (implGroup: ArgReprInfo list) (sigGroup: ArgReprInfo list) ->
(implGroup, sigGroup) ||> List.forall2 (fun implArgInfo sigArgInfo ->
checkAttribs aenv (implArgInfo.Attribs.AsList()) (sigArgInfo.Attribs.AsList()) (fun attribs ->
match implArgInfo.Name, sigArgInfo.Name with
| Some iname, Some sname when sname.idText <> iname.idText ->
Expand Down Expand Up @@ -661,7 +683,7 @@ type Checker(g, amap, denv, remapInfo: SignatureRepackageInfo, checkingSig) =
let fkey = fv.GetLinkagePartialKey()
(akey.MemberParentMangledName = fkey.MemberParentMangledName) &&
(akey.LogicalName = fkey.LogicalName) &&
(akey.TotalArgCount = fkey.TotalArgCount)
(akey.TotalArgCount = fkey.TotalArgCount)
Comment thread
T-Gro marked this conversation as resolved.

(implModType.AllValsAndMembersByLogicalNameUncached, signModType.AllValsAndMembersByLogicalNameUncached)
||> NameMap.suball2
Expand All @@ -672,6 +694,10 @@ type Checker(g, amap, denv, remapInfo: SignatureRepackageInfo, checkingSig) =
| [av], [fv] ->
if valuesPartiallyMatch av fv then
checkVal implModRef aenv infoReader av fv
elif av.IsMember && fv.IsMember
&& av.LogicalName <> ".ctor"
&& typeAEquivAux EraseAll g aenv av.Type fv.Type then
checkVal implModRef aenv infoReader av fv
else
sigValHadNoMatchingImplementation fv None
false
Expand All @@ -683,9 +709,31 @@ type Checker(g, amap, denv, remapInfo: SignatureRepackageInfo, checkingSig) =
| None -> None
| Some av -> Some(fv, av))

// For unmatched sig vals, try relaxed matching for unit-parameter equivalence:
// member M(()) has TotalArgCount 1, sig member M: unit -> unit has TotalArgCount 2,
// but their types are both unit -> unit.
let matchedAvs = matchingPairs |> List.map snd
let matchedFvs = matchingPairs |> List.map fst
let unmatchedFvs = fvs |> List.filter (fun fv -> not (List.exists (fun fv2 -> obj.ReferenceEquals(fv, fv2)) matchedFvs))
let unmatchedAvs = avs |> List.filter (fun av -> not (List.exists (fun av2 -> obj.ReferenceEquals(av, av2)) matchedAvs))
let relaxedPairs, _ =
(([], unmatchedAvs), unmatchedFvs)
||> List.fold (fun (pairs, remainingAvs) fv ->
let fkey = fv.GetLinkagePartialKey()
match remainingAvs |> List.tryFind (fun av ->
let akey = av.GetLinkagePartialKey()
akey.MemberParentMangledName = fkey.MemberParentMangledName &&
akey.LogicalName = fkey.LogicalName &&
av.IsMember && fv.IsMember &&
av.LogicalName <> ".ctor" &&
typeAEquivAux EraseAll g aenv av.Type fv.Type) with
| None -> (pairs, remainingAvs)
| Some av -> ((fv, av) :: pairs, remainingAvs |> List.filter (fun a -> not (obj.ReferenceEquals(a, av)))))
let allMatchingPairs = matchingPairs @ relaxedPairs

// Check the ones with matching linkage
let allPairsOk = matchingPairs |> List.map (fun (fv, av) -> checkVal implModRef aenv infoReader av fv) |> List.forall id
let someNotOk = matchingPairs.Length < fvs.Length
let allPairsOk = allMatchingPairs |> List.map (fun (fv, av) -> checkVal implModRef aenv infoReader av fv) |> List.forall id
let someNotOk = allMatchingPairs.Length < fvs.Length
// Report an error for those that don't. Try pairing up by enclosing-type/name
if someNotOk then
let noMatches, partialMatchingPairs =
Expand Down
241 changes: 236 additions & 5 deletions tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs
Comment thread
T-Gro marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -762,10 +762,90 @@ let (|A|B|) (x: int) = if x > 0 then A else B
"""

// Sweep: overloaded member with unit parameter (FS0193) — #19596
// Roundtrip fails: member M(()) generates sig 'member M: unit -> unit' but
// conformance checker can't match it when M is overloaded. The sig syntax
// is correct but the conformance check for unit-parameter overloads is broken.
[<Fact(Skip = "Sig conformance: member M(()) vs member M: unit -> unit fails when overloaded - FS0193")>]
// Testing both directions and consumer access to understand conformance boundaries.

// Direction 1: handwritten sig says "member M: unit -> unit", impl has "member M(()) = ()"
[<Fact>]
let ``Unit param overload - sig with consumer compiles`` () =
let sigSource = """
module Lib

type R1 =
{ f1: int }

type D =
new: unit -> D
member M: unit -> unit
member M: y: R1 -> unit
member N: unit
"""
let implSource = """
module Lib
type R1 = { f1 : int }
type D() =
member x.N = x.M { f1 = 3 }
member x.M((y: R1)) = ()
member x.M(()) = ()
"""
let consumerSource = """
module Consumer
open Lib
let test() =
let d = D()
d.M(())
d.M { f1 = 42 }
d.N
"""
Fsi sigSource
|> withAdditionalSourceFile (FsSourceWithFileName "Lib.fs" implSource)
|> withAdditionalSourceFile (FsSourceWithFileName "Consumer.fs" consumerSource)
|> ignoreWarnings
|> compile
|> shouldSucceed
|> ignore

// Direction 2: impl without explicit unit parens "member x.M() = ()"
[<Fact>]
let ``Unit param overload - non-paren impl with sig and consumer`` () =
let sigSource = """
module Lib

type R1 =
{ f1: int }

type D =
new: unit -> D
member M: unit -> unit
member M: y: R1 -> unit
member N: unit
"""
let implSource = """
module Lib
type R1 = { f1 : int }
type D() =
member x.N = x.M { f1 = 3 }
member x.M((y: R1)) = ()
member x.M() = ()
"""
let consumerSource = """
module Consumer
open Lib
let test() =
let d = D()
d.M()
d.M { f1 = 42 }
d.N
"""
Fsi sigSource
|> withAdditionalSourceFile (FsSourceWithFileName "Lib.fs" implSource)
|> withAdditionalSourceFile (FsSourceWithFileName "Consumer.fs" consumerSource)
|> ignoreWarnings
|> compile
|> shouldSucceed
|> ignore

// Roundtrip: generated sig from impl, then compile sig+impl+consumer
[<Fact>]
let ``Sweep - overloaded member with unit param roundtrips`` () =
assertSignatureRoundtrip """
module Repro
Expand All @@ -774,4 +854,155 @@ type D() =
member x.N = x.M { f1 = 3 }
member x.M((y: R1)) = ()
member x.M(()) = ()
"""
"""

// Inverse direction 1: impl M(()) but consumer calls d.M() (no parens) — must fail with FS0503
[<Fact>]
let ``Unit param overload - consumer cannot call M() when impl is M(()) with overloads`` () =
let sigSource = """
module Lib

type R1 =
{ f1: int }

type D =
new: unit -> D
member M: unit -> unit
member M: y: R1 -> unit
member N: unit
"""
let implSource = """
module Lib
type R1 = { f1 : int }
type D() =
member x.N = x.M { f1 = 3 }
member x.M((y: R1)) = ()
member x.M(()) = ()
"""
let consumerSource = """
module Consumer
open Lib
let test() =
let d = D()
d.M()
d.M { f1 = 42 }
d.N
"""
Fsi sigSource
|> withAdditionalSourceFile (FsSourceWithFileName "Lib.fs" implSource)
|> withAdditionalSourceFile (FsSourceWithFileName "Consumer.fs" consumerSource)
|> ignoreWarnings
|> compile
|> shouldFail
|> withErrorCode 503
|> ignore

// Inverse direction 2: impl M() (no explicit unit), sig M: unit -> unit, consumer calls d.M()
[<Fact>]
let ``Unit param overload - consumer calls M() with normal impl and sig`` () =
let sigSource = """
module Lib

type R1 =
{ f1: int }

type D =
new: unit -> D
member M: unit -> unit
member M: y: R1 -> unit
member N: unit
"""
let implSource = """
module Lib
type R1 = { f1 : int }
type D() =
member x.N = x.M { f1 = 3 }
member x.M((y: R1)) = ()
member x.M() = ()
"""
let consumerSource = """
module Consumer
open Lib
let test() =
let d = D()
d.M()
d.M { f1 = 42 }
d.N
"""
Fsi sigSource
|> withAdditionalSourceFile (FsSourceWithFileName "Lib.fs" implSource)
|> withAdditionalSourceFile (FsSourceWithFileName "Consumer.fs" consumerSource)
|> ignoreWarnings
|> compile
|> shouldSucceed
|> ignore

// Non-overloaded: member M(()) with sig member M: unit -> unit (no other overloads)
[<Fact>]
let ``Unit param - non-overloaded member M(()) conforms to sig member M: unit -> unit`` () =
let sigSource = """
module Lib

type D =
new: unit -> D
member M: unit -> unit
"""
let implSource = """
module Lib
type D() =
member _.M(()) = ()
"""
Fsi sigSource
|> withAdditionalSourceFile (FsSourceWithFileName "Lib.fs" implSource)
|> ignoreWarnings
|> compile
|> shouldSucceed
|> ignore

// Non-overloaded: member M(()) with sig member M: unit -> unit, consumer calls d.M()
// Verifies that synthesized ArgReprInfo from sig allows consumer access
[<Fact>]
let ``Unit param - non-overloaded member M(()) consumer can call M()`` () =
let sigSource = """
module Lib

type D =
new: unit -> D
member M: unit -> unit
"""
let implSource = """
module Lib
type D() =
member _.M(()) = ()
"""
let consumerSource = """
module Consumer
open Lib
let test() =
let d = D()
d.M()
"""
Fsi sigSource
|> withAdditionalSourceFile (FsSourceWithFileName "Lib.fs" implSource)
|> withAdditionalSourceFile (FsSourceWithFileName "Consumer.fs" consumerSource)
|> ignoreWarnings
|> compile
|> shouldSucceed
|> ignore

// Verify M(()) and M() produce identical IL method signatures
[<Fact>]
let ``Unit param - M(()) and M() produce same IL method signature`` () =
FSharp """
module Test
type D() =
member x.M(()) = 1
member x.M(y: int) = y
"""
|> compile
|> shouldSucceed
|> verifyILContains [
".method public hidebysig instance int32 M() cil managed"
".method public hidebysig instance int32 M(int32 y) cil managed"
]
|> ignore
Loading