Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"System.Func overload-is-better": checking if anything breaks, or not (in which case, we'd like a test) #11538

Closed
25 changes: 15 additions & 10 deletions src/fsharp/CompilerDiagnostics.fs
Original file line number Diff line number Diff line change
Expand Up @@ -195,16 +195,17 @@ let GetRangeOfDiagnostic(err: PhasedDiagnostic) =
| DeprecatedCommandLineOptionNoDescription(_, m)
| InternalCommandLineOption(_, m)
| HashIncludeNotAllowedInNonScript m
| HashReferenceNotAllowedInNonScript m
| HashDirectiveNotAllowedInNonScript m
| FileNameNotResolved(_, _, m)
| LoadedSourceNotFoundIgnoring(_, m)
| MSBuildReferenceResolutionWarning(_, _, m)
| MSBuildReferenceResolutionError(_, _, m)
| AssemblyNotResolved(_, m)
| HashLoadedSourceHasIssues(_, _, m)
| HashLoadedScriptConsideredSource m ->
Some m
| HashReferenceNotAllowedInNonScript m
| HashDirectiveNotAllowedInNonScript m
| FileNameNotResolved(_, _, m)
| LoadedSourceNotFoundIgnoring(_, m)
| MSBuildReferenceResolutionWarning(_, _, m)
| MSBuildReferenceResolutionError(_, _, m)
| AssemblyNotResolved(_, m)
| HashLoadedSourceHasIssues(_, _, m)
| HashLoadedScriptConsideredSource m
| FuncIsBetterOverloadResolutionDeprecation(m=m) ->
Some m
// Strip TargetInvocationException wrappers
| :? System.Reflection.TargetInvocationException as e ->
RangeFromException e.InnerException
Expand Down Expand Up @@ -1378,6 +1379,10 @@ let OutputPhasedErrorR (os: StringBuilder) (err: PhasedDiagnostic) (canSuggestNa
let errorText = FunctionValueUnexpectedE().Format (NicePrint.stringOfTy denv ty)
os.Append errorText |> ignore

| FuncIsBetterOverloadResolutionDeprecation (method=method) ->
let _,errorText = FSComp.SR.funcIsBetterOverloadResolutionDeprecation(method.DisplayName)
os.Append errorText |> ignore

| UnitTypeExpected (denv, ty, _) ->
let ty, _cxs = PrettyTypes.PrettifyType denv.g ty
let warningText = UnitTypeExpectedE().Format (NicePrint.stringOfTy denv ty)
Expand Down
31 changes: 23 additions & 8 deletions src/fsharp/ConstraintSolver.fs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ exception UnresolvedOverloading of displayEnv: DisplayEnv * callerArgs: CallerAr

exception UnresolvedConversionOperator of displayEnv: DisplayEnv * TType * TType * range

exception FuncIsBetterOverloadResolutionDeprecation of displayEnv: DisplayEnv * method: MethInfo * m:range

type TcValF = (ValRef -> ValUseFlag -> TType list -> range -> Expr * TType)

type ConstraintSolverState =
Expand Down Expand Up @@ -2643,9 +2645,10 @@ and ResolveOverloading
/// Compare types under the feasibly-subsumes ordering
let compareTypes ty1 ty2 =
(ty1, ty2) ||> compareCond (fun x1 x2 -> TypeFeasiblySubsumesType ndeep csenv.g csenv.amap m x2 CanCoerce x1)


let mutable funcIsBetterSpecialCasingArgPosition = ValueNone
/// Compare arguments under the feasibly-subsumes ordering and the adhoc Func-is-better-than-other-delegates rule
let compareArg (calledArg1: CalledArg) (calledArg2: CalledArg) =
let compareArg argIndex (calledArg1: CalledArg) (calledArg2: CalledArg) =
let c = compareTypes calledArg1.CalledArgumentType calledArg2.CalledArgumentType
if c <> 0 then c else

Expand All @@ -2658,8 +2661,9 @@ and ResolveOverloading
tcref1.DisplayName = "Func" &&
(match tcref1.PublicPath with Some p -> p.EnclosingPath = [| "System" |] | _ -> false) &&
isDelegateTy g ty1 &&
isDelegateTy g ty2 -> true

isDelegateTy g ty2 ->
funcIsBetterSpecialCasingArgPosition <- ValueSome (argIndex,calledArg1)
true
// T is always better than inref<T>
| _ when isInByrefTy csenv.g ty2 && typeEquiv csenv.g ty1 (destByrefTy csenv.g ty2) ->
true
Expand Down Expand Up @@ -2722,7 +2726,7 @@ and ResolveOverloading
[]
else
[]) @
((candidate.AllUnnamedCalledArgs, other.AllUnnamedCalledArgs) ||> List.map2 compareArg)
((candidate.AllUnnamedCalledArgs, other.AllUnnamedCalledArgs) ||> List.mapi2 compareArg)
// "all args are at least as good, and one argument is actually better"
if cs |> List.forall (fun x -> x >= 0) && cs |> List.exists (fun x -> x > 0) then
1
Expand Down Expand Up @@ -2755,15 +2759,15 @@ and ResolveOverloading

// F# 5.0 rule - prior to F# 5.0 named arguments (on the caller side) were not being taken
// into account when comparing overloads. So adding a name to an argument might mean
// overloads ould no longer be distinguished. We thus look at *all* arguments (whether
// overloads could no longer be distinguished. We thus look at *all* arguments (whether
// optional or not) as an additional comparison technique.
let c =
if g.langVersion.SupportsFeature(Features.LanguageFeature.NullableOptionalInterop) then
let cs =
let args1 = candidate.AllCalledArgs |> List.concat
let args2 = other.AllCalledArgs |> List.concat
if args1.Length = args2.Length then
(args1, args2) ||> List.map2 compareArg
(args1, args2) ||> List.mapi2 compareArg
else
[]
// "all args are at least as good, and one argument is actually better"
Expand Down Expand Up @@ -2794,7 +2798,18 @@ and ResolveOverloading
else
None)
match bestMethods with
| [(calledMeth, warns, t)] -> Some calledMeth, OkResult (warns, ()), WithTrace t
| [(calledMeth, warns, t)] ->
let calledMeth = calledMeth
match funcIsBetterSpecialCasingArgPosition with
| ValueNone -> ()
| ValueSome (argIndex, _calledArg) ->
let callerArg = calledMeth.GetCallerArgAt argIndex
match callerArg.Expr with
| Expr.Lambda _ ->
// https://github.com/dotnet/fsharp/issues/11534
warning (FuncIsBetterOverloadResolutionDeprecation(denv,calledMeth.Method,m))
| _ -> ()
Some calledMeth, OkResult (warns, ()), WithTrace t
| bestMethods ->
let methods =
let getMethodSlotsAndErrors methodSlot errors =
Expand Down
1 change: 1 addition & 0 deletions src/fsharp/ConstraintSolver.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ exception ErrorsFromAddingSubsumptionConstraint of tcGlobals: TcGlobals * displa
exception ErrorFromAddingConstraint of displayEnv: DisplayEnv * exn * range
exception UnresolvedConversionOperator of displayEnv: DisplayEnv * TType * TType * range
exception UnresolvedOverloading of displayEnv: DisplayEnv * callerArgs: CallerArgs<Expr> * failure: OverloadResolutionFailure * range
exception FuncIsBetterOverloadResolutionDeprecation of displayEnv:DisplayEnv * method: MethInfo * m:range
exception NonRigidTypar of displayEnv: DisplayEnv * string option * range * TType * TType * range

