Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/release-notes/.FSharp.Core/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<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))

* `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))
3 changes: 3 additions & 0 deletions src/FSharp.Core/prim-types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5554,6 +5554,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 @@ -5834,6 +5834,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 @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading