Skip to content

Commit a72457e

Browse files
authored
Feature nullness :: Try infer without null even when function/method arg is marked as nullable (#17269)
1 parent 3603443 commit a72457e

File tree

9 files changed

+128
-23
lines changed

9 files changed

+128
-23
lines changed

src/Compiler/Checking/CheckExpressions.fs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,8 +483,9 @@ let UnifyOverallType (cenv: cenv) (env: TcEnv) m overallTy actualTy =
483483
match overallTy with
484484
| MustConvertTo(isMethodArg, reqdTy) when g.langVersion.SupportsFeature LanguageFeature.AdditionalTypeDirectedConversions ->
485485
let actualTy = tryNormalizeMeasureInType g actualTy
486-
let reqdTy = tryNormalizeMeasureInType g reqdTy
487-
if AddCxTypeEqualsTypeUndoIfFailed env.DisplayEnv cenv.css m reqdTy actualTy then
486+
let reqdTy = tryNormalizeMeasureInType g reqdTy
487+
let reqTyForUnification = reqTyForArgumentNullnessInference g actualTy reqdTy
488+
if AddCxTypeEqualsTypeUndoIfFailed env.DisplayEnv cenv.css m reqTyForUnification actualTy then
488489
()
489490
else
490491
// try adhoc type-directed conversions

src/Compiler/Checking/ConstraintSolver.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,8 @@ and SolveNullnessSubsumesNullness (csenv: ConstraintSolverEnv) m2 (trace: Option
10811081
SolveNullnessSubsumesNullness csenv m2 trace ty1 ty2 nv1.Solution nullness2
10821082
| _, Nullness.Variable nv2 when nv2.IsSolved ->
10831083
SolveNullnessSubsumesNullness csenv m2 trace ty1 ty2 nullness1 nv2.Solution
1084+
| Nullness.Variable _nv1, Nullness.Known NullnessInfo.WithoutNull ->
1085+
CompleteD
10841086
| Nullness.Variable nv1, _ ->
10851087
trace.Exec (fun () -> nv1.Set nullness2) (fun () -> nv1.Unset())
10861088
CompleteD
@@ -1414,6 +1416,8 @@ and SolveFunTypeEqn csenv ndeep m2 trace cxsln domainTy1 domainTy2 rangeTy1 rang
14141416
trackErrors {
14151417
// TODO NULLNESS: consider whether flipping the actual and expected in argument position
14161418
// causes other problems, e.g. better/worse diagnostics
1419+
let g = csenv.g
1420+
let domainTy2 = reqTyForArgumentNullnessInference g domainTy1 domainTy2
14171421
do! SolveTypeEqualsTypeKeepAbbrevsWithCxsln csenv ndeep m2 trace cxsln domainTy2 domainTy1
14181422
return! SolveTypeEqualsTypeKeepAbbrevsWithCxsln csenv ndeep m2 trace cxsln rangeTy1 rangeTy2
14191423
}

src/Compiler/Checking/MethodCalls.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ let MakeCalledArgs amap m (minfo: MethInfo) minst =
480480
IsOutArg=isOutArg
481481
ReflArgInfo=reflArgInfo
482482
NameOpt=nmOpt
483-
CalledArgumentType=calledArgTy })
483+
CalledArgumentType= changeWithNullReqTyToVariable amap.g calledArgTy})
484484

485485
/// <summary>
486486
/// Represents the syntactic matching between a caller of a method and the called method.

src/Compiler/TypedTree/TypedTreeBasics.fs

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -236,18 +236,22 @@ let rec stripUnitEqnsAux canShortcut unt =
236236
| _ -> unt
237237

238238
let combineNullness (nullnessOrig: Nullness) (nullnessNew: Nullness) =
239-
match nullnessOrig.Evaluate() with
240-
| NullnessInfo.WithoutNull -> nullnessNew
241-
| NullnessInfo.AmbivalentToNull ->
242-
match nullnessNew.Evaluate() with
243-
| NullnessInfo.WithoutNull -> nullnessOrig
244-
| NullnessInfo.AmbivalentToNull -> nullnessOrig
245-
| NullnessInfo.WithNull -> nullnessNew
246-
| NullnessInfo.WithNull ->
247-
match nullnessNew.Evaluate() with
248-
| NullnessInfo.WithoutNull -> nullnessOrig
249-
| NullnessInfo.AmbivalentToNull -> nullnessNew
250-
| NullnessInfo.WithNull -> nullnessOrig
239+
match nullnessOrig, nullnessNew with
240+
| Nullness.Variable _, Nullness.Known NullnessInfo.WithoutNull ->
241+
nullnessOrig
242+
| _ ->
243+
match nullnessOrig.Evaluate() with
244+
| NullnessInfo.WithoutNull -> nullnessNew
245+
| NullnessInfo.AmbivalentToNull ->
246+
match nullnessNew.Evaluate() with
247+
| NullnessInfo.WithoutNull -> nullnessOrig
248+
| NullnessInfo.AmbivalentToNull -> nullnessOrig
249+
| NullnessInfo.WithNull -> nullnessNew
250+
| NullnessInfo.WithNull ->
251+
match nullnessNew.Evaluate() with
252+
| NullnessInfo.WithoutNull -> nullnessOrig
253+
| NullnessInfo.AmbivalentToNull -> nullnessNew
254+
| NullnessInfo.WithNull -> nullnessOrig
251255

