diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md index 884146bc4e4..0f516eaee54 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -7,6 +7,8 @@ ### 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)) + * Add `Async.Await`, mirroring `Async.AwaitTask` semantics, but elides egregious `AggregateException` wrapping. Includes `ValueTask` support, and a SRTP-based overload accepting any Task-like value that supports the `GetAwaiter` protocol. ([Language Suggestion #840](https://github.com/fsharp/fslang-suggestions/issues/840), [PR #19785](https://github.com/dotnet/fsharp/pull/19785)) * `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)) * Added modules for `Async`, `Task` and `ValueTask` with consistent `result`, `map`, `bind`, `ignore`, `catchWith`, `catch`, and `empty` functions ([LanguageSuggestion #1466](https://github.com/fsharp/fslang-suggestions/issues/1466), [PR #19844](https://github.com/dotnet/fsharp/pull/19844)) 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 1f6241a57e3..ce13489f72f 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl @@ -1842,6 +1842,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 0b025a942de..deb16a38d01 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl @@ -1842,6 +1842,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 9496d1dfe2b..7c03163775a 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl @@ -1857,6 +1857,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 b1a377ea33b..10a915ced6f 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl @@ -1857,6 +1857,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)