exception ArgDoesNotMatchError of error: ErrorsFromAddingSubsumptionConstraint * calledMeth: CalledMeth<Expr> * calledArg: CalledArg * callerArg: CallerArg<Expr>
Expand Down
3 changes: 2 additions & 1 deletion src/fsharp/FSComp.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1563,4 +1563,5 @@ forFormatInvalidForInterpolated4,"Interpolated strings used as type IFormattable
3390,xmlDocMissingParameter,"This XML comment is incomplete: no documentation for parameter '%s'"
3391,tcLiteralAttributeCannotUseActivePattern,"A [<Literal>] declaration cannot use an active pattern for its identifier"
3392,containerDeprecated,"The 'AssemblyKeyNameAttribute' has been deprecated. Use 'AssemblyKeyFileAttribute' instead."
3393,containerSigningUnsupportedOnThisPlatform,"Key container signing is not supported on this platform."
3393,containerSigningUnsupportedOnThisPlatform,"Key container signing is not supported on this platform."
3394,funcIsBetterOverloadResolutionDeprecation,"The method '%s' has overload with 'System.Func' delegate in the signature, to fix the warning, you need to specify the type explicitly."
23 changes: 23 additions & 0 deletions src/fsharp/MethodCalls.fs
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,29 @@ type CalledMeth<'T>
/// The argument analysis for each set of curried arguments
member x.ArgSets = argSets