252256
let nullnessEquiv (nullnessOrig: Nullness) (nullnessNew: Nullness) = LanguagePrimitives.PhysicalEquality nullnessOrig nullnessNew
253257

@@ -278,8 +282,9 @@ let tryAddNullnessToTy nullnessNew (ty:TType) =
278282
| TType_measure _ -> None
279283

280284
let addNullnessToTy (nullness: Nullness) (ty:TType) =
281-
match nullness.Evaluate() with
282-
| NullnessInfo.WithoutNull -> ty
285+
match nullness with
286+
| Nullness.Known NullnessInfo.WithoutNull -> ty
287+
| Nullness.Variable nv when nv.IsSolved && nv.Evaluate() = NullnessInfo.WithoutNull -> ty
283288
| _ ->
284289
match ty with
285290
| TType_var (tp, nullnessOrig) -> TType_var (tp, combineNullness nullnessOrig nullness)

src/Compiler/TypedTree/TypedTreeOps.fs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9157,11 +9157,33 @@ let nullnessOfTy g ty =
91579157
|> function
91589158
| TType_app(tcref, _, nullness) ->
91599159
let nullness2 = intrinsicNullnessOfTyconRef g tcref
9160-
combineNullness nullness nullness2
9160+
if nullness2 === g.knownWithoutNull then
9161+
nullness
9162+
else
9163+
combineNullness nullness nullness2
91619164
| TType_fun (_, _, nullness) | TType_var (_, nullness) ->
91629165
nullness
91639166
| _ -> g.knownWithoutNull
91649167

9168+
let changeWithNullReqTyToVariable g reqTy =
9169+
let sty = stripTyEqns g reqTy
9170+
match isTyparTy g sty with
9171+
| false ->
9172+
match nullnessOfTy g sty with
9173+
| Nullness.Known NullnessInfo.WithNull ->
9174+
reqTy |> replaceNullnessOfTy (NewNullnessVar())
9175+
| _ -> reqTy
9176+
| true -> reqTy
9177+
9178+
/// When calling a null-allowing API, we prefer to infer a without null argument for idiomatic F# code.
9179+
/// That is, unless caller explicitely marks a value (e.g. coming from a function parameter) as WithNull, it should not be infered as such.
9180+
let reqTyForArgumentNullnessInference g actualTy reqTy =
9181+
// Only change reqd nullness if actualTy is an inference variable
9182+
match tryDestTyparTy g actualTy with
9183+
| ValueSome t when t.IsCompilerGenerated && not(t.Constraints |> List.exists(function | TyparConstraint.SupportsNull _ -> true | _ -> false))->
9184+
changeWithNullReqTyToVariable g reqTy
9185+
| _ -> reqTy
9186+
91659187
/// The new logic about whether a type admits the use of 'null' as a value.
91669188
let TypeNullIsExtraValueNew g m ty =
91679189
let sty = stripTyparEqns ty

src/Compiler/TypedTree/TypedTreeOps.fsi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,10 @@ val tryDestForallTy: TcGlobals -> TType -> Typars * TType
649649

650650
val nullnessOfTy: TcGlobals -> TType -> Nullness
651651

652+
val changeWithNullReqTyToVariable: TcGlobals -> reqTy: TType -> TType
653+
654+
val reqTyForArgumentNullnessInference: TcGlobals -> actualTy: TType -> reqTy: TType -> TType
655+
652656
val isFunTy: TcGlobals -> TType -> bool
653657

654658
val isForallTy: TcGlobals -> TType -> bool

tests/FSharp.Compiler.ComponentTests/EmittedIL/Nullness/ReferenceDU.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ type MyDu =
88

99
let giveMeLabel () = JustLabel
1010

