diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md index 97c667e0eb3..6783373a261 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -7,4 +7,6 @@ ### Added +* Add `Unchecked.withNull`, an interop escape hatch that re-types any `'T` to `'T | null` without the usual `not null`/`not struct` constraints, so unconstrained C# nullable-generic APIs (e.g. `T? M()`) can be implemented and consumed from F#. ([Issue #17734](https://github.com/dotnet/fsharp/issues/17734), [PR #20232](https://github.com/dotnet/fsharp/pull/20232)) + * `Async.RunSynchronouslyImmediate`: runs work on the calling thread until the first asynchronous suspension (as opposed to `RunSynchronously`, which immediately offloads if not on a background and/or threadpool thread). ([Issue #1042](https://github.com/fsharp/fslang-suggestions/issues/1042), [PR #19804](https://github.com/dotnet/fsharp/pull/19804)) diff --git a/src/FSharp.Core/prim-types.fs b/src/FSharp.Core/prim-types.fs index 036ba49ce48..f58002127d5 100644 --- a/src/FSharp.Core/prim-types.fs +++ b/src/FSharp.Core/prim-types.fs @@ -5554,6 +5554,9 @@ namespace Microsoft.FSharp.Core [] let inline (|NonNullQuick|) (value : 'T | null when 'T : not null and 'T : not struct) = nonNull value + [] + let inline withNull (value: 'T) : 'T | null = (# "" value : 'T | null #) + module Checked = let inline (+) (x: ^T) (y: ^U) : ^V = diff --git a/src/FSharp.Core/prim-types.fsi b/src/FSharp.Core/prim-types.fsi index 82ab0584ca8..1c2d6e2d433 100644 --- a/src/FSharp.Core/prim-types.fsi +++ b/src/FSharp.Core/prim-types.fsi @@ -5834,6 +5834,15 @@ namespace Microsoft.FSharp.Core [] val inline (|NonNullQuick|) : value: 'T | null -> 'T when 'T : not null and 'T : not struct + /// Unsafely retypes the value from 'T to ('T | null), bypassing the 'not null' and 'not struct' constraints that F# otherwise requires in order to write ('T | null). This is an unsafe operation. + /// This exists purely for interoperability with C# APIs that expose an unconstrained nullable generic, such as a method T? M<T>() or an interface member T? GetValue<T>(int index) where T has no class constraint and can therefore also be a struct. Without it such a signature cannot be implemented or consumed from F# without spurious FS3261 nullness warnings. + /// + /// It is unsafe precisely because it sidesteps those constraints. Unlike it adds no not null or not struct constraint, so the resulting ('T | null) can be formed even when 'T is a struct, where null is not a representable value: there the annotation carries no runtime meaning and is erased, and assigning null to such a location yields Unchecked.defaultof<'T> rather than a true null. Use it only to satisfy an interop signature. + /// The value. + /// The same value, retyped as ('T | null). + [] + val inline withNull<'T> : value: 'T -> 'T | null + /// A module of comparison and equality operators that are statically resolved, but which are not fully generic and do not make structural comparison. Opening this /// module may make code that relies on structural or generic comparison no longer compile. module NonStructuralComparison = diff --git a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableCsharpImportTests.fs b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableCsharpImportTests.fs index 0284263d714..f3900c89b38 100644 --- a/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableCsharpImportTests.fs +++ b/tests/FSharp.Compiler.ComponentTests/Language/Nullness/NullableCsharpImportTests.fs @@ -259,5 +259,66 @@ let theOtherOne = NullableClass.nullableImmArrayOfNotNullStrings |> shouldFail |> withDiagnostics [Error 3261, Line 7, Col 18, Line 7, Col 29, "Nullness warning: Possible dereference of a null value when accessing member 'Length' on the nullable value 'firstString' of type 'string | null'."] - - + +// https://github.com/dotnet/fsharp/issues/17734#issuecomment-5197965168 +// Implementing a C#-authored interface whose member returns an unconstrained 'T | null +// (e.g. SocketIO's IEventContext.GetValue). Writing the generic member with Unchecked.defaultof +// alone reports FS3261; withNull re-types it to the expected 'T | null without adding constraints. +[] +let ``Unchecked.withNull implements an unconstrained C# nullable generic member`` () = + let csharpLib = + CSharp """ +#nullable enable +namespace Interop { + public interface IEventContext { + T? GetValue(int index); + } +}""" |> withName "csEventContext" + |> withCSharpLanguageVersionPreview + + FSharp """module MyLibrary +open Interop + +let ctx = + { new IEventContext with + member _.GetValue<'T>(index: int) = Unchecked.withNull (Unchecked.defaultof<'T>) } +""" + |> asLibrary + |> withReferences [csharpLib] + |> withStrictNullness + |> compile + |> shouldSucceed + +[] +let ``Unchecked.withNull null assignment through generic layers is valid IL for structs`` () = + FSharp """module MyProgram +let observe (v: 'T) : objnull = + let mutable x = Unchecked.withNull v + x <- null + box x + +let forward (v: 'T) = observe v + +[] +let main _ = + System.Console.Write(sprintf "%A %b" (forward 42) (isNull (forward "hello"))) + 0 +""" + |> withStrictNullness + |> compileExeAndRun + |> shouldSucceed + |> withStdOutContains "0 true" + +[] +let ``Unchecked.withNull does not allow assigning null to a concrete struct mutable`` () = + FSharp """module MyLibrary +let f () = + let mutable x = Unchecked.withNull 42 + x <- null +""" + |> asLibrary + |> withStrictNullness + |> typecheck + |> shouldFail + |> withErrorCode 43 + diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl index 89fb1bb6146..9b1a0927aff 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl @@ -1822,6 +1822,7 @@ Microsoft.FSharp.Core.Operators+Unchecked: T DefaultOf[T]() Microsoft.FSharp.Core.Operators+Unchecked: T NonNullQuickPattern[T](T) Microsoft.FSharp.Core.Operators+Unchecked: T NonNull[T](T) Microsoft.FSharp.Core.Operators+Unchecked: T Unbox[T](System.Object) +Microsoft.FSharp.Core.Operators+Unchecked: T WithNull[T](T) Microsoft.FSharp.Core.Operators: Boolean IsNullV[T](System.Nullable`1[T]) Microsoft.FSharp.Core.Operators: Boolean IsNull[T](T) Microsoft.FSharp.Core.Operators: Boolean Not(Boolean) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl index 6d29205d290..e495a60c3e7 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl @@ -1822,6 +1822,7 @@ Microsoft.FSharp.Core.Operators+Unchecked: T DefaultOf[T]() Microsoft.FSharp.Core.Operators+Unchecked: T NonNullQuickPattern[T](T) Microsoft.FSharp.Core.Operators+Unchecked: T NonNull[T](T) Microsoft.FSharp.Core.Operators+Unchecked: T Unbox[T](System.Object) +Microsoft.FSharp.Core.Operators+Unchecked: T WithNull[T](T) Microsoft.FSharp.Core.Operators: Boolean IsNullV[T](System.Nullable`1[T]) Microsoft.FSharp.Core.Operators: Boolean IsNull[T](T) Microsoft.FSharp.Core.Operators: Boolean Not(Boolean) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl index d8d7ff44b21..7de296fe434 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl @@ -1825,6 +1825,7 @@ Microsoft.FSharp.Core.Operators+Unchecked: T DefaultOf[T]() Microsoft.FSharp.Core.Operators+Unchecked: T NonNullQuickPattern[T](T) Microsoft.FSharp.Core.Operators+Unchecked: T NonNull[T](T) Microsoft.FSharp.Core.Operators+Unchecked: T Unbox[T](System.Object) +Microsoft.FSharp.Core.Operators+Unchecked: T WithNull[T](T) Microsoft.FSharp.Core.Operators: Boolean IsNullV[T](System.Nullable`1[T]) Microsoft.FSharp.Core.Operators: Boolean IsNull[T](T) Microsoft.FSharp.Core.Operators: Boolean Not(Boolean) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl index db9f41d97a8..3e8daead8fd 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl @@ -1825,6 +1825,7 @@ Microsoft.FSharp.Core.Operators+Unchecked: T DefaultOf[T]() Microsoft.FSharp.Core.Operators+Unchecked: T NonNullQuickPattern[T](T) Microsoft.FSharp.Core.Operators+Unchecked: T NonNull[T](T) Microsoft.FSharp.Core.Operators+Unchecked: T Unbox[T](System.Object) +Microsoft.FSharp.Core.Operators+Unchecked: T WithNull[T](T) Microsoft.FSharp.Core.Operators: Boolean IsNullV[T](System.Nullable`1[T]) Microsoft.FSharp.Core.Operators: Boolean IsNull[T](T) Microsoft.FSharp.Core.Operators: Boolean Not(Boolean)