member x.GetCallerArgAt index =
// Arrays are better...
let mutable currentIndex = 0
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be looking at reimplementing this in terms of List.mapi2, this has been first try to make things work (it passes the test, but the test likely doesn't covers all possible cases).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is problematic, I wrote it before surveying some more my assumptions (in #11544, which shows that my assumptions are just wrong).

Looking for guidance on how we want that to work to get this PR considered for merge.

Also eager to hear if there is a point or no, to move from list list representation of SynValInfo, I know @baronfel was facing slightly related question recently:

anyone have any insight into why a named tuple-parameter is erased in the TAST? eg
let myFun (f: (int * int * int)) = ()
in the TAST is represented as an FSharpMemberOrFunctionOrValue, whose CurriedParameterArgs is a list< list< FSharpParameter > > of length 1, whose inner list is of length 3. The three inner parameters are unnamed, but as a result of this form I lose information (as an IDE user) about the name/documentation of the parameter.

which shows that the representation as we have it, from the parser AST down to the typed AST is a bit arcane to the people not used to it.

let mutable result = ValueNone
for argSet in argSets do
for arg in argSet.UnnamedCallerArgs do
if currentIndex = index then
result <- ValueSome arg
currentIndex <- currentIndex + 1
if ValueOption.isSome result then () else
for arg in argSet.AssignedNamedArgs do
if currentIndex = index then
result <- ValueSome arg.CallerArg
currentIndex <- currentIndex + 1
if ValueOption.isSome result then () else
for arg in argSet.ParamArrayCallerArgs do
if currentIndex = index then
result <- ValueSome arg
currentIndex <- currentIndex + 1

match result with
| ValueNone -> failwithf $"{nameof CalledMeth}.{nameof x.GetCallerArgAt}: wrong index requested %A{argSets}"
| ValueSome callerArg -> callerArg
/// The return type after implicit deference of byref returns is taken into account
member x.CalledReturnTypeAfterByrefDeref =
let retTy = methodRetTy
Expand Down
3 changes: 3 additions & 0 deletions src/fsharp/MethodCalls.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ type CalledMeth<'T> =
member amap: ImportMap
member infoReader: InfoReader

/// Get the CallerArg at index
member GetCallerArgAt: index: int -> CallerArg<'T>

val NamesOfCalledArgs: calledArgs:CalledArg list -> Ident list

type ArgumentAnalysis =
Expand Down
3 changes: 3 additions & 0 deletions tests/fsharp/tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3064,6 +3064,9 @@ module OverloadResolution =
let [<Test>] ``neg_System.Threading.Tasks.Task.Run.OverloadList``() = singleNegTest (testConfig "typecheck/overloads") "neg_System.Threading.Tasks.Task.Run.OverloadList"
let [<Test>] ``neg_System.Drawing.Graphics.DrawRectangleOverloadList.fsx``() = singleNegTest (testConfig "typecheck/overloads") "neg_System.Drawing.Graphics.DrawRectangleOverloadList"

module ``ad hoc code overload error messages or not``=
let [<Test>] ``neg_at_somepoint_in_the_future_func-is-better-or-not.fsx`` () = singleNegTest (testConfig "typecheck/overloads") "neg_at_somepoint_in_the_future_func-is-better-or-not"

module ``ad hoc code overload error messages``=
let [<Test>] ``neg_many_many_overloads`` () = singleNegTest (testConfig "typecheck/overloads") "neg_many_many_overloads"
let [<Test>] ``neg_interface_generics`` () = singleNegTest (testConfig "typecheck/overloads") "neg_interface_generics"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

neg_at_somepoint_in_the_future_func-is-better-or-not.fsx(14,1,14,17): typecheck error FS0193: The method 'M' has overload with 'System.Func' delegate in the signature, to fix the warning, you need to specify the type explicitly.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrt to the warning message itself:

  • there may be potential to show the CallerArg.Expr expression, if there are existing facilities to prettify it
  • it might be worth to mention which argument (by the name of it from CalledArg) is causing the warning

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifying the argument name may be problematic in circumstances similar to what @baronfel was dealing with, patterns and "anonymous" parameters.

If there are places we already prettify Expr to display to the end user, please let me know and I'll try to experiment with it.

Probably several error messages could benefit from having this kind of "expanding the expression to explain the error in actual case".


neg_at_somepoint_in_the_future_func-is-better-or-not.fsx(16,1,16,19): typecheck error FS0193: The method 'M2' has overload with 'System.Func' delegate in the signature, to fix the warning, you need to specify the type explicitly.

neg_at_somepoint_in_the_future_func-is-better-or-not.fsx(25,1,25,37): typecheck error FS0041: A unique overload for method 'M3' could not be determined based on type information prior to this program point. A type annotation may be needed.

Known types of arguments: (unit -> unit) * (unit -> unit)

Candidates:
- member C.M3 : [<ParamArray>] funcs:Action array -> unit
- member C.M3 : [<ParamArray>] funcs:Func<unit,unit> array -> unit

neg_at_somepoint_in_the_future_func-is-better-or-not.fsx(38,5,38,63): typecheck error FS0193: The method 'Min' has overload with 'System.Func' delegate in the signature, to fix the warning, you need to specify the type explicitly.

neg_at_somepoint_in_the_future_func-is-better-or-not.fsx(40,5,40,74): typecheck error FS0193: The method 'Min' has overload with 'System.Func' delegate in the signature, to fix the warning, you need to specify the type explicitly.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
open System

type C() =
member _.M(a: Action<int>) = printfn "M(action)"
member _.M(f: Func<int, unit>) = printfn "M(func)"
member _.M2(f: Func<unit, unit>) = printfn "M2(func())"
member _.M2(f: Action) = printfn "M2(action())"
member _.M3([<ParamArray>] funcs: Func<unit,unit> array) = printfn "M3(funcs)"
member _.M3([<ParamArray>] funcs: Action array) = printfn "M3(actions)"
let c = C()

// https://github.com/dotnet/fsharp/issues/11534
// we may want a warning here, or maybe an error proper at some point
c.M(fun _ -> ())

c.M2(fun () -> ())

c.M3(Action(fun () -> ()), (fun () -> ()))
c.M3(Action id, id, id, id)
c.M3(Func<_,_> id, id, id, id)
c.M3(Func<_,_>(fun () -> ()), (fun () -> ()))
c.M3((fun () -> ()), Func<_,_>(fun () -> ()))

// this one doesn't resolve, although without the param array, it would
c.M3((fun () -> ()), (fun () -> ()))


open System.Collections.Generic
open System.Linq

// here we verify different style of calling BCL overloaded methods
type Query =
static member MinBy_ExplicitDelegate<'T, 'Q, 'Key when 'Key: equality and 'Key: comparison> (source: IEnumerable<'T>, valueSelector: 'T -> 'Key) =
Enumerable.Min(source, Func<'T, 'Key>(valueSelector))
static member MinBy_ExplicitDelegateWithArgName<'T, 'Q, 'Key when 'Key: equality and 'Key: comparison> (source: IEnumerable<'T>, valueSelector: 'T -> 'Key) =
Enumerable.Min(source, selector = Func<'T, 'Key>(valueSelector))
static member MinBy_Lambda<'T, 'Q, 'Key when 'Key: equality and 'Key: comparison> (source: IEnumerable<'T>, valueSelector: 'T -> 'Key) =
Enumerable.Min(source, (fun (t: 'T) -> (valueSelector t)))
static member MinBy_LambdaWithArgName<'T, 'Q, 'Key when 'Key: equality and 'Key: comparison> (source: IEnumerable<'T>, valueSelector: 'T -> 'Key) =
Enumerable.Min(source, selector = (fun (t: 'T) -> (valueSelector t)))