Skip to content

Commit ee55997

Browse files
authored
Handle |null types when optimizing away equals/hash/compare from LanguagePrimitves into instance method calls (#18296)
* Handle |null types when optimizing away eqals/hash/compare from function call to instance method * Set propper test assert * fantomas and notes * remove comment
1 parent 841ba8e commit ee55997

File tree

4 files changed

+94
-25
lines changed

4 files changed

+94
-25
lines changed

docs/release-notes/.FSharp.Compiler.Service/9.0.300.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* Fix missing nullness warning when static upcast dropped nullness ([Issue #18232](https://github.com/dotnet/fsharp/issues/18232), [PR #18261](https://github.com/dotnet/fsharp/pull/18261))
88
* Cancellable: only cancel on OCE with own token ([PR #18277](https://github.com/dotnet/fsharp/pull/18277))
99
* Cancellable: set token in more places ([PR #18283](https://github.com/dotnet/fsharp/pull/18283))
10+
* Fix NRE when accessing nullable fields of types within their equals/hash/compare methods ([PR #18296](https://github.com/dotnet/fsharp/pull/18296))
1011

1112
### Added
1213
* Added missing type constraints in FCS. ([PR #18241](https://github.com/dotnet/fsharp/pull/18241))

src/Compiler/Checking/AugmentWithHashCompare.fsi

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ type EqualityWithComparerAugmentation =
1414
EqualsWithComparer: Val
1515
EqualsExactWithComparer: Val }
1616

17+
val mkBindNullComparison: TcGlobals -> Text.Range -> thise: Expr -> thate: Expr -> expr: Expr -> Expr
18+
19+
val mkBindThisNullEquals: TcGlobals -> Text.Range -> thise: Expr -> thate: Expr -> expr: Expr -> Expr
20+
21+
val mkBindNullHash: TcGlobals -> Text.Range -> thise: Expr -> expr: Expr -> Expr
22+
1723
val CheckAugmentationAttribs: bool -> TcGlobals -> Import.ImportMap -> Tycon -> unit
1824

1925
val TyconIsCandidateForAugmentationWithCompare: TcGlobals -> Tycon -> bool

src/Compiler/Optimize/Optimizer.fs

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1664,7 +1664,9 @@ and OpHasEffect g m op =
16641664
| TOp.ExnFieldGet (ecref, n) -> isExnFieldMutable ecref n
16651665
| TOp.RefAddrGet _ -> false
16661666
| TOp.AnonRecdGet _ -> true // conservative
1667-
| TOp.ValFieldGet rfref -> rfref.RecdField.IsMutable || (TryFindTyconRefBoolAttribute g range0 g.attrib_AllowNullLiteralAttribute rfref.TyconRef = Some true)
1667+
| TOp.ValFieldGet rfref ->
1668+
rfref.RecdField.IsMutable
1669+
|| (TryFindTyconRefBoolAttribute g range0 g.attrib_AllowNullLiteralAttribute rfref.TyconRef = Some true)
16681670
| TOp.ValFieldGetAddr (rfref, _readonly) -> rfref.RecdField.IsMutable
16691671
| TOp.UnionCaseFieldGetAddr _ -> false // union case fields are immutable
16701672
| TOp.LValueOp (LAddrOf _, _) -> false // addresses of values are always constants
@@ -3167,7 +3169,7 @@ and CanDevirtualizeApplication cenv v vref ty args =
31673169
&& not (isUnitTy g ty)
31683170
&& isAppTy g ty
31693171
// Exclusion: Some unions have null as representations
3170-
&& not (IsUnionTypeWithNullAsTrueValue g (fst(StripToNominalTyconRef cenv ty)).Deref)
3172+
&& not (IsUnionTypeWithNullAsTrueValue g (fst(StripToNominalTyconRef cenv ty)).Deref)
31713173
// If we de-virtualize an operation on structs then we have to take the address of the object argument
31723174
// Hence we have to actually have the object argument available to us,
31733175
&& (not (isStructTy g ty) || not (isNil args))
@@ -3189,9 +3191,13 @@ and TakeAddressOfStructArgumentIfNeeded cenv (vref: ValRef) ty args m =
31893191
else
31903192
id, args
31913193

3192-
and DevirtualizeApplication cenv env (vref: ValRef) ty tyargs args m =
3194+
and DevirtualizeApplication cenv env (vref: ValRef) ty tyargs args m nullHandlerOpt =
31933195
let g = cenv.g
3194-
let wrap, args = TakeAddressOfStructArgumentIfNeeded cenv vref ty args m
3196+
let wrap, args =
3197+
match nullHandlerOpt with
3198+
| Some nullHandler when g.checkNullness && TypeNullIsExtraValueNew g vref.Range ty ->
3199+
nullHandler g m, args
3200+
| _ -> TakeAddressOfStructArgumentIfNeeded cenv vref ty args m
31953201
let transformedExpr = wrap (MakeApplicationAndBetaReduce g (exprForValRef m vref, vref.Type, (if isNil tyargs then [] else [tyargs]), args, m))
31963202
OptimizeExpr cenv env transformedExpr
31973203

@@ -3212,8 +3218,10 @@ and TryDevirtualizeApplication cenv env (f, tyargs, args, m) =
32123218
| Expr.Val (v, _, _), [ty], _ when CanDevirtualizeApplication cenv v g.generic_comparison_inner_vref ty args ->
32133219

32143220
let tcref, tyargs = StripToNominalTyconRef cenv ty
3215-
match tcref.GeneratedCompareToValues with
3216-
| Some (_, vref) -> Some (DevirtualizeApplication cenv env vref ty tyargs args m)
3221+
match tcref.GeneratedCompareToValues, args with
3222+
| Some (_, vref), [x;y] ->
3223+
let nullHandler g m = AugmentTypeDefinitions.mkBindNullComparison g m x y
3224+
Some (DevirtualizeApplication cenv env vref ty tyargs args m (Some nullHandler))
32173225
| _ -> None
32183226

32193227
| Expr.Val (v, _, _), [ty], _ when CanDevirtualizeApplication cenv v g.generic_comparison_withc_inner_vref ty args ->
@@ -3225,7 +3233,8 @@ and TryDevirtualizeApplication cenv env (f, tyargs, args, m) =
32253233
// arg list, and create a tuple of y & comp
32263234
// push the comparer to the end and box the argument
32273235
let args2 = [x; mkRefTupledNoTypes g m [mkCoerceExpr(y, g.obj_ty_ambivalent, m, ty) ; comp]]
3228-
Some (DevirtualizeApplication cenv env vref ty tyargs args2 m)
3236+
let nullHandler g m = AugmentTypeDefinitions.mkBindNullComparison g m x y
3237+
Some (DevirtualizeApplication cenv env vref ty tyargs args2 m (Some nullHandler))
32293238
| _ -> None
32303239

32313240
// Optimize/analyze calls to LanguagePrimitives.HashCompare.GenericEqualityIntrinsic when type is known
@@ -3235,8 +3244,10 @@ and TryDevirtualizeApplication cenv env (f, tyargs, args, m) =
32353244
| Expr.Val (v, _, _), [ty], _ when CanDevirtualizeApplication cenv v g.generic_equality_er_inner_vref ty args ->
32363245

32373246
let tcref, tyargs = StripToNominalTyconRef cenv ty
3238-
match tcref.GeneratedHashAndEqualsValues with
3239-
| Some (_, vref) -> Some (DevirtualizeApplication cenv env vref ty tyargs args m)
3247+
match tcref.GeneratedHashAndEqualsValues, args with
3248+
| Some (_, vref),[x;y] ->
3249+
let nullHandler g m = AugmentTypeDefinitions.mkBindThisNullEquals g m x y
3250+
Some (DevirtualizeApplication cenv env vref ty tyargs args m (Some nullHandler))
32403251
| _ -> None
32413252

32423253
// Optimize/analyze calls to LanguagePrimitives.HashCompare.GenericEqualityWithComparerIntrinsic
@@ -3246,11 +3257,13 @@ and TryDevirtualizeApplication cenv env (f, tyargs, args, m) =
32463257
| Some (_, _, _, Some withcEqualsExactVal), [comp; x; y] ->
32473258
// push the comparer to the end
32483259
let args2 = [x; mkRefTupledNoTypes g m [y; comp]]
3249-
Some (DevirtualizeApplication cenv env withcEqualsExactVal ty tyargs args2 m)
3260+
let nullHandler g m = AugmentTypeDefinitions.mkBindThisNullEquals g m x y
3261+
Some (DevirtualizeApplication cenv env withcEqualsExactVal ty tyargs args2 m (Some nullHandler))
32503262
| Some (_, _, withcEqualsVal, _ ), [comp; x; y] ->
32513263
// push the comparer to the end and box the argument
32523264
let args2 = [x; mkRefTupledNoTypes g m [mkCoerceExpr(y, g.obj_ty_ambivalent, m, ty) ; comp]]
3253-
Some (DevirtualizeApplication cenv env withcEqualsVal ty tyargs args2 m)
3265+
let nullHandler g m = AugmentTypeDefinitions.mkBindThisNullEquals g m x y
3266+
Some (DevirtualizeApplication cenv env withcEqualsVal ty tyargs args2 m (Some nullHandler))
32543267
| _ -> None
32553268

32563269
// Optimize/analyze calls to LanguagePrimitives.HashCompare.GenericEqualityIntrinsic
@@ -3259,20 +3272,23 @@ and TryDevirtualizeApplication cenv env (f, tyargs, args, m) =
32593272
match tcref.GeneratedHashAndEqualsWithComparerValues, args with
32603273
| Some (_, _, _, Some withcEqualsExactVal), [x; y] ->
32613274
let args2 = [x; mkRefTupledNoTypes g m [y; (mkCallGetGenericPEREqualityComparer g m)]]
3262-
Some (DevirtualizeApplication cenv env withcEqualsExactVal ty tyargs args2 m)
3275+
let nullHandler g m = AugmentTypeDefinitions.mkBindThisNullEquals g m x y
3276+
Some (DevirtualizeApplication cenv env withcEqualsExactVal ty tyargs args2 m (Some nullHandler))
32633277
| Some (_, _, withcEqualsVal, _), [x; y] ->
32643278
let equalsExactOpt =
32653279
tcref.MembersOfFSharpTyconByName.TryFind("Equals")
32663280
|> Option.map (List.where (fun x -> x.IsCompilerGenerated))
32673281
|> Option.bind List.tryExactlyOne
32683282

3283+
let nullHandler g m = AugmentTypeDefinitions.mkBindThisNullEquals g m x y
3284+
32693285
match equalsExactOpt with
32703286
| Some equalsExact ->
32713287
let args2 = [x; mkRefTupledNoTypes g m [y; (mkCallGetGenericPEREqualityComparer g m)]]
3272-
Some (DevirtualizeApplication cenv env equalsExact ty tyargs args2 m)
3288+
Some (DevirtualizeApplication cenv env equalsExact ty tyargs args2 m (Some nullHandler))
32733289
| None ->
32743290
let args2 = [x; mkRefTupledNoTypes g m [mkCoerceExpr(y, g.obj_ty_ambivalent, m, ty); (mkCallGetGenericPEREqualityComparer g m)]]
3275-
Some (DevirtualizeApplication cenv env withcEqualsVal ty tyargs args2 m)
3291+
Some (DevirtualizeApplication cenv env withcEqualsVal ty tyargs args2 m (Some nullHandler))
32763292
| _ -> None
32773293

32783294
// Optimize/analyze calls to LanguagePrimitives.HashCompare.GenericHashIntrinsic
@@ -3281,7 +3297,8 @@ and TryDevirtualizeApplication cenv env (f, tyargs, args, m) =
32813297
match tcref.GeneratedHashAndEqualsWithComparerValues, args with
32823298
| Some (_, withcGetHashCodeVal, _, _), [x] ->
32833299
let args2 = [x; mkCallGetGenericEREqualityComparer g m]
3284-
Some (DevirtualizeApplication cenv env withcGetHashCodeVal ty tyargs args2 m)
3300+
let nullHandler g m = AugmentTypeDefinitions.mkBindNullHash g m x
3301+
Some (DevirtualizeApplication cenv env withcGetHashCodeVal ty tyargs args2 m (Some nullHandler))
32853302
| _ -> None
32863303

32873304
// Optimize/analyze calls to LanguagePrimitives.HashCompare.GenericHashWithComparerIntrinsic
@@ -3290,7 +3307,8 @@ and TryDevirtualizeApplication cenv env (f, tyargs, args, m) =
32903307
match tcref.GeneratedHashAndEqualsWithComparerValues, args with
32913308
| Some (_, withcGetHashCodeVal, _, _), [comp; x] ->
32923309
let args2 = [x; comp]
3293-
Some (DevirtualizeApplication cenv env withcGetHashCodeVal ty tyargs args2 m)
3310+
let nullHandler g m = AugmentTypeDefinitions.mkBindNullHash g m x
3311+
Some (DevirtualizeApplication cenv env withcGetHashCodeVal ty tyargs args2 m (Some nullHandler))
32943312
| _ -> None
32953313

32963314
// Optimize/analyze calls to LanguagePrimitives.HashCompare.GenericComparisonWithComparerIntrinsic for tuple types
@@ -3304,7 +3322,7 @@ and TryDevirtualizeApplication cenv env (f, tyargs, args, m) =
33043322
| 5 -> Some g.generic_compare_withc_tuple5_vref
33053323
| _ -> None
33063324
match vref with
3307-
| Some vref -> Some (DevirtualizeApplication cenv env vref ty tyargs (mkCallGetGenericComparer g m :: args) m)
3325+
| Some vref -> Some (DevirtualizeApplication cenv env vref ty tyargs (mkCallGetGenericComparer g m :: args) m None)
33083326
| None -> None
33093327

33103328
// Optimize/analyze calls to LanguagePrimitives.HashCompare.GenericHashWithComparerIntrinsic for tuple types
@@ -3318,7 +3336,7 @@ and TryDevirtualizeApplication cenv env (f, tyargs, args, m) =
33183336
| 5 -> Some g.generic_hash_withc_tuple5_vref
33193337
| _ -> None
33203338
match vref with
3321-
| Some vref -> Some (DevirtualizeApplication cenv env vref ty tyargs (mkCallGetGenericEREqualityComparer g m :: args) m)
3339+
| Some vref -> Some (DevirtualizeApplication cenv env vref ty tyargs (mkCallGetGenericEREqualityComparer g m :: args) m None)
33223340
| None -> None
33233341

33243342
// Optimize/analyze calls to LanguagePrimitives.HashCompare.GenericEqualityIntrinsic for tuple types
@@ -3334,7 +3352,7 @@ and TryDevirtualizeApplication cenv env (f, tyargs, args, m) =
33343352
| 5 -> Some g.generic_equals_withc_tuple5_vref
33353353
| _ -> None
33363354
match vref with
3337-
| Some vref -> Some (DevirtualizeApplication cenv env vref ty tyargs (mkCallGetGenericPEREqualityComparer g m :: args) m)
3355+
| Some vref -> Some (DevirtualizeApplication cenv env vref ty tyargs (mkCallGetGenericPEREqualityComparer g m :: args) m None)
33383356
| None -> None
33393357

33403358
// Optimize/analyze calls to LanguagePrimitives.HashCompare.GenericComparisonWithComparerIntrinsic for tuple types
@@ -3348,7 +3366,7 @@ and TryDevirtualizeApplication cenv env (f, tyargs, args, m) =
33483366
| 5 -> Some g.generic_compare_withc_tuple5_vref
33493367
| _ -> None
33503368
match vref with
3351-
| Some vref -> Some (DevirtualizeApplication cenv env vref ty tyargs args m)
3369+
| Some vref -> Some (DevirtualizeApplication cenv env vref ty tyargs args m None)
33523370
| None -> None
33533371

33543372
// Optimize/analyze calls to LanguagePrimitives.HashCompare.GenericHashWithComparerIntrinsic for tuple types
@@ -3362,7 +3380,7 @@ and TryDevirtualizeApplication cenv env (f, tyargs, args, m) =
33623380
| 5 -> Some g.generic_hash_withc_tuple5_vref
33633381
| _ -> None
33643382
match vref with
3365-
| Some vref -> Some (DevirtualizeApplication cenv env vref ty tyargs args m)
3383+
| Some vref -> Some (DevirtualizeApplication cenv env vref ty tyargs args m None)
33663384
| None -> None
33673385

33683386
// Optimize/analyze calls to LanguagePrimitives.HashCompare.GenericEqualityWithComparerIntrinsic for tuple types
@@ -3376,7 +3394,7 @@ and TryDevirtualizeApplication cenv env (f, tyargs, args, m) =
33763394
| 5 -> Some g.generic_equals_withc_tuple5_vref
33773395
| _ -> None
33783396
match vref with
3379-
| Some vref -> Some (DevirtualizeApplication cenv env vref ty tyargs args m)
3397+
| Some vref -> Some (DevirtualizeApplication cenv env vref ty tyargs args m None)
33803398
| None -> None
33813399

33823400
// Calls to LanguagePrimitives.IntrinsicFunctions.UnboxGeneric can be optimized to calls to UnboxFast when we know that the
@@ -3385,15 +3403,15 @@ and TryDevirtualizeApplication cenv env (f, tyargs, args, m) =
33853403
| Expr.Val (v, _, _), [ty], _ when valRefEq g v g.unbox_vref &&
33863404
canUseUnboxFast g m ty ->
33873405

3388-
Some(DevirtualizeApplication cenv env g.unbox_fast_vref ty tyargs args m)
3406+
Some(DevirtualizeApplication cenv env g.unbox_fast_vref ty tyargs args m None)
33893407

33903408
// Calls to LanguagePrimitives.IntrinsicFunctions.TypeTestGeneric can be optimized to calls to TypeTestFast when we know that the
33913409
// target type isn't 'NullNotTrueValue', i.e. that the target type is not an F# union, record etc.
33923410
// Note TypeTestFast is just the .NET IL 'isinst' instruction followed by a non-null comparison
33933411
| Expr.Val (v, _, _), [ty], _ when valRefEq g v g.istype_vref &&
33943412
canUseTypeTestFast g ty ->
33953413

3396-
Some(DevirtualizeApplication cenv env g.istype_fast_vref ty tyargs args m)
3414+
Some(DevirtualizeApplication cenv env g.istype_fast_vref ty tyargs args m None)
33973415

33983416
// Don't fiddle with 'methodhandleof' calls - just remake the application
33993417
| Expr.Val (vref, _, _), _, _ when valRefEq g vref g.methodhandleof_vref ->

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

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1329,4 +1329,48 @@ let myOption () : option<string> = None """
13291329
.maxstack 8
13301330
IL_0000: ldnull
13311331
IL_0001: ret
1332-
}"]
1332+
}"]
1333+
1334+
// Regression https://github.com/dotnet/fsharp/issues/18286
1335+
[<Fact>]
1336+
let ``Equality and hashcode augmentation is null safe`` () =
1337+
1338+
Fsx """
1339+
1340+
type Bar = { b: string | null }
1341+
type Foo = { f: Bar | null }
1342+
type DUFoo = WithNull of (Foo|null)
1343+
let a = { f = null }
1344+
let b = { f = null }
1345+
1346+
let c = WithNull(null)
1347+
let d = WithNull(null)
1348+
1349+
let e = WithNull(a)
1350+
let f = WithNull(b)
1351+
1352+
[<EntryPoint>]
1353+
let main _ =
1354+
printf "Test %A;" ({b = null} = {b = null})
1355+
printf ",1 %A" (a = b)
1356+
printf ",2 %A" (a.GetHashCode() = b.GetHashCode())
1357+
printf ",3 %A" (c = d)
1358+
printf ",4 %A" (c.GetHashCode() = d.GetHashCode())
1359+
printf ",5 %A" (e = f)
1360+
printf ",6 %A" (e = c)
1361+
printf ",7 %A" (e.GetHashCode() = f.GetHashCode())
1362+
printf ",8 %A" (e.GetHashCode() = c.GetHashCode())
1363+
1364+
printf ",9 %A" (a > b)
1365+
printf ",10 %A" (c > d)
1366+
printf ",11 %A" (e > f)
1367+
printf ",12 %A" (e > c)
1368+
0
1369+
"""
1370+
|> withNullnessOptions
1371+
|> withOptimization false
1372+
|> asExe
1373+
|> compile
1374+
//|> verifyIL ["abc"]
1375+
|> run
1376+
|> verifyOutputContains [|"Test true;,1 true,2 true,3 true,4 true,5 true,6 false,7 true,8 false,9 false,10 false,11 false,12 true"|]

0 commit comments

Comments
 (0)