Skip to content

Commit d90885c

Browse files
authored
Nullness - option<> must be indicated as nullable in IL for C# consumers (#17528)
1 parent d37a8ae commit d90885c

File tree

11 files changed

+139
-22
lines changed

11 files changed

+139
-22
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* Fix `function` implicit conversion. ([Issue #7401](https://github.com/dotnet/fsharp/issues/7401), [PR #17487](https://github.com/dotnet/fsharp/pull/17487))
99
* Compiler fails to recognise namespace in FQN with enabled GraphBasedChecking. ([Issue #17508](https://github.com/dotnet/fsharp/issues/17508), [PR #17510](https://github.com/dotnet/fsharp/pull/17510))
1010
* Fix missing message for type error (FS0001). ([Issue #17373](https://github.com/dotnet/fsharp/issues/17373), [PR #17516](https://github.com/dotnet/fsharp/pull/17516))
11+
* Nullness export - make sure option<> and other UseNullAsTrueValue types are properly annotated as nullable for C# and reflection consumers [PR #17528](https://github.com/dotnet/fsharp/pull/17528)
1112
* MethodAccessException on equality comparison of a type private to module. ([Issue #17541](https://github.com/dotnet/fsharp/issues/17541), [PR #17548](https://github.com/dotnet/fsharp/pull/17548))
1213

1314
### Added

src/Compiler/CodeGen/EraseUnions.fs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ open FSharp.Compiler.IlxGenSupport
88
open System.Collections.Generic
99
open System.Reflection
1010
open Internal.Utilities.Library
11+
open FSharp.Compiler.TypedTree
1112
open FSharp.Compiler.TypedTreeOps
1213
open FSharp.Compiler.Features
1314
open FSharp.Compiler.TcGlobals
@@ -955,7 +956,14 @@ let convAlternativeDef
955956
&& g.langFeatureNullness
956957
&& repr.RepresentAlternativeAsNull(info, alt)
957958
then
958-
GetNullableAttribute g [ FSharp.Compiler.TypedTree.NullnessInfo.WithNull ]
959+
let noTypars = td.GenericParams.Length
960+
961+
GetNullableAttribute
962+
g
963+
[
964+
yield NullnessInfo.WithNull // The top-level value itself, e.g. option, is nullable
965+
yield! List.replicate noTypars NullnessInfo.AmbivalentToNull
966+
] // The typars are not (i.e. do not change option<string> into option<string?>
959967
|> Array.singleton
960968
|> mkILCustomAttrsFromArray
961969
else
@@ -1199,7 +1207,7 @@ let convAlternativeDef
11991207

12001208
let attrs =
12011209
if g.checkNullness && g.langFeatureNullness then
1202-
GetNullableContextAttribute g :: debugAttrs
1210+
GetNullableContextAttribute g 1uy :: debugAttrs
12031211
else
12041212
debugAttrs
12051213

@@ -1365,8 +1373,7 @@ let mkClassUnionDef
13651373
match nullableIdx with
13661374
| None ->
13671375
existingAttrs
1368-
|> Array.append
1369-
[| GetNullableAttribute g [ FSharp.Compiler.TypedTree.NullnessInfo.WithNull ] |]
1376+
|> Array.append [| GetNullableAttribute g [ NullnessInfo.WithNull ] |]
13701377
| Some idx ->
13711378
let replacementAttr =
13721379
match existingAttrs[idx] with
@@ -1619,7 +1626,7 @@ let mkClassUnionDef
16191626
customAttrs =
16201627
if cud.IsNullPermitted && g.checkNullness && g.langFeatureNullness then
16211628
td.CustomAttrs.AsArray()
1622-
|> Array.append [| GetNullableAttribute g [ FSharp.Compiler.TypedTree.NullnessInfo.WithNull ] |]
1629+
|> Array.append [| GetNullableAttribute g [ NullnessInfo.WithNull ] |]
16231630
|> mkILCustomAttrsFromArray
16241631
|> storeILCustomAttrs
16251632
else

src/Compiler/CodeGen/IlxGen.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1912,7 +1912,7 @@ type TypeDefBuilder(tdef: ILTypeDef, tdefDiscards) =
19121912
if attrsBefore |> TryFindILAttribute g.attrib_AllowNullLiteralAttribute then
19131913
yield GetNullableAttribute g [ NullnessInfo.WithNull ]
19141914
if (gmethods.Count + gfields.Count + gproperties.Count) > 0 then
1915-
yield GetNullableContextAttribute g
1915+
yield GetNullableContextAttribute g 1uy
19161916
|]
19171917
|> mkILCustomAttrsFromArray
19181918
else

src/Compiler/CodeGen/IlxGenSupport.fs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ let GetDynamicDependencyAttribute (g: TcGlobals) memberTypes (ilType: ILType) =
319319
/// Nested items not being annotated with Nullable attribute themselves are interpreted as being withoutnull
320320
/// Doing it that way is a heuristical decision supporting limited usage of (| null) annotations and not allowing nulls in >50% of F# code
321321
/// (if majority of fields/parameters/return values would be nullable, this heuristic would lead to bloat of generated metadata)
322-
let GetNullableContextAttribute (g: TcGlobals) =
322+
let GetNullableContextAttribute (g: TcGlobals) flagValue =
323323
let tref = g.attrib_NullableContextAttribute.TypeRef
324324

325325
g.TryEmbedILType(
@@ -329,7 +329,7 @@ let GetNullableContextAttribute (g: TcGlobals) =
329329
mkLocalPrivateAttributeWithPropertyConstructors (g, tref.Name, fields, PublicFields))
330330
)
331331

332-
mkILCustomAttribute (tref, [ g.ilg.typ_Byte ], [ ILAttribElem.Byte 1uy ], [])
332+
mkILCustomAttribute (tref, [ g.ilg.typ_Byte ], [ ILAttribElem.Byte flagValue ], [])
333333

334334
let GetNotNullWhenTrueAttribute (g: TcGlobals) (propNames: string array) =
335335
let tref = g.attrib_MemberNotNullWhenAttribute.TypeRef
@@ -407,6 +407,11 @@ let rec GetNullnessFromTType (g: TcGlobals) ty =
407407
else if isValueType then
408408
// Generic value type: 0, followed by the representation of the type arguments in order including containing types
409409
yield NullnessInfo.AmbivalentToNull
410+
else if
411+
IsUnionTypeWithNullAsTrueValue g tcref.Deref
412+
|| TypeHasAllowNull tcref g FSharp.Compiler.Text.Range.Zero
413+
then
414+
yield NullnessInfo.WithNull
410415
else
411416
// Reference type: the nullability (0, 1, or 2), followed by the representation of the type arguments in order including containing types
412417
yield nullness.Evaluate()

src/Compiler/CodeGen/IlxGenSupport.fsi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@ val GenAdditionalAttributesForTy: g: TcGlobals -> ty: TypedTree.TType -> ILAttri
2323
val GetReadOnlyAttribute: g: TcGlobals -> ILAttribute
2424
val GetIsUnmanagedAttribute: g: TcGlobals -> ILAttribute
2525
val GetNullableAttribute: g: TcGlobals -> nullnessInfos: TypedTree.NullnessInfo list -> ILAttribute
26-
val GetNullableContextAttribute: g: TcGlobals -> ILAttribute
26+
val GetNullableContextAttribute: g: TcGlobals -> byte -> ILAttribute
2727
val GetNotNullWhenTrueAttribute: g: TcGlobals -> string array -> ILAttribute

src/Compiler/TypedTree/TypedTreeOps.fs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9198,16 +9198,18 @@ let reqTyForArgumentNullnessInference g actualTy reqTy =
91989198
changeWithNullReqTyToVariable g reqTy
91999199
| _ -> reqTy
92009200

9201+
let TypeHasAllowNull (tcref:TyconRef) g m =
9202+
not tcref.IsStructOrEnumTycon &&
9203+
not (isByrefLikeTyconRef g m tcref) &&
9204+
(TryFindTyconRefBoolAttribute g m g.attrib_AllowNullLiteralAttribute tcref = Some true)
9205+
92019206
/// The new logic about whether a type admits the use of 'null' as a value.
92029207
let TypeNullIsExtraValueNew g m ty =
92039208
let sty = stripTyparEqns ty
92049209

92059210
// Check if the type has AllowNullLiteral
92069211
(match tryTcrefOfAppTy g sty with
9207-
| ValueSome tcref ->
9208-
not tcref.IsStructOrEnumTycon &&
9209-
not (isByrefLikeTyconRef g m tcref) &&
9210-
(TryFindTyconRefBoolAttribute g m g.attrib_AllowNullLiteralAttribute tcref = Some true)
9212+
| ValueSome tcref -> TypeHasAllowNull tcref g m
92119213
| _ -> false)
92129214
||
92139215
// Check if the type has a nullness annotation

src/Compiler/TypedTree/TypedTreeOps.fsi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1809,6 +1809,8 @@ val TypeNullIsTrueValue: TcGlobals -> TType -> bool
18091809

18101810
val TypeNullIsExtraValue: TcGlobals -> range -> TType -> bool
18111811

1812+
val TypeHasAllowNull: TyconRef -> TcGlobals -> range -> bool
1813+
18121814
val TypeNullIsExtraValueNew: TcGlobals -> range -> TType -> bool
18131815

18141816
val TypeNullNever: TcGlobals -> TType -> bool

tests/FSharp.Compiler.ComponentTests/EmittedIL/Nullness/NullAsTrueValue.fs.il.net472.bsl

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
.custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags,
7474
int32) = ( 01 00 08 00 00 00 00 00 00 00 00 00 )
7575
.param [0]
76-
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 02 00 00 )
76+
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = ( 01 00 02 00 00 00 02 00 00 00 )
7777

7878
.maxstack 8
7979
IL_0000: ldnull
@@ -188,7 +188,7 @@
188188
.property class TestModule/MyNullableOption`1<!T>
189189
MyNone()
190190
{
191-
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 02 00 00 )
191+
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = ( 01 00 02 00 00 00 02 00 00 00 )
192192
.custom instance void [runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
193193
.custom instance void [runtime]System.Diagnostics.DebuggerNonUserCodeAttribute::.ctor() = ( 01 00 00 00 )
194194
.custom instance void [runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [runtime]System.Diagnostics.DebuggerBrowsableState) = ( 01 00 00 00 00 00 00 00 )
@@ -266,7 +266,7 @@
266266
.custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags,
267267
int32) = ( 01 00 08 00 00 00 00 00 00 00 00 00 )
268268
.param [0]
269-
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 02 00 00 )
269+
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = ( 01 00 02 00 00 00 02 00 00 00 )
270270

271271
.maxstack 8
272272
IL_0000: ldnull
@@ -382,7 +382,7 @@
382382
.property class TestModule/MyOptionWhichCannotHaveNullInTheInside`1<!T>
383383
MyNotNullNone()
384384
{
385-
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 02 00 00 )
385+
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = ( 01 00 02 00 00 00 02 00 00 00 )
386386
.custom instance void [runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
387387
.custom instance void [runtime]System.Diagnostics.DebuggerNonUserCodeAttribute::.ctor() = ( 01 00 00 00 )
388388
.custom instance void [runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [runtime]System.Diagnostics.DebuggerBrowsableState) = ( 01 00 00 00 00 00 00 00 )
@@ -422,6 +422,10 @@
422422
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 02 00 00 )
423423
.param type b
424424
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 02 00 00 )
425+
.param [0]
426+
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = ( 01 00 02 00 00 00 02 01 00 00 )
427+
.param [2]
428+
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = ( 01 00 02 00 00 00 02 01 00 00 )
425429

426430
.maxstack 4
427431
.locals init (class TestModule/MyNullableOption`1<!!a> V_0,
@@ -458,6 +462,10 @@
458462
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 01 00 00 )
459463
.param type b
460464
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 01 00 00 )
465+
.param [0]
466+
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = ( 01 00 02 00 00 00 02 01 00 00 )
467+
.param [2]
468+
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = ( 01 00 02 00 00 00 02 01 00 00 )
461469

462470
.maxstack 4
463471
.locals init (class TestModule/MyOptionWhichCannotHaveNullInTheInside`1<!!a> V_0,

tests/FSharp.Compiler.ComponentTests/EmittedIL/Nullness/NullAsTrueValue.fs.il.netcore.bsl

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
.custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags,
7474
int32) = ( 01 00 08 00 00 00 00 00 00 00 00 00 )
7575
.param [0]
76-
.custom instance void [runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 02 00 00 )
76+
.custom instance void [runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = ( 01 00 02 00 00 00 02 00 00 00 )
7777

7878
.maxstack 8
7979
IL_0000: ldnull
@@ -188,7 +188,7 @@
188188
.property class TestModule/MyNullableOption`1<!T>
189189
MyNone()
190190
{
191-
.custom instance void [runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 02 00 00 )
191+
.custom instance void [runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = ( 01 00 02 00 00 00 02 00 00 00 )
192192
.custom instance void [runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
193193
.custom instance void [runtime]System.Diagnostics.DebuggerNonUserCodeAttribute::.ctor() = ( 01 00 00 00 )
194194
.custom instance void [runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [runtime]System.Diagnostics.DebuggerBrowsableState) = ( 01 00 00 00 00 00 00 00 )
@@ -266,7 +266,7 @@
266266
.custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags,
267267
int32) = ( 01 00 08 00 00 00 00 00 00 00 00 00 )
268268
.param [0]
269-
.custom instance void [runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 02 00 00 )
269+
.custom instance void [runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = ( 01 00 02 00 00 00 02 00 00 00 )
270270

271271
.maxstack 8
272272
IL_0000: ldnull
@@ -382,7 +382,7 @@
382382
.property class TestModule/MyOptionWhichCannotHaveNullInTheInside`1<!T>
383383
MyNotNullNone()
384384
{
385-
.custom instance void [runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 02 00 00 )
385+
.custom instance void [runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = ( 01 00 02 00 00 00 02 00 00 00 )
386386
.custom instance void [runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 )
387387
.custom instance void [runtime]System.Diagnostics.DebuggerNonUserCodeAttribute::.ctor() = ( 01 00 00 00 )
388388
.custom instance void [runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [runtime]System.Diagnostics.DebuggerBrowsableState) = ( 01 00 00 00 00 00 00 00 )
@@ -422,6 +422,10 @@
422422
.custom instance void [runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 02 00 00 )
423423
.param type b
424424
.custom instance void [runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 02 00 00 )
425+
.param [0]
426+
.custom instance void [runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = ( 01 00 02 00 00 00 02 01 00 00 )
427+
.param [2]
428+
.custom instance void [runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = ( 01 00 02 00 00 00 02 01 00 00 )
425429

426430
.maxstack 4
427431
.locals init (class TestModule/MyNullableOption`1<!!a> V_0,
@@ -458,6 +462,10 @@
458462
.custom instance void [runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 01 00 00 )
459463
.param type b
460464
.custom instance void [runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 01 00 00 )
465+
.param [0]
466+
.custom instance void [runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = ( 01 00 02 00 00 00 02 01 00 00 )
467+
.param [2]
468+
.custom instance void [runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = ( 01 00 02 00 00 00 02 01 00 00 )
461469

462470
.maxstack 4
463471
.locals init (class TestModule/MyOptionWhichCannotHaveNullInTheInside`1<!!a> V_0,

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,39 @@ module Interop =
112112
|> File.ReadAllText
113113
|> fsharpLibCreator
114114

115+
[<Fact>]
116+
let ``Csharp understands option like type using UseNullAsTrueValue`` () =
117+
let csharpCode = """
118+
using System;
119+
using static TestModule;
120+
using static Microsoft.FSharp.Core.FuncConvert;
121+
#nullable enable
122+
public class C {
123+
// MyNullableOption has [<CompilationRepresentation(CompilationRepresentationFlags.UseNullAsTrueValue)>] applied on it
124+
public void M(MyNullableOption<string> customOption) {
125+
126+
Console.WriteLine(customOption.ToString()); // should not warn
127+
128+
var thisIsNone = MyNullableOption<string>.MyNone;
129+
Console.WriteLine(thisIsNone.ToString()); // !! should warn !!
130+
131+
var mapped = mapPossiblyNullable<string,string>(ToFSharpFunc<string,string>(x => x.ToString()), customOption); // should not warn, because null will not be passed
132+
var mapped2 = mapPossiblyNullable<string,string>(ToFSharpFunc<string,string>(x => x + ".."), thisIsNone); // should NOT warn for passing in none, this is allowed
133+
134+
if(thisIsNone != null)
135+
Console.WriteLine(thisIsNone.ToString()); // should NOT warn
136+
137+
if(customOption != null)
138+
Console.WriteLine(customOption.ToString()); // should NOT warn
139+
140+
Console.WriteLine(MyOptionWhichCannotHaveNullInTheInside<string>.NewMyNotNullSome("").ToString()); // should NOT warn
141+
142+
}
143+
}"""
144+
csharpCode
145+
|> csharpLibCompile (FsharpFromFile "NullAsTrueValue.fs")
146+
|> withDiagnostics [ Warning 8602, Line 12, Col 27, Line 12, Col 37, "Dereference of a possibly null reference."]
147+
115148
[<Fact>]
116149
let ``Csharp understands Fsharp-produced struct unions via IsXXX flow analysis`` () =
117150
let csharpCode = """

0 commit comments

Comments
 (0)