diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index d58cb9ebab..17d233e34b 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -39,6 +39,7 @@ * Fix signature generation: `namespace global` header dropped from generated signature. ([Issue #19593](https://github.com/dotnet/fsharp/issues/19593), [PR #19609](https://github.com/dotnet/fsharp/pull/19609)) * Fix signature generation: SRTP constraints use postfix syntax that fails conformance, now uses explicit type param declarations. ([Issue #19594](https://github.com/dotnet/fsharp/issues/19594), [PR #19609](https://github.com/dotnet/fsharp/pull/19609)) * Fix signature generation: type params with special characters missing backtick escaping. ([Issue #19595](https://github.com/dotnet/fsharp/issues/19595), [PR #19609](https://github.com/dotnet/fsharp/pull/19609)) +* 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 diff --git a/src/Compiler/Checking/NicePrint.fs b/src/Compiler/Checking/NicePrint.fs index bbfb587911..2964163301 100644 --- a/src/Compiler/Checking/NicePrint.fs +++ b/src/Compiler/Checking/NicePrint.fs @@ -1503,6 +1503,15 @@ module PrintTastMemberOrVals = let layoutNonMemberVal denv (tps, v: Val, tau, cxs) = let env = SimplifyTypes.CollectInfo true [tau] cxs let cxs = env.postfixConstraints + let hasStaticallyResolvedTypars = + tps |> List.exists (fun (tp: Typar) -> tp.StaticReq = TyparStaticReq.HeadType) && + not (IsLogicalOpName v.LogicalName) + // When SRTP typars are shown on explicit type param declarations, exclude + // their constraints from the postfix to avoid duplicated "when" clauses. + let cxs = + if hasStaticallyResolvedTypars then + cxs |> List.filter (fun (tp, _) -> tp.StaticReq <> TyparStaticReq.HeadType) + else cxs let valReprInfo = arityOfValForDisplay v let argInfos, retTy = GetTopTauTypeInFSharpForm denv.g valReprInfo.ArgInfos tau v.Range let nameL = diff --git a/src/Compiler/Checking/SignatureConformance.fs b/src/Compiler/Checking/SignatureConformance.fs index 5c947bda13..9bb322f62d 100644 --- a/src/Compiler/Checking/SignatureConformance.fs +++ b/src/Compiler/Checking/SignatureConformance.fs @@ -295,11 +295,24 @@ 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 + || (List.length implGroup = 0 && List.length sigGroup = 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 [] and sig has [unit_arg], use min to avoid taking more than available + let implArgInfos = (implArgInfos, sigArgInfos) ||> List.map2 (fun l1 l2 -> l1 |> List.take (min l1.Length l2.Length)) // Propagate some information signature to implementation. // Check the attributes on each argument, and update the ValReprInfo for @@ -307,7 +320,11 @@ type Checker(g, amap, denv, remapInfo: SignatureRepackageInfo, checkingSig) = // 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) -> + // When impl group is empty (unit param like member M(())), skip arg-level checks + if implGroup.IsEmpty then true + else + (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 -> @@ -661,7 +678,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) (implModType.AllValsAndMembersByLogicalNameUncached, signModType.AllValsAndMembersByLogicalNameUncached) ||> NameMap.suball2 @@ -683,9 +700,29 @@ 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 -> System.Object.ReferenceEquals(fv, fv2)) matchedFvs)) + let unmatchedAvs = avs |> List.filter (fun av -> not (List.exists (fun av2 -> System.Object.ReferenceEquals(av, av2)) matchedAvs)) + let relaxedPairs = + unmatchedFvs |> List.choose (fun fv -> + let fkey = fv.GetLinkagePartialKey() + match unmatchedAvs |> List.tryFind (fun av -> + let akey = av.GetLinkagePartialKey() + akey.MemberParentMangledName = fkey.MemberParentMangledName && + akey.LogicalName = fkey.LogicalName && + av.IsMember && fv.IsMember && + typeAEquivAux EraseAll g aenv av.Type fv.Type) with + | None -> None + | Some av -> Some(fv, 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 = diff --git a/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs b/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs index d4b4815e08..61f69ce2db 100644 --- a/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Signatures/TypeTests.fs @@ -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. -[ 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(()) = ()" +[] +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() = ()" +[] +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 +[] let ``Sweep - overloaded member with unit param roundtrips`` () = assertSignatureRoundtrip """ module Repro @@ -774,4 +854,102 @@ type D() = member x.N = x.M { f1 = 3 } member x.M((y: R1)) = () member x.M(()) = () -""" \ No newline at end of file +""" + +// Inverse direction 1: impl M(()) but consumer calls d.M() (no parens) — must fail with FS0503 +[] +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() +[] +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 + +// Verify M(()) and M() produce identical IL method signatures +[] +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