11-
let createMaybeString (innerValue) = MaybeString innerValue
11+
let createMaybeString (innerValue:string|null) = MaybeString innerValue
1212

1313
let processNullableDu (x : (MyDu | null)) : string | null =
1414
match x with

tests/FSharp.Compiler.ComponentTests/Language/NullableReferenceTypesTests.fs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,75 @@ let nonStrictFunc(x:string | null) = strictFunc(x)
2929
|> withDiagnostics [
3030
Error 3261, Line 4, Col 49, Line 4, Col 50, "Nullness warning: The types 'string' and 'string | null' do not have equivalent nullability."]
3131

32+
[<Theory>]
33+
[<InlineData("fileExists(path)")>]
34+
[<InlineData("fileExists path")>]
35+
[<InlineData("fileExists null")>]
36+
[<InlineData("path |> fileExists")>]
37+
[<InlineData("null |> fileExists")>]
38+
[<InlineData("System.IO.File.Exists(path)")>]
39+
[<InlineData("System.IO.File.Exists(null)")>]
40+
[<InlineData("path |> System.IO.File.Exists")>]
41+
[<InlineData("null |> System.IO.File.Exists")>]
42+
[<InlineData("System.String.IsNullOrEmpty(path)")>]
43+
let ``Calling a nullAllowing API can still infer a withoutNull type``(functionCall) =
44+
FSharp $"""
45+
module MyLib
46+
47+
let myStrictFunc(x: string) = x.GetHashCode()
48+
let fileExists (path:string|null) = true
49+
50+
let myStringReturningFunc (path) =
51+
let ex = {functionCall}
52+
myStrictFunc(path)
53+
"""
54+
|> asLibrary
55+
|> typeCheckWithStrictNullness
56+
|> shouldSucceed
57+
58+
//[<Fact>]
59+
// TODO Tomas - as of now, this does not bring the desired result
60+
let ``Type inference with underscore or null`` () =
61+
FSharp $"""
62+
module MyLib
63+
64+
let myFunc (path: _ | null) =
65+
System.IO.File.Exists(path)
66+
"""
67+
|> asLibrary
68+
|> typeCheckWithStrictNullness
69+
|> shouldSucceed
70+
71+
[<Fact>]
72+
let ``Type inference SystemIOFileExists`` () =
73+
FSharp $"""
74+
module MyLib
75+
76+
let test() =
77+
let maybeString : string | null = null
78+
System.IO.File.Exists(maybeString)
79+
80+
let myFunc path : string =
81+
let exists = path |> System.IO.File.Exists
82+
path
83+
"""
84+
|> asLibrary
85+
|> typeCheckWithStrictNullness
86+
|> shouldSucceed
87+
88+
[<Fact>]
89+
let ``Type inference fsharp func`` () =
90+
FSharp $"""module MyLib
91+
92+
let fileExists (path:string|null) = true
93+
let myStringReturningFunc (pathArg) : string =
94+
let ex = pathArg |> fileExists
95+
pathArg
96+
"""
97+
|> asLibrary
98+
|> typeCheckWithStrictNullness
99+
|> shouldSucceed
100+
32101

33102
// P1: inline or not
34103
// P2: type annotation for function argument

tests/adhoc/nullness/enabled/positive.fs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,10 @@ module KonsoleWithNullsModule =
9292
let WriteLineC2(fmt: C | null, arg1: C | null) = Console.WriteLine(fmt.Value, arg1.Value)
9393

9494
module KonsoleWithNullsModule2 =
95-
let WriteLine x = KonsoleWithNullsModule.WriteLine x
96-
let WriteLine2 (fmt, arg1) = KonsoleWithNullsModule.WriteLine2(fmt, arg1)
97-
let WriteLineC(s) = KonsoleWithNullsModule.WriteLineC(s)
98-
let WriteLineC2(fmt, arg1) = KonsoleWithNullsModule.WriteLineC2(fmt, arg1)
95+
let WriteLine (x : string | null) = KonsoleWithNullsModule.WriteLine x
96+
let WriteLine2 (fmt: string | null, arg1: string | null) = KonsoleWithNullsModule.WriteLine2(fmt, arg1)
97+
let WriteLineC(s: _ | null) = KonsoleWithNullsModule.WriteLineC(s)
98+
let WriteLineC2(fmt: _ | null, arg1: _ | null) = KonsoleWithNullsModule.WriteLineC2(fmt, arg1)
9999

100100
type KonsoleNoNulls =
101101
static member WriteLine(s: String) = Console.WriteLine(s)

0 commit comments

Comments
 (0)