Skip to content

Commit 0f2bc13

Browse files
dsymecartermp
andauthored
[RFC FS-1075] Improve interop to C# nullable-typed optionals (#7989)
* add test cases we need to make work * cleanup method call argument processing * update for new cases * update tests * compat Nullable allowed * reduce diff * fix mistake with byrefs * update error message * trigger CI * add version tests * mnimize diff * mnimize diff * remove assert * minor updates for code review * fix overloading change of behaviour Co-authored-by: Phillip Carter <[email protected]>
1 parent 3af8959 commit 0f2bc13

File tree

18 files changed

+1005
-438
lines changed

18 files changed

+1005
-438
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,4 @@ If you're curious about F# itself, check out these links:
115115
* [Get started with F#](https://docs.microsoft.com/dotnet/fsharp/get-started/)
116116
* [F# Software Foundation](https://fsharp.org)
117117
* [F# Testimonials](https://fsharp.org/testimonials)
118+

src/fsharp/ConstraintSolver.fs

+43-10
Original file line numberDiff line numberDiff line change
@@ -2289,14 +2289,15 @@ and ArgsMustSubsumeOrConvert
22892289
trace
22902290
cxsln
22912291
isConstraint
2292+
enforceNullableOptionalsKnownTypes // use known types from nullable optional args?
22922293
(calledArg: CalledArg)
22932294
(callerArg: CallerArg<'T>) = trackErrors {
22942295

22952296
let g = csenv.g
22962297
let m = callerArg.Range
2297-
let calledArgTy = AdjustCalledArgType csenv.InfoReader isConstraint calledArg callerArg
2298-
do! SolveTypeSubsumesTypeWithReport csenv ndeep m trace cxsln calledArgTy callerArg.Type
2299-
if calledArg.IsParamArray && isArray1DTy g calledArgTy && not (isArray1DTy g callerArg.Type) then
2298+
let calledArgTy = AdjustCalledArgType csenv.InfoReader isConstraint enforceNullableOptionalsKnownTypes calledArg callerArg
2299+
do! SolveTypeSubsumesTypeWithReport csenv ndeep m trace cxsln calledArgTy callerArg.CallerArgumentType
2300+
if calledArg.IsParamArray && isArray1DTy g calledArgTy && not (isArray1DTy g callerArg.CallerArgumentType) then
23002301
return! ErrorD(Error(FSComp.SR.csMethodExpectsParams(), m))
23012302
else ()
23022303
}
@@ -2308,14 +2309,14 @@ and MustUnifyInsideUndo csenv ndeep trace cxsln ty1 ty2 =
23082309
SolveTypeEqualsTypeWithReport csenv ndeep csenv.m (WithTrace trace) cxsln ty1 ty2
23092310

23102311
and ArgsMustSubsumeOrConvertInsideUndo (csenv: ConstraintSolverEnv) ndeep trace cxsln isConstraint calledArg (CallerArg(callerArgTy, m, _, _) as callerArg) =
2311-
let calledArgTy = AdjustCalledArgType csenv.InfoReader isConstraint calledArg callerArg
2312+
let calledArgTy = AdjustCalledArgType csenv.InfoReader isConstraint true calledArg callerArg
23122313
SolveTypeSubsumesTypeWithReport csenv ndeep m (WithTrace trace) cxsln calledArgTy callerArgTy
23132314

23142315
and TypesMustSubsumeOrConvertInsideUndo (csenv: ConstraintSolverEnv) ndeep trace cxsln m calledArgTy callerArgTy =
23152316
SolveTypeSubsumesTypeWithReport csenv ndeep m trace cxsln calledArgTy callerArgTy
23162317

23172318
and ArgsEquivInsideUndo (csenv: ConstraintSolverEnv) isConstraint calledArg (CallerArg(callerArgTy, m, _, _) as callerArg) =
2318-
let calledArgTy = AdjustCalledArgType csenv.InfoReader isConstraint calledArg callerArg
2319+
let calledArgTy = AdjustCalledArgType csenv.InfoReader isConstraint true calledArg callerArg
23192320
if typeEquiv csenv.g calledArgTy callerArgTy then CompleteD else ErrorD(Error(FSComp.SR.csArgumentTypesDoNotMatch(), m))
23202321

23212322
and ReportNoCandidatesError (csenv: ConstraintSolverEnv) (nUnnamedCallerArgs, nNamedCallerArgs) methodName ad (calledMethGroup: CalledMeth<_> list) isSequential =
@@ -2593,11 +2594,18 @@ and ResolveOverloading
25932594
| _ when isInByrefTy csenv.g ty2 && typeEquiv csenv.g ty1 (destByrefTy csenv.g ty2) ->
25942595
true
25952596

2597+
// T is always better than Nullable<T> from F# 5.0 onwards
2598+
| _ when g.langVersion.SupportsFeature(Features.LanguageFeature.NullableOptionalInterop) &&
2599+
isNullableTy csenv.g ty2 &&
2600+
typeEquiv csenv.g ty1 (destNullableTy csenv.g ty2) ->
2601+
true
2602+
25962603
| _ -> false)
25972604

25982605
if c <> 0 then c else
25992606
0
26002607

2608+
/// Check whether one overload is better than another
26012609
let better (candidate: CalledMeth<_>, candidateWarnings, _) (other: CalledMeth<_>, otherWarnings, _) =
26022610
let candidateWarnCount = List.length candidateWarnings
26032611
let otherWarnCount = List.length otherWarnings
@@ -2614,7 +2622,7 @@ and ResolveOverloading
26142622
// Prefer methods with more precise param array arg type
26152623
let c =
26162624
if candidate.UsesParamArrayConversion && other.UsesParamArrayConversion then
2617-
compareTypes candidate.ParamArrayElementType other.ParamArrayElementType
2625+
compareTypes (candidate.GetParamArrayElementType()) (other.GetParamArrayElementType())
26182626
else
26192627
0
26202628
if c <> 0 then c else
@@ -2629,7 +2637,7 @@ and ResolveOverloading
26292637
let c = compare (not candidate.HasOptArgs) (not other.HasOptArgs)
26302638
if c <> 0 then c else
26312639

2632-
// check regular args. The argument counts will only be different if one is using param args
2640+
// check regular unnamed args. The argument counts will only be different if one is using param args
26332641
let c =
26342642
if candidate.TotalNumUnnamedCalledArgs = other.TotalNumUnnamedCalledArgs then
26352643
// For extension members, we also include the object argument type, if any in the comparison set
@@ -2670,12 +2678,37 @@ and ResolveOverloading
26702678
0
26712679
if c <> 0 then c else
26722680

2673-
26742681
// Prefer non-generic methods
26752682
// Note: Relies on 'compare' respecting true > false
26762683
let c = compare candidate.CalledTyArgs.IsEmpty other.CalledTyArgs.IsEmpty
26772684
if c <> 0 then c else
26782685

2686+
// F# 5.0 rule - prior to F# 5.0 named arguments (on the caller side) were not being taken
2687+
// into account when comparing overloads. So adding a name to an argument might mean
2688+
// overloads ould no longer be distinguished. We thus look at *all* arguments (whether
2689+
// optional or not) as an additional comparison technique.
2690+
let c =
2691+
if g.langVersion.SupportsFeature(Features.LanguageFeature.NullableOptionalInterop) then
2692+
let cs =
2693+
let args1 = candidate.AllCalledArgs |> List.concat
2694+
let args2 = other.AllCalledArgs |> List.concat
2695+
if args1.Length = args2.Length then
2696+
(args1, args2) ||> List.map2 compareArg
2697+
else
2698+
[]
2699+
// "all args are at least as good, and one argument is actually better"
2700+
if cs |> List.forall (fun x -> x >= 0) && cs |> List.exists (fun x -> x > 0) then
2701+
1
2702+
// "all args are at least as bad, and one argument is actually worse"
2703+
elif cs |> List.forall (fun x -> x <= 0) && cs |> List.exists (fun x -> x < 0) then
2704+
-1
2705+
// "argument lists are incomparable"
2706+
else
2707+
0
2708+
else
2709+
0
2710+
if c <> 0 then c else
2711+
26792712
0
26802713

26812714

@@ -2734,7 +2767,7 @@ and ResolveOverloading
27342767
true
27352768
(MustUnify csenv ndeep trace cxsln)
27362769
(TypesMustSubsumeOrConvertInsideUndo csenv ndeep trace cxsln m)// REVIEW: this should not be an "InsideUndo" operation
2737-
(ArgsMustSubsumeOrConvert csenv ndeep trace cxsln cx.IsSome)
2770+
(ArgsMustSubsumeOrConvert csenv ndeep trace cxsln cx.IsSome true)
27382771
reqdRetTyOpt
27392772
calledMeth
27402773
| WithTrace calledMethTrc ->
@@ -2787,7 +2820,7 @@ let UnifyUniqueOverloading
27872820
true // always check return type
27882821
(MustUnify csenv ndeep NoTrace None)
27892822
(TypesMustSubsumeOrConvertInsideUndo csenv ndeep NoTrace None m)
2790-
(ArgsMustSubsumeOrConvert csenv ndeep NoTrace None false) // UnifyUniqueOverloading is not called in case of trait call - pass isConstraint=false
2823+
(ArgsMustSubsumeOrConvert csenv ndeep NoTrace None false false) // UnifyUniqueOverloading is not called in case of trait call - pass isConstraint=false
27912824
(Some reqdRetTy)
27922825
calledMeth
27932826
return true

src/fsharp/FSharp.Core/option.fsi

-1
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,6 @@ module Option =
173173
[<CompiledName("ToList")>]
174174
val toList: option:'T option -> 'T list
175175

176-
177176
/// <summary>Convert the option to a Nullable value.</summary>
178177
/// <param name="option">The input option.</param>
179178
/// <returns>The result value.</returns>

src/fsharp/LanguageFeatures.fs

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type LanguageFeature =
2929
| FromEndSlicing
3030
| FixedIndexSlice3d4d
3131
| AndBang
32+
| NullableOptionalInterop
3233

3334
/// LanguageVersion management
3435
type LanguageVersion (specifiedVersionAsString) =
@@ -63,6 +64,7 @@ type LanguageVersion (specifiedVersionAsString) =
6364
LanguageFeature.OpenStaticClasses, previewVersion
6465
LanguageFeature.PackageManagement, previewVersion
6566
LanguageFeature.AndBang, previewVersion
67+
LanguageFeature.NullableOptionalInterop, previewVersion
6668
]
6769

6870
let specified =

src/fsharp/LanguageFeatures.fsi

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type LanguageFeature =
1717
| FromEndSlicing
1818
| FixedIndexSlice3d4d
1919
| AndBang
20+
| NullableOptionalInterop
2021

2122
/// LanguageVersion management
2223
type LanguageVersion =

0 commit comments

Comments
 (0)