Skip to content
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Core/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### 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<T>()`) 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))
* Added generic `print` and `printn` functions (`'T -> unit`) to `ExtraTopLevelOperators` for simple value printing to stdout. ([RFC FS-1125](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1125-print-printn-functions.md), [PR #19265](https://github.com/dotnet/fsharp/pull/19265))
* Ship `FSharp.Core` with an additional `net10.0` target framework (next to `netstandard2.0` and `netstandard2.1`). The `net`-TFM assembly is public-surface-identical to the `netstandard2.1` one; the target version is a pinned, deliberately advanced knob. ([PR #20229](https://github.com/dotnet/fsharp/pull/20229))
* 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))
Expand Down
3 changes: 3 additions & 0 deletions src/FSharp.Core/prim-types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5557,6 +5557,9 @@ namespace Microsoft.FSharp.Core
[<CompiledName("NonNullQuickPattern")>]
let inline (|NonNullQuick|) (value : 'T | null when 'T : not null and 'T : not struct) = nonNull value

[<CompiledName("WithNull")>]
let inline withNull (value: 'T) : 'T | null = (# "" value : 'T | null #)

module Checked =

let inline (+) (x: ^T) (y: ^U) : ^V =
Expand Down
9 changes: 9 additions & 0 deletions src/FSharp.Core/prim-types.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -5839,6 +5839,15 @@ namespace Microsoft.FSharp.Core
[<CompiledName("NonNullQuickPattern")>]
val inline (|NonNullQuick|) : value: 'T | null -> 'T when 'T : not null and 'T : not struct

/// <summary>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.</summary>
/// <remarks>This exists purely for interoperability with C# APIs that expose an unconstrained nullable generic, such as a method <c>T? M&lt;T&gt;()</c> or an interface member <c>T? GetValue&lt;T&gt;(int index)</c> where <c>T</c> has no <c>class</c> 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 <see cref="M:Microsoft.FSharp.Core.Operators.WithNull``1(``0)"/> it adds no <c>not null</c> or <c>not struct</c> constraint, so the resulting ('T | null) can be formed even when <c>'T</c> is a struct, where <c>null</c> is not a representable value: there the annotation carries no runtime meaning and is erased, and assigning <c>null</c> to such a location yields <c>Unchecked.defaultof&lt;'T&gt;</c> rather than a true null. Use it only to satisfy an interop signature.</remarks>
/// <param name="value">The value.</param>
/// <returns>The same value, retyped as ('T | null).</returns>
[<CompiledName("WithNull")>]
val inline withNull<'T> : value: 'T -> 'T | null

/// <summary>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.</summary>
module NonStructuralComparison =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>). Writing the generic member with Unchecked.defaultof
// alone reports FS3261; withNull re-types it to the expected 'T | null without adding constraints.
[<FactForNETCOREAPP>]
let ``Unchecked.withNull implements an unconstrained C# nullable generic member`` () =
let csharpLib =
CSharp """
#nullable enable
namespace Interop {
public interface IEventContext {
T? GetValue<T>(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

[<FactForNETCOREAPP>]
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

[<EntryPoint>]
let main _ =
System.Console.Write(sprintf "%A %b" (forward 42) (isNull (forward "hello")))
0
"""
|> withStrictNullness
|> compileExeAndRun
|> shouldSucceed
|> withStdOutContains "0 true"

[<Fact>]
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

Original file line number Diff line number Diff line change
Expand Up @@ -1844,6 +1844,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1844,6 +1844,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1859,6 +1859,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1859,6 +1859,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)
Expand Down
Loading