From 0fdc10c74bd2c95250f7336d6020040d96dce79c Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 25 May 2026 18:36:54 +0100 Subject: [PATCH 01/12] Apply even more feedback goodness --- docs/release-notes/.FSharp.Core/11.0.100.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md index 884146bc4e4..baef8f73036 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -11,3 +11,4 @@ * `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)) * Added conversion functions `Task.ofValueTask` and `ValueTask.ofTask`. ([LanguageSuggestion #1466](https://github.com/fsharp/fslang-suggestions/issues/1466), [PR #19844](https://github.com/dotnet/fsharp/pull/19844)) +* Add `Async.Await`, which mirrors `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)) From 7da92ce12df82b52f4fee169d15fab009b3078cf Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Fri, 22 May 2026 14:32:34 +0100 Subject: [PATCH 02/12] feat(Async): StartTaskImmediate # Conflicts: # tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs --- docs/release-notes/.FSharp.Core/11.0.100.md | 3 + src/FSharp.Core/async.fs | 13 +++ src/FSharp.Core/async.fsi | 73 ++++++++++++ ...p.Core.SurfaceArea.netstandard20.debug.bsl | 2 + ...Core.SurfaceArea.netstandard20.release.bsl | 2 + ...p.Core.SurfaceArea.netstandard21.debug.bsl | 4 + ...Core.SurfaceArea.netstandard21.release.bsl | 4 + .../Microsoft.FSharp.Control/AsyncType.fs | 110 ++++++++++++++++++ 8 files changed, 211 insertions(+) diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md index baef8f73036..25cd0e53a08 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -12,3 +12,6 @@ * 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)) * Added conversion functions `Task.ofValueTask` and `ValueTask.ofTask`. ([LanguageSuggestion #1466](https://github.com/fsharp/fslang-suggestions/issues/1466), [PR #19844](https://github.com/dotnet/fsharp/pull/19844)) * Add `Async.Await`, which mirrors `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)) +* Add `Async.Await`, which mirrors `Async.AwaitTask` semantics, but elides egregious `AggregateException` wrapping. Also adds an SRTP 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)) +* Add `Async.Await`, which mirrors `Async.AwaitTask` semantics, but elides egregious `AggregateException` wrapping. ([Language Suggestion #840](https://github.com/fsharp/fslang-suggestions/issues/840), [PR #19785](https://github.com/dotnet/fsharp/pull/19785)) +* Add `Async.StartTaskImmediate`, which captures the ambient `CancellationToken`, passes it to a task factory, then awaits the result using `Async.Await` semantics. Overloads for `Task`, `Task<'T>`, `ValueTask`, and `ValueTask<'T>`. ([Language Suggestion #1284](https://github.com/fsharp/fslang-suggestions/issues/1284)) diff --git a/src/FSharp.Core/async.fs b/src/FSharp.Core/async.fs index e73dc4aa230..1ffb1de64a8 100644 --- a/src/FSharp.Core/async.fs +++ b/src/FSharp.Core/async.fs @@ -2268,6 +2268,19 @@ type Async = AwaitUnitTask true (task.AsTask()) #endif + static member StartTaskImmediate(createTask: CancellationToken -> Task<'T>) : Async<'T> = + CreateBindAsync Async.CancellationToken (createTask >> Async.Await) + static member StartTaskImmediate(createTask: CancellationToken -> Task) : Async = + CreateBindAsync Async.CancellationToken (createTask >> Async.Await) + +#if NETSTANDARD2_1 + static member StartTaskImmediate(createTask: CancellationToken -> ValueTask<'T>) : Async<'T> = + CreateBindAsync Async.CancellationToken (createTask >> Async.Await) + + static member StartTaskImmediate(createTask: CancellationToken -> ValueTask) : Async = + CreateBindAsync Async.CancellationToken (createTask >> Async.Await) +#endif + module AsyncTaskLikeExtensions = type Async with diff --git a/src/FSharp.Core/async.fsi b/src/FSharp.Core/async.fsi index 2171f75164a..9974dfae11c 100644 --- a/src/FSharp.Core/async.fsi +++ b/src/FSharp.Core/async.fsi @@ -983,6 +983,79 @@ namespace Microsoft.FSharp.Control static member Await: task: ValueTask -> Async #endif + /// Creates an asynchronous computation that captures the ambient CancellationToken, passes it to + /// createTask, and then awaits the resulting task, returning its result. + /// + /// A function that accepts a CancellationToken and returns a Task<'T>. + /// + /// The cancellation token of the enclosing async computation is automatically passed to + /// createTask, propagating cancellation naturally to the task without requiring manual token capture. + /// + /// The resulting task is awaited using ; + /// exception unwrapping and cancellation handling are as per that overload. + /// + /// Starting Async Computations + /// + /// + /// async { + /// let! text = Async.StartTaskImmediate(fun ct -> File.ReadAllTextAsync("file.txt", ct)) + /// printfn "Content: %s" text + /// } + /// + /// + static member StartTaskImmediate: createTask: (CancellationToken -> Task<'T>) -> Async<'T> + + /// Creates an asynchronous computation that captures the ambient CancellationToken, passes it to + /// createTask, and then awaits the resulting task. + /// + /// A function that accepts a CancellationToken and returns a Task. + /// + /// The cancellation token of the enclosing async computation is automatically passed to + /// createTask, propagating cancellation naturally to the task without requiring manual token capture. + /// + /// The resulting task is awaited using ; + /// exception unwrapping and cancellation handling are as per that overload. + /// + /// Starting Async Computations + /// + /// + /// async { + /// do! Async.StartTaskImmediate(fun ct -> File.WriteAllTextAsync("file.txt", "hello", ct)) + /// } + /// + /// + static member StartTaskImmediate: createTask: (CancellationToken -> Task) -> Async + +#if NETSTANDARD2_1 + /// Creates an asynchronous computation that captures the ambient CancellationToken, passes it to + /// createTask, and then awaits the resulting ValueTask, returning its result. + /// + /// A function that accepts a CancellationToken and returns a ValueTask<'T>. + /// + /// The cancellation token of the enclosing async computation is automatically passed to + /// createTask, propagating cancellation naturally to the task without requiring manual token capture. + /// + /// The resulting task is awaited using ; + /// exception unwrapping and cancellation handling are as per that overload. + /// + /// Starting Async Computations + static member StartTaskImmediate: createTask: (CancellationToken -> ValueTask<'T>) -> Async<'T> + + /// Creates an asynchronous computation that captures the ambient CancellationToken, passes it to + /// createTask, and then awaits the resulting ValueTask. + /// + /// A function that accepts a CancellationToken and returns a ValueTask. + /// + /// The cancellation token of the enclosing async computation is automatically passed to + /// createTask, propagating cancellation naturally to the task without requiring manual token capture. + /// + /// The resulting task is awaited using ; + /// exception unwrapping and cancellation handling are as per that overload. + /// + /// Starting Async Computations + static member StartTaskImmediate: createTask: (CancellationToken -> ValueTask) -> Async + +#endif /// /// Creates an asynchronous computation that will sleep for the given time. This is scheduled /// using a System.Threading.Timer object. The operation will not block operating system threads 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..52bcff8a83c 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl @@ -657,6 +657,7 @@ Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Mic Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Ignore[T](Microsoft.FSharp.Control.FSharpAsync`1[T]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Sleep(Int32) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Sleep(System.TimeSpan) +Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] StartTaskImmediate(Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,System.Threading.Tasks.Task]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] SwitchToContext(System.Threading.SynchronizationContext) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] SwitchToNewThread() Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] SwitchToThreadPool() @@ -677,6 +678,7 @@ Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromBeginEnd[TArg1,T](TArg1, Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`3[TArg1,System.AsyncCallback,System.Object],System.IAsyncResult], Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromBeginEnd[T](Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`2[System.AsyncCallback,System.Object],System.IAsyncResult], Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromContinuations[T](Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`3[Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.Unit],Microsoft.FSharp.Core.FSharpFunc`2[System.Exception,Microsoft.FSharp.Core.Unit],Microsoft.FSharp.Core.FSharpFunc`2[System.OperationCanceledException,Microsoft.FSharp.Core.Unit]],Microsoft.FSharp.Core.Unit]) +Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] StartTaskImmediate[T](Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,System.Threading.Tasks.Task`1[T]]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] TryCancelled[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpFunc`2[System.OperationCanceledException,Microsoft.FSharp.Core.Unit]) Microsoft.FSharp.Control.FSharpAsync: System.Threading.CancellationToken DefaultCancellationToken Microsoft.FSharp.Control.FSharpAsync: System.Threading.CancellationToken get_DefaultCancellationToken() 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..6e38a636871 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl @@ -657,6 +657,7 @@ Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Mic Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Ignore[T](Microsoft.FSharp.Control.FSharpAsync`1[T]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Sleep(Int32) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Sleep(System.TimeSpan) +Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] StartTaskImmediate(Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,System.Threading.Tasks.Task]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] SwitchToContext(System.Threading.SynchronizationContext) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] SwitchToNewThread() Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] SwitchToThreadPool() @@ -677,6 +678,7 @@ Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromBeginEnd[TArg1,T](TArg1, Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`3[TArg1,System.AsyncCallback,System.Object],System.IAsyncResult], Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromBeginEnd[T](Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`2[System.AsyncCallback,System.Object],System.IAsyncResult], Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromContinuations[T](Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`3[Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.Unit],Microsoft.FSharp.Core.FSharpFunc`2[System.Exception,Microsoft.FSharp.Core.Unit],Microsoft.FSharp.Core.FSharpFunc`2[System.OperationCanceledException,Microsoft.FSharp.Core.Unit]],Microsoft.FSharp.Core.Unit]) +Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] StartTaskImmediate[T](Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,System.Threading.Tasks.Task`1[T]]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] TryCancelled[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpFunc`2[System.OperationCanceledException,Microsoft.FSharp.Core.Unit]) Microsoft.FSharp.Control.FSharpAsync: System.Threading.CancellationToken DefaultCancellationToken Microsoft.FSharp.Control.FSharpAsync: System.Threading.CancellationToken get_DefaultCancellationToken() 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..6a3dec18e5f 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl @@ -660,6 +660,8 @@ Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Mic Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Ignore[T](Microsoft.FSharp.Control.FSharpAsync`1[T]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Sleep(Int32) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Sleep(System.TimeSpan) +Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] StartTaskImmediate(Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,System.Threading.Tasks.Task]) +Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] StartTaskImmediate(Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,System.Threading.Tasks.ValueTask]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] SwitchToContext(System.Threading.SynchronizationContext) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] SwitchToNewThread() Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] SwitchToThreadPool() @@ -681,6 +683,8 @@ Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromBeginEnd[TArg1,T](TArg1, Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`3[TArg1,System.AsyncCallback,System.Object],System.IAsyncResult], Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromBeginEnd[T](Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`2[System.AsyncCallback,System.Object],System.IAsyncResult], Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromContinuations[T](Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`3[Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.Unit],Microsoft.FSharp.Core.FSharpFunc`2[System.Exception,Microsoft.FSharp.Core.Unit],Microsoft.FSharp.Core.FSharpFunc`2[System.OperationCanceledException,Microsoft.FSharp.Core.Unit]],Microsoft.FSharp.Core.Unit]) +Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] StartTaskImmediate[T](Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,System.Threading.Tasks.Task`1[T]]) +Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] StartTaskImmediate[T](Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,System.Threading.Tasks.ValueTask`1[T]]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] TryCancelled[T](Microsoft.FSharp.Control.FSharpAsync`1[T], Microsoft.FSharp.Core.FSharpFunc`2[System.OperationCanceledException,Microsoft.FSharp.Core.Unit]) Microsoft.FSharp.Control.FSharpAsync: System.Threading.CancellationToken DefaultCancellationToken Microsoft.FSharp.Control.FSharpAsync: System.Threading.CancellationToken get_DefaultCancellationToken() 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..89fbc8dc499 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl @@ -660,6 +660,8 @@ Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Mic Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Ignore[T](Microsoft.FSharp.Control.FSharpAsync`1[T]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Sleep(Int32) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] Sleep(System.TimeSpan) +Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] StartTaskImmediate(Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,System.Threading.Tasks.Task]) +Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] StartTaskImmediate(Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,System.Threading.Tasks.ValueTask]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] SwitchToContext(System.Threading.SynchronizationContext) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] SwitchToNewThread() Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] SwitchToThreadPool() @@ -676,6 +678,8 @@ Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] Await[T](System.Threading.Tasks.Task`1[T]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] Await[T](System.Threading.Tasks.ValueTask`1[T]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] AwaitTask[T](System.Threading.Tasks.Task`1[T]) +Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] StartTaskImmediate[T](Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,System.Threading.Tasks.Task`1[T]]) +Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] StartTaskImmediate[T](Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,System.Threading.Tasks.ValueTask`1[T]]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromBeginEnd[TArg1,TArg2,TArg3,T](TArg1, TArg2, TArg3, Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`5[TArg1,TArg2,TArg3,System.AsyncCallback,System.Object],System.IAsyncResult], Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromBeginEnd[TArg1,TArg2,T](TArg1, TArg2, Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`4[TArg1,TArg2,System.AsyncCallback,System.Object],System.IAsyncResult], Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]]) Microsoft.FSharp.Control.FSharpAsync: Microsoft.FSharp.Control.FSharpAsync`1[T] FromBeginEnd[TArg1,T](TArg1, Microsoft.FSharp.Core.FSharpFunc`2[System.Tuple`3[TArg1,System.AsyncCallback,System.Object],System.IAsyncResult], Microsoft.FSharp.Core.FSharpFunc`2[System.IAsyncResult,T], Microsoft.FSharp.Core.FSharpOption`1[Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit]]) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs index 2ce61e65968..1becd4e782b 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs @@ -653,6 +653,74 @@ type AsyncType() = let ok = Async.RunSynchronously a Assert.True ok + (* StartTaskImmediate(Task/Task<'T>) coverage *) + + [] + member _.``StartTaskImmediate(Task<'T>) flows result``() = + let a = async { + let! v = Async.StartTaskImmediate(fun _ct -> Task.FromResult(42)) + return v = 42 + } + Assert.True(Async.RunSynchronously a) + + [] + member _.``StartTaskImmediate(Task) happy path``() = + let mutable called = false + let a = async { + do! Async.StartTaskImmediate(fun _ct -> Task.Run(fun () -> called <- true)) + return true + } + let ok = Async.RunSynchronously a + Assert.True(called && ok) + + [] + member _.``StartTaskImmediate flows CancellationToken``() = + let cts = new CancellationTokenSource() + let capturedCt = ref CancellationToken.None + let a = async { + do! Async.StartTaskImmediate(fun ct -> + capturedCt.Value <- ct + Task.CompletedTask) + } + Async.RunSynchronously(a, cancellationToken = cts.Token) + Assert.Equal(cts.Token, capturedCt.Value) + + [] + member _.``StartTaskImmediate(Task<'T>) exception unwraps``() = + let tcs = TaskCompletionSource() + tcs.SetException(ArgumentException "original") + let a = async { + try let! _ = Async.StartTaskImmediate(fun _ct -> tcs.Task) + return false + with :? ArgumentException as ae -> return ae.Message = "original" + } + let ok = Async.RunSynchronously a + Assert.True ok + + [] + member _.``StartTaskImmediate(Task) exception unwraps``() = + let tcs = TaskCompletionSource() + tcs.SetException(ArgumentException "original") + let a = async { + try do! Async.StartTaskImmediate(fun _ct -> tcs.Task :> Task) + return false + with :? ArgumentException as ae -> return ae.Message = "original" + } + let ok = Async.RunSynchronously a + Assert.True ok + + [] + member _.``StartTaskImmediate(Task<'T>) cancellation raises TaskCanceledException``() = + let tcs = TaskCompletionSource() + tcs.SetCanceled() + let a = async { + try let! _ = Async.StartTaskImmediate(fun _ct -> tcs.Task) + return false + with :? TaskCanceledException -> return true + } + let ok = Async.RunSynchronously a + Assert.True ok + #if !NETFRAMEWORK (* Await(ValueTask and ValueTask<'T>) overloads coverage of mainline behaviors *) @@ -698,6 +766,48 @@ type AsyncType() = } let ok = Async.RunSynchronously a Assert.True ok + + (* StartTaskImmediate(ValueTask/ValueTask<'T>) coverage *) + + [] + member _.``StartTaskImmediate(ValueTask<'T>) flows result``() = + let a = async { + let! v = Async.StartTaskImmediate(fun _ct -> ValueTask(42)) + return v = 42 + } + Assert.True(Async.RunSynchronously a) + + [] + member _.``StartTaskImmediate(ValueTask) happy path``() = + let a = async { + do! Async.StartTaskImmediate(fun _ct -> ValueTask()) + return true + } + Assert.True(Async.RunSynchronously a) + + [] + member _.``StartTaskImmediate(ValueTask<'T>) exception unwraps``() = + let tcs = TaskCompletionSource() + tcs.SetException(ArgumentException "original") + let a = async { + try + let! _ = Async.StartTaskImmediate(fun _ct -> ValueTask(tcs.Task)) + return false + with :? ArgumentException as ae -> return ae.Message = "original" + } + Assert.True(Async.RunSynchronously a) + + [] + member _.``StartTaskImmediate(ValueTask) exception unwraps``() = + let tcs = TaskCompletionSource() + tcs.SetException(ArgumentException "original") + let a = async { + try + do! Async.StartTaskImmediate(fun _ct -> ValueTask(tcs.Task :> Task)) + return false + with :? ArgumentException as ae -> return ae.Message = "original" + } + Assert.True(Async.RunSynchronously a) #endif [] From 1ee88256e27a88b462a7aca1afdec42af06467ea Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 25 May 2026 17:55:41 +0100 Subject: [PATCH 03/12] Add Tasklike overload --- src/FSharp.Core/async.fs | 19 +++++-- src/FSharp.Core/async.fsi | 37 ++++++++++++++ ...p.Core.SurfaceArea.netstandard20.debug.bsl | 2 + ...Core.SurfaceArea.netstandard20.release.bsl | 2 + ...p.Core.SurfaceArea.netstandard21.debug.bsl | 2 + ...Core.SurfaceArea.netstandard21.release.bsl | 2 + .../Microsoft.FSharp.Control/AsyncType.fs | 49 +++++++++++++++++-- 7 files changed, 106 insertions(+), 7 deletions(-) diff --git a/src/FSharp.Core/async.fs b/src/FSharp.Core/async.fs index 1ffb1de64a8..bebe104e830 100644 --- a/src/FSharp.Core/async.fs +++ b/src/FSharp.Core/async.fs @@ -2269,16 +2269,17 @@ type Async = #endif static member StartTaskImmediate(createTask: CancellationToken -> Task<'T>) : Async<'T> = - CreateBindAsync Async.CancellationToken (createTask >> Async.Await) + CreateBindAsync Async.CancellationToken (createTask >> Async.Await) + static member StartTaskImmediate(createTask: CancellationToken -> Task) : Async = - CreateBindAsync Async.CancellationToken (createTask >> Async.Await) + CreateBindAsync Async.CancellationToken (createTask >> Async.Await) #if NETSTANDARD2_1 static member StartTaskImmediate(createTask: CancellationToken -> ValueTask<'T>) : Async<'T> = - CreateBindAsync Async.CancellationToken (createTask >> Async.Await) + CreateBindAsync Async.CancellationToken (createTask >> Async.Await) static member StartTaskImmediate(createTask: CancellationToken -> ValueTask) : Async = - CreateBindAsync Async.CancellationToken (createTask >> Async.Await) + CreateBindAsync Async.CancellationToken (createTask >> Async.Await) #endif module AsyncTaskLikeExtensions = @@ -2309,6 +2310,16 @@ module AsyncTaskLikeExtensions = with e -> econt e)) + [] + static member inline StartTaskImmediate< ^TaskLike, ^Awaiter, 'T + when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter) + and ^Awaiter :> ICriticalNotifyCompletion + and ^Awaiter: (member get_IsCompleted: unit -> bool) + and ^Awaiter: (member GetResult: unit -> 'T)> + (createTask: CancellationToken -> ^TaskLike) + : Async<'T> = + CreateBindAsync Async.CancellationToken (createTask >> Async.Await) + module CommonExtensions = type System.IO.Stream with diff --git a/src/FSharp.Core/async.fsi b/src/FSharp.Core/async.fsi index 9974dfae11c..25360a65682 100644 --- a/src/FSharp.Core/async.fsi +++ b/src/FSharp.Core/async.fsi @@ -1403,6 +1403,43 @@ namespace Microsoft.FSharp.Control and ^Awaiter: (member get_IsCompleted: unit -> bool) and ^Awaiter: (member GetResult: unit -> 'T) + /// Creates an asynchronous computation that passes the ambient CancellationToken, to + /// createTask, and then awaits the resulting task-like value. + /// + /// A function that accepts a CancellationToken and returns a task-like value + /// satisfying the GetAwaiter pattern. + /// + /// The value returned by createTask must satisfy the GetAwaiter pattern: it must have a + /// GetAwaiter() method returning an awaiter implementing + /// + /// with IsCompleted and GetResult() members. + /// + /// This overload uses statically resolved type parameters (SRTP) so it can accept factories returning + /// any task-like type, including YieldAwaitable (from Task.Yield()) and + /// ConfiguredTaskAwaitable (from task.ConfigureAwait(false)). + /// The specific overloads for , , + /// and + /// are preferred when the factory return type is known. + /// + /// Starting Async Computations + /// + /// + /// async { + /// // Yield to other work and resume, without capturing a SynchronizationContext + /// do! Async.StartTaskImmediate(fun _ct -> Task.Yield()) + /// let! v = Async.StartTaskImmediate(fun _ct -> Task.FromResult(42).ConfigureAwait(false)) + /// printfn $"Result: {v}" + /// } |> Async.RunSynchronously + /// + /// + [] + static member inline StartTaskImmediate< ^TaskLike, ^Awaiter, 'T> : + createTask: (CancellationToken -> ^TaskLike) -> Async<'T> + when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter) + and ^Awaiter :> ICriticalNotifyCompletion + and ^Awaiter: (member get_IsCompleted: unit -> bool) + and ^Awaiter: (member GetResult: unit -> 'T) + /// The F# compiler emits references to this type to implement F# async expressions. /// /// Async Internals 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 52bcff8a83c..da9227f2a63 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl @@ -633,6 +633,8 @@ Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.AsyncReturn T Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.FSharpAsync`1[T] MakeAsync[T](Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Control.AsyncActivation`1[T],Microsoft.FSharp.Control.AsyncReturn]) Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.Await.Static$W[TTaskLike,TAwaiter,T](Microsoft.FSharp.Core.FSharpFunc`2[TTaskLike,TAwaiter], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,T], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,System.Boolean], TTaskLike) Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.Await.Static[TTaskLike,TAwaiter,T](TTaskLike) +Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.StartTaskImmediate.Static$W[TTaskLike,TAwaiter,T](Microsoft.FSharp.Core.FSharpFunc`2[TTaskLike,TAwaiter], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,T], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,System.Boolean], Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,TTaskLike]) +Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.StartTaskImmediate.Static[TTaskLike,TAwaiter,T](Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,TTaskLike]) Microsoft.FSharp.Control.BackgroundTaskBuilder: System.Threading.Tasks.Task`1[T] RunDynamic[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T]) Microsoft.FSharp.Control.BackgroundTaskBuilder: System.Threading.Tasks.Task`1[T] Run[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T]) Microsoft.FSharp.Control.CommonExtensions: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] AsyncWrite(System.IO.Stream, Byte[], Microsoft.FSharp.Core.FSharpOption`1[System.Int32], Microsoft.FSharp.Core.FSharpOption`1[System.Int32]) 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 6e38a636871..b0d3ebc9665 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl @@ -633,6 +633,8 @@ Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.AsyncReturn T Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.FSharpAsync`1[T] MakeAsync[T](Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Control.AsyncActivation`1[T],Microsoft.FSharp.Control.AsyncReturn]) Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.Await.Static$W[TTaskLike,TAwaiter,T](Microsoft.FSharp.Core.FSharpFunc`2[TTaskLike,TAwaiter], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,T], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,System.Boolean], TTaskLike) Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.Await.Static[TTaskLike,TAwaiter,T](TTaskLike) +Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.StartTaskImmediate.Static$W[TTaskLike,TAwaiter,T](Microsoft.FSharp.Core.FSharpFunc`2[TTaskLike,TAwaiter], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,T], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,System.Boolean], Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,TTaskLike]) +Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.StartTaskImmediate.Static[TTaskLike,TAwaiter,T](Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,TTaskLike]) Microsoft.FSharp.Control.BackgroundTaskBuilder: System.Threading.Tasks.Task`1[T] RunDynamic[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T]) Microsoft.FSharp.Control.BackgroundTaskBuilder: System.Threading.Tasks.Task`1[T] Run[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T]) Microsoft.FSharp.Control.CommonExtensions: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] AsyncWrite(System.IO.Stream, Byte[], Microsoft.FSharp.Core.FSharpOption`1[System.Int32], Microsoft.FSharp.Core.FSharpOption`1[System.Int32]) 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 6a3dec18e5f..174a09bf2c0 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl @@ -635,6 +635,8 @@ Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.AsyncReturn T Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.FSharpAsync`1[T] MakeAsync[T](Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Control.AsyncActivation`1[T],Microsoft.FSharp.Control.AsyncReturn]) Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.Await.Static$W[TTaskLike,TAwaiter,T](Microsoft.FSharp.Core.FSharpFunc`2[TTaskLike,TAwaiter], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,T], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,System.Boolean], TTaskLike) Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.Await.Static[TTaskLike,TAwaiter,T](TTaskLike) +Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.StartTaskImmediate.Static$W[TTaskLike,TAwaiter,T](Microsoft.FSharp.Core.FSharpFunc`2[TTaskLike,TAwaiter], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,T], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,System.Boolean], Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,TTaskLike]) +Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.StartTaskImmediate.Static[TTaskLike,TAwaiter,T](Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,TTaskLike]) Microsoft.FSharp.Control.BackgroundTaskBuilder: System.Threading.Tasks.Task`1[T] RunDynamic[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T]) Microsoft.FSharp.Control.BackgroundTaskBuilder: System.Threading.Tasks.Task`1[T] Run[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T]) Microsoft.FSharp.Control.CommonExtensions: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] AsyncWrite(System.IO.Stream, Byte[], Microsoft.FSharp.Core.FSharpOption`1[System.Int32], Microsoft.FSharp.Core.FSharpOption`1[System.Int32]) 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 89fbc8dc499..ffe81d5ba6b 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl @@ -635,6 +635,8 @@ Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.AsyncReturn T Microsoft.FSharp.Control.AsyncPrimitives: Microsoft.FSharp.Control.FSharpAsync`1[T] MakeAsync[T](Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Control.AsyncActivation`1[T],Microsoft.FSharp.Control.AsyncReturn]) Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.Await.Static$W[TTaskLike,TAwaiter,T](Microsoft.FSharp.Core.FSharpFunc`2[TTaskLike,TAwaiter], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,T], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,System.Boolean], TTaskLike) Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.Await.Static[TTaskLike,TAwaiter,T](TTaskLike) +Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.StartTaskImmediate.Static$W[TTaskLike,TAwaiter,T](Microsoft.FSharp.Core.FSharpFunc`2[TTaskLike,TAwaiter], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,T], Microsoft.FSharp.Core.FSharpFunc`2[TAwaiter,System.Boolean], Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,TTaskLike]) +Microsoft.FSharp.Control.AsyncTaskLikeExtensions: Microsoft.FSharp.Control.FSharpAsync`1[T] Async.StartTaskImmediate.Static[TTaskLike,TAwaiter,T](Microsoft.FSharp.Core.FSharpFunc`2[System.Threading.CancellationToken,TTaskLike]) Microsoft.FSharp.Control.BackgroundTaskBuilder: System.Threading.Tasks.Task`1[T] RunDynamic[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T]) Microsoft.FSharp.Control.BackgroundTaskBuilder: System.Threading.Tasks.Task`1[T] Run[T](Microsoft.FSharp.Core.CompilerServices.ResumableCode`2[Microsoft.FSharp.Control.TaskStateMachineData`1[T],T]) Microsoft.FSharp.Control.CommonExtensions: Microsoft.FSharp.Control.FSharpAsync`1[Microsoft.FSharp.Core.Unit] AsyncWrite(System.IO.Stream, Byte[], Microsoft.FSharp.Core.FSharpOption`1[System.Int32], Microsoft.FSharp.Core.FSharpOption`1[System.Int32]) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs index 1becd4e782b..cafc916cb54 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs @@ -676,14 +676,14 @@ type AsyncType() = [] member _.``StartTaskImmediate flows CancellationToken``() = let cts = new CancellationTokenSource() - let capturedCt = ref CancellationToken.None + let mutable capturedCt = CancellationToken.None let a = async { do! Async.StartTaskImmediate(fun ct -> - capturedCt.Value <- ct + capturedCt <- ct Task.CompletedTask) } Async.RunSynchronously(a, cancellationToken = cts.Token) - Assert.Equal(cts.Token, capturedCt.Value) + Assert.Equal(cts.Token, capturedCt) [] member _.``StartTaskImmediate(Task<'T>) exception unwraps``() = @@ -914,6 +914,49 @@ module AsyncTaskLikeAwaitTests = |> Async.RunSynchronously Assert.Equal(42, result) +[] +module AsyncStartTaskImmediateTaskLikeTests = + + [] + let ``StartTaskImmediate(YieldAwaitable factory) yields and resumes``() = + // Task.Yield() returns a struct YieldAwaitable — exercises the struct-awaiter SRTP path. + let mutable before, after = false, false + + async { + before <- true + do! Async.StartTaskImmediate(fun _ -> Task.Yield()) + after <- true + } + |> Async.RunSynchronously + + Assert.True(before && after) + + [] + let ``StartTaskImmediate(ConfiguredTaskAwaitable factory) returns result``() = + // ConfigureAwait(false) returns a ConfiguredTaskAwaitable — a common real-world task-like. + let result = + async { + return! Async.StartTaskImmediate(fun _ -> Task.FromResult(42).ConfigureAwait(false)) + } + |> Async.RunSynchronously + + Assert.Equal(42, result) + + [] + let ``StartTaskImmediate flows CancellationToken``() = + // The factory receives the ambient cancellation token from the enclosing async. + let mutable capturedCt = CancellationToken.None + use cts = new CancellationTokenSource() + + let a = async { + do! Async.StartTaskImmediate(fun ct -> + capturedCt <- ct + Task.CompletedTask.ConfigureAwait(false)) + } + Async.RunSynchronously(a, cancellationToken = cts.Token) + + Assert.Equal(cts.Token, capturedCt) + [] module AsyncAwaitStackTraceTests = From 6d00748ab04122bf67305b2d5f7eb90c2637c402 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 25 May 2026 18:23:54 +0100 Subject: [PATCH 04/12] Polish xmldoc --- docs/release-notes/.FSharp.Core/11.0.100.md | 4 +-- src/FSharp.Core/async.fsi | 28 ++++++++++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md index 25cd0e53a08..916fe1cda95 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -12,6 +12,4 @@ * 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)) * Added conversion functions `Task.ofValueTask` and `ValueTask.ofTask`. ([LanguageSuggestion #1466](https://github.com/fsharp/fslang-suggestions/issues/1466), [PR #19844](https://github.com/dotnet/fsharp/pull/19844)) * Add `Async.Await`, which mirrors `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)) -* Add `Async.Await`, which mirrors `Async.AwaitTask` semantics, but elides egregious `AggregateException` wrapping. Also adds an SRTP 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)) -* Add `Async.Await`, which mirrors `Async.AwaitTask` semantics, but elides egregious `AggregateException` wrapping. ([Language Suggestion #840](https://github.com/fsharp/fslang-suggestions/issues/840), [PR #19785](https://github.com/dotnet/fsharp/pull/19785)) -* Add `Async.StartTaskImmediate`, which captures the ambient `CancellationToken`, passes it to a task factory, then awaits the result using `Async.Await` semantics. Overloads for `Task`, `Task<'T>`, `ValueTask`, and `ValueTask<'T>`. ([Language Suggestion #1284](https://github.com/fsharp/fslang-suggestions/issues/1284)) +* Add `Async.StartTaskImmediate`, which propagates the ambient `Async.CancellationToken`, to a task factory, then awaits the result using `Async.Await` semantics. Overloads for `Task`, `Task<'T>`, `ValueTask`, `ValueTask<'T>` and task-like `.GetAwaiter()` (via SRTP). ([Language Suggestion #1284](https://github.com/fsharp/fslang-suggestions/issues/1284)) diff --git a/src/FSharp.Core/async.fsi b/src/FSharp.Core/async.fsi index 25360a65682..3836fcf54f5 100644 --- a/src/FSharp.Core/async.fsi +++ b/src/FSharp.Core/async.fsi @@ -983,7 +983,7 @@ namespace Microsoft.FSharp.Control static member Await: task: ValueTask -> Async #endif - /// Creates an asynchronous computation that captures the ambient CancellationToken, passes it to + /// Creates an asynchronous computation that passes the ambient Async.CancellationToken to /// createTask, and then awaits the resulting task, returning its result. /// /// A function that accepts a CancellationToken and returns a Task<'T>. @@ -1005,7 +1005,7 @@ namespace Microsoft.FSharp.Control /// static member StartTaskImmediate: createTask: (CancellationToken -> Task<'T>) -> Async<'T> - /// Creates an asynchronous computation that captures the ambient CancellationToken, passes it to + /// Creates an asynchronous computation that passes the ambient Async.CancellationToken to /// createTask, and then awaits the resulting task. /// /// A function that accepts a CancellationToken and returns a Task. @@ -1027,7 +1027,7 @@ namespace Microsoft.FSharp.Control static member StartTaskImmediate: createTask: (CancellationToken -> Task) -> Async #if NETSTANDARD2_1 - /// Creates an asynchronous computation that captures the ambient CancellationToken, passes it to + /// Creates an asynchronous computation that captures the ambient Async.CancellationToken to /// createTask, and then awaits the resulting ValueTask, returning its result. /// /// A function that accepts a CancellationToken and returns a ValueTask<'T>. @@ -1041,7 +1041,7 @@ namespace Microsoft.FSharp.Control /// Starting Async Computations static member StartTaskImmediate: createTask: (CancellationToken -> ValueTask<'T>) -> Async<'T> - /// Creates an asynchronous computation that captures the ambient CancellationToken, passes it to + /// Creates an asynchronous computation that passes the ambient CancellationToken to /// createTask, and then awaits the resulting ValueTask. /// /// A function that accepts a CancellationToken and returns a ValueTask. @@ -1403,7 +1403,7 @@ namespace Microsoft.FSharp.Control and ^Awaiter: (member get_IsCompleted: unit -> bool) and ^Awaiter: (member GetResult: unit -> 'T) - /// Creates an asynchronous computation that passes the ambient CancellationToken, to + /// Creates an asynchronous computation that passes the ambient CancellationToken to /// createTask, and then awaits the resulting task-like value. /// /// A function that accepts a CancellationToken and returns a task-like value @@ -1424,11 +1424,21 @@ namespace Microsoft.FSharp.Control /// Starting Async Computations /// /// + /// // Straightforward: factory returns Task<string>, which is handled by + /// // the specific Task<'T> overload of StartTaskImmediate (not this one). + /// let fetchPlain (url: string) = + /// Async.StartTaskImmediate(fun ct -> + /// httpClient.GetStringAsync(url, ct)) // returns Task<string> + /// + /// // Adding ConfigureAwait(false) to the mix yields a ConfiguredTaskAwaitable<string>, + /// // which has no specific overload — this SRTP overload handles it. + /// let fetchConfigured (url: string) = + /// Async.StartTaskImmediate(fun ct -> + /// httpClient.GetStringAsync(url, ct).ConfigureAwait(false)) + /// /// async { - /// // Yield to other work and resume, without capturing a SynchronizationContext - /// do! Async.StartTaskImmediate(fun _ct -> Task.Yield()) - /// let! v = Async.StartTaskImmediate(fun _ct -> Task.FromResult(42).ConfigureAwait(false)) - /// printfn $"Result: {v}" + /// let! html = fetchConfigured "https://example.com" + /// printfn $"Downloaded {html.Length} chars" /// } |> Async.RunSynchronously /// /// From cf9aee81da07025cbb7d45dbded8a708fd67a6e6 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Tue, 26 May 2026 23:42:56 +0100 Subject: [PATCH 05/12] Address copilot review --- docs/release-notes/.FSharp.Core/11.0.100.md | 2 +- src/FSharp.Core/async.fsi | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md index 916fe1cda95..e18e415092a 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -12,4 +12,4 @@ * 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)) * Added conversion functions `Task.ofValueTask` and `ValueTask.ofTask`. ([LanguageSuggestion #1466](https://github.com/fsharp/fslang-suggestions/issues/1466), [PR #19844](https://github.com/dotnet/fsharp/pull/19844)) * Add `Async.Await`, which mirrors `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)) -* Add `Async.StartTaskImmediate`, which propagates the ambient `Async.CancellationToken`, to a task factory, then awaits the result using `Async.Await` semantics. Overloads for `Task`, `Task<'T>`, `ValueTask`, `ValueTask<'T>` and task-like `.GetAwaiter()` (via SRTP). ([Language Suggestion #1284](https://github.com/fsharp/fslang-suggestions/issues/1284)) +* Add `Async.StartTaskImmediate`, which propagates the ambient `Async.CancellationToken` to a task factory, then awaits the result using `Async.Await` semantics. Overloads for `Task`, `Task<'T>`, `ValueTask`, `ValueTask<'T>` and task-like `.GetAwaiter()` (via SRTP). ([Language Suggestion #1284](https://github.com/fsharp/fslang-suggestions/issues/1284)) diff --git a/src/FSharp.Core/async.fsi b/src/FSharp.Core/async.fsi index 3836fcf54f5..123c0336d34 100644 --- a/src/FSharp.Core/async.fsi +++ b/src/FSharp.Core/async.fsi @@ -1027,7 +1027,7 @@ namespace Microsoft.FSharp.Control static member StartTaskImmediate: createTask: (CancellationToken -> Task) -> Async #if NETSTANDARD2_1 - /// Creates an asynchronous computation that captures the ambient Async.CancellationToken to + /// Creates an asynchronous computation that passes the ambient Async.CancellationToken to /// createTask, and then awaits the resulting ValueTask, returning its result. /// /// A function that accepts a CancellationToken and returns a ValueTask<'T>. @@ -1041,7 +1041,7 @@ namespace Microsoft.FSharp.Control /// Starting Async Computations static member StartTaskImmediate: createTask: (CancellationToken -> ValueTask<'T>) -> Async<'T> - /// Creates an asynchronous computation that passes the ambient CancellationToken to + /// Creates an asynchronous computation that passes the ambient Async.CancellationToken to /// createTask, and then awaits the resulting ValueTask. /// /// A function that accepts a CancellationToken and returns a ValueTask. @@ -1403,7 +1403,7 @@ namespace Microsoft.FSharp.Control and ^Awaiter: (member get_IsCompleted: unit -> bool) and ^Awaiter: (member GetResult: unit -> 'T) - /// Creates an asynchronous computation that passes the ambient CancellationToken to + /// Creates an asynchronous computation that passes the ambient Async.CancellationToken to /// createTask, and then awaits the resulting task-like value. /// /// A function that accepts a CancellationToken and returns a task-like value From 19749b0ddc316d784fa67a4807b4ec3e4b0466a4 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 13 Aug 2026 15:14:55 +0100 Subject: [PATCH 06/12] doc: Tidy release notes --- docs/release-notes/.FSharp.Core/11.0.100.md | 9 ++++----- src/FSharp.Core/async.fsi | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md index e18e415092a..ba44cd453d8 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -8,8 +8,7 @@ ### Added * 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)) -* Added conversion functions `Task.ofValueTask` and `ValueTask.ofTask`. ([LanguageSuggestion #1466](https://github.com/fsharp/fslang-suggestions/issues/1466), [PR #19844](https://github.com/dotnet/fsharp/pull/19844)) -* Add `Async.Await`, which mirrors `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)) -* Add `Async.StartTaskImmediate`, which propagates the ambient `Async.CancellationToken` to a task factory, then awaits the result using `Async.Await` semantics. Overloads for `Task`, `Task<'T>`, `ValueTask`, `ValueTask<'T>` and task-like `.GetAwaiter()` (via SRTP). ([Language Suggestion #1284](https://github.com/fsharp/fslang-suggestions/issues/1284)) +* Add `Async.StartTaskImmediate`, which propagates the ambient `Async.CancellationToken` to a task factory, then awaits the result using `Async.Await` semantics. Overloads for `Task`, `Task<'T>`, `ValueTask`, `ValueTask<'T>` and task-like `.GetAwaiter()` (via SRTP). ([Language Suggestion #1284](https://github.com/fsharp/fslang-suggestions/issues/1284), [PR #20258](https://github.com/dotnet/fsharp/pull/20258)) +* Add `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)) +* Add 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)) +* Add conversion functions `Task.ofValueTask` and `ValueTask.ofTask`. ([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/async.fsi b/src/FSharp.Core/async.fsi index 123c0336d34..a8f8f307ad6 100644 --- a/src/FSharp.Core/async.fsi +++ b/src/FSharp.Core/async.fsi @@ -1439,7 +1439,7 @@ namespace Microsoft.FSharp.Control /// async { /// let! html = fetchConfigured "https://example.com" /// printfn $"Downloaded {html.Length} chars" - /// } |> Async.RunSynchronously + /// } |> Async.RunSynchronouslyImmediate /// /// [] From 0540c495a171788afb6c63f234e7e6ac0cfb08e2 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 13 Aug 2026 17:17:07 +0100 Subject: [PATCH 07/12] review: tidy tests --- .../Microsoft.FSharp.Control/AsyncType.fs | 59 ++++++++----------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs index cafc916cb54..9b33aaedb77 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs @@ -653,36 +653,33 @@ type AsyncType() = let ok = Async.RunSynchronously a Assert.True ok - (* StartTaskImmediate(Task/Task<'T>) coverage *) + (* StartTaskImmediate(Task/Task<'T>) *) [] member _.``StartTaskImmediate(Task<'T>) flows result``() = let a = async { - let! v = Async.StartTaskImmediate(fun _ct -> Task.FromResult(42)) + let! v = Async.StartTaskImmediate(fun _ct -> Task.result 42) return v = 42 } - Assert.True(Async.RunSynchronously a) + Assert.True(Async.RunSynchronouslyImmediate a) [] member _.``StartTaskImmediate(Task) happy path``() = let mutable called = false let a = async { - do! Async.StartTaskImmediate(fun _ct -> Task.Run(fun () -> called <- true)) - return true + do! Async.StartTaskImmediate(fun _ct -> task { called <- true }) } - let ok = Async.RunSynchronously a - Assert.True(called && ok) + Async.RunSynchronouslyImmediate a + Assert.True called [] member _.``StartTaskImmediate flows CancellationToken``() = let cts = new CancellationTokenSource() let mutable capturedCt = CancellationToken.None let a = async { - do! Async.StartTaskImmediate(fun ct -> - capturedCt <- ct - Task.CompletedTask) + do! Async.StartTaskImmediate(fun ct -> task { capturedCt <- ct }) } - Async.RunSynchronously(a, cancellationToken = cts.Token) + Async.RunSynchronouslyImmediate(a, cancellationToken = cts.Token) Assert.Equal(cts.Token, capturedCt) [] @@ -694,8 +691,7 @@ type AsyncType() = return false with :? ArgumentException as ae -> return ae.Message = "original" } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) [] member _.``StartTaskImmediate(Task) exception unwraps``() = @@ -706,8 +702,7 @@ type AsyncType() = return false with :? ArgumentException as ae -> return ae.Message = "original" } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) [] member _.``StartTaskImmediate(Task<'T>) cancellation raises TaskCanceledException``() = @@ -718,8 +713,7 @@ type AsyncType() = return false with :? TaskCanceledException -> return true } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) #if !NETFRAMEWORK (* Await(ValueTask and ValueTask<'T>) overloads coverage of mainline behaviors *) @@ -730,8 +724,7 @@ type AsyncType() = do! Async.Await(ValueTask()) return true } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True (Async.RunSynchronouslyImmediate a) [] member _.``Await(ValueTask<'T>) happy path``() = @@ -739,8 +732,7 @@ type AsyncType() = let! v = Async.Await(ValueTask(42)) return v = 42 } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True (Async.RunSynchronouslyImmediate a) [] member _.``Await(ValueTask) exception unwraps``() = @@ -752,8 +744,7 @@ type AsyncType() = return false with :? ArgumentException as ae -> return ae.Message = "original" } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) [] member _.``Await(ValueTask<'T>) exception unwraps``() = @@ -764,10 +755,9 @@ type AsyncType() = return false with :? ArgumentException as ae -> return ae.Message = "original" } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) - (* StartTaskImmediate(ValueTask/ValueTask<'T>) coverage *) + (* StartTaskImmediate(ValueTask/ValueTask<'T>) *) [] member _.``StartTaskImmediate(ValueTask<'T>) flows result``() = @@ -775,7 +765,7 @@ type AsyncType() = let! v = Async.StartTaskImmediate(fun _ct -> ValueTask(42)) return v = 42 } - Assert.True(Async.RunSynchronously a) + Assert.True(Async.RunSynchronouslyImmediate a) [] member _.``StartTaskImmediate(ValueTask) happy path``() = @@ -783,7 +773,7 @@ type AsyncType() = do! Async.StartTaskImmediate(fun _ct -> ValueTask()) return true } - Assert.True(Async.RunSynchronously a) + Assert.True(Async.RunSynchronouslyImmediate a) [] member _.``StartTaskImmediate(ValueTask<'T>) exception unwraps``() = @@ -795,7 +785,7 @@ type AsyncType() = return false with :? ArgumentException as ae -> return ae.Message = "original" } - Assert.True(Async.RunSynchronously a) + Assert.True(Async.RunSynchronouslyImmediate a) [] member _.``StartTaskImmediate(ValueTask) exception unwraps``() = @@ -807,10 +797,9 @@ type AsyncType() = return false with :? ArgumentException as ae -> return ae.Message = "original" } - Assert.True(Async.RunSynchronously a) + Assert.True(Async.RunSynchronouslyImmediate a) #endif -[] module AsyncTaskLikeAwaitTests = // Minimal custom task-like type wrapping Task<'T> @@ -914,7 +903,6 @@ module AsyncTaskLikeAwaitTests = |> Async.RunSynchronously Assert.Equal(42, result) -[] module AsyncStartTaskImmediateTaskLikeTests = [] @@ -927,7 +915,7 @@ module AsyncStartTaskImmediateTaskLikeTests = do! Async.StartTaskImmediate(fun _ -> Task.Yield()) after <- true } - |> Async.RunSynchronously + |> Async.RunSynchronouslyImmediate Assert.True(before && after) @@ -938,7 +926,7 @@ module AsyncStartTaskImmediateTaskLikeTests = async { return! Async.StartTaskImmediate(fun _ -> Task.FromResult(42).ConfigureAwait(false)) } - |> Async.RunSynchronously + |> Async.RunSynchronouslyImmediate Assert.Equal(42, result) @@ -953,11 +941,10 @@ module AsyncStartTaskImmediateTaskLikeTests = capturedCt <- ct Task.CompletedTask.ConfigureAwait(false)) } - Async.RunSynchronously(a, cancellationToken = cts.Token) + Async.RunSynchronouslyImmediate(a, cancellationToken = cts.Token) Assert.Equal(cts.Token, capturedCt) -[] module AsyncAwaitStackTraceTests = open System.Runtime.CompilerServices From f777b5d5f2aa4daff11e249e580ffabbf46f687d Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 13 Aug 2026 17:38:42 +0100 Subject: [PATCH 08/12] doc: Add crossreference to StartTaskImmediate from Await/AwaitTask --- src/FSharp.Core/async.fsi | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/FSharp.Core/async.fsi b/src/FSharp.Core/async.fsi index a8f8f307ad6..0f12de7a1b3 100644 --- a/src/FSharp.Core/async.fsi +++ b/src/FSharp.Core/async.fsi @@ -782,12 +782,16 @@ namespace Microsoft.FSharp.Control /// its result. Note exceptions are wrapped in ; for new /// code, prefer Async.Await, which surfaces single exceptions directly. /// The task to await. - /// If the task is canceled then is raised. Note + /// + ///

If the task is canceled then is raised. Note /// that the task may be governed by a different cancellation token to the overall async computation /// where the AwaitTask occurs. In practice you should normally start the task with the /// cancellation token returned by let! ct = Async.CancellationToken, and catch /// any at the point where the - /// overall async is started. + /// overall async is started.

+ ///

For the common case where you are running a Task within an Asynchronous Computation, + /// see StartTaskImmediate, which surfaces the ambient CancellationToken + /// so that it can be passed to the Task being started.

///
/// Awaiting Results /// @@ -813,12 +817,15 @@ namespace Microsoft.FSharp.Control /// Note exceptions are wrapped in ; for new /// code, prefer Async.Await, which surfaces single exceptions directly.
/// The task to await. - /// If the task is canceled then is raised. Note + ///

If the task is canceled then is raised. Note /// that the task may be governed by a different cancellation token to the overall async computation /// where the AwaitTask occurs. In practice you should normally start the task with the /// cancellation token returned by let! ct = Async.CancellationToken, and catch /// any at the point where the - /// overall async is started. + /// overall async is started.

+ ///

For the common case where you are running a Task within an Asynchronous Computation, + /// see StartTaskImmediate, which surfaces the ambient CancellationToken + /// so that it can be passed to the Task being started.

///
/// Awaiting Results /// @@ -856,6 +863,9 @@ namespace Microsoft.FSharp.Control /// typically tasks should be wired to the ambient cancellation token obtained via /// let! ct = Async.CancellationToken, catching /// where the overall async is started.

+ ///

For the common case where you are running a Task within an Asynchronous Computation, + /// see StartTaskImmediate, which surfaces the ambient CancellationToken + /// so that it can be passed to the Task being started.

///
/// /// Awaiting Results @@ -893,6 +903,9 @@ namespace Microsoft.FSharp.Control /// typically tasks should be wired to the ambient cancellation token obtained via /// let! ct = Async.CancellationToken, catching /// where the overall async is started.

+ ///

For the common case where you are running a Task within an Asynchronous Computation, + /// see StartTaskImmediate, which surfaces the ambient CancellationToken + /// so that it can be passed to the Task being started.

/// /// Awaiting Results /// @@ -929,6 +942,9 @@ namespace Microsoft.FSharp.Control /// typically tasks should be wired to the ambient cancellation token obtained via /// let! ct = Async.CancellationToken, catching /// where the overall async is started.

+ ///

For the common case where you are running a Task within an Asynchronous Computation, + /// see StartTaskImmediate, which surfaces the ambient CancellationToken + /// so that it can be passed to the Task being started.

/// /// Awaiting Results /// @@ -963,6 +979,9 @@ namespace Microsoft.FSharp.Control /// typically tasks should be wired to the ambient cancellation token obtained via /// let! ct = Async.CancellationToken, catching /// where the overall async is started.

+ ///

For the common case where you are running a Task within an Asynchronous Computation, + /// see StartTaskImmediate, which surfaces the ambient CancellationToken + /// so that it can be passed to the Task being started.

/// /// Awaiting Results /// @@ -1359,7 +1378,11 @@ namespace Microsoft.FSharp.Control /// Creates an asynchronous computation that will wait for the given task-like value to complete and return /// its result. /// The task-like value to await. - ///

The value must satisfy the GetAwaiter pattern: it must have a GetAwaiter() method + /// + ///

For the common case where you are running a Task within an Asynchronous Computation, + /// see StartTaskImmediate, which surfaces the ambient CancellationToken + /// so that it can be passed to the Task being started.

+ ///

The value must satisfy the GetAwaiter pattern: it must have a GetAwaiter() method /// returning an awaiter implementing /// with IsCompleted and GetResult() members.

///

Exceptions thrown by GetResult() are propagated directly.

From 69f2d3ba5eceefe41ef7383bf7c7e7d55ee250be Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 13 Aug 2026 18:10:14 +0100 Subject: [PATCH 09/12] review: clarify comments --- .../FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs index 9b33aaedb77..d0452d1c2b9 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs @@ -11,7 +11,7 @@ open Xunit open System.Threading open System.Threading.Tasks -// Cancels default token. +// Test affects global state via Async.CancelDefaultToken [] module AsyncType = @@ -40,6 +40,7 @@ module AsyncType = async { return () } |> expect Success +// Multiple tests affect global state via Async.CancelDefaultToken [] type AsyncType() = From 267be0c2a1b36d2fc8de127656cb4767ea3e2343 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 13 Aug 2026 18:16:58 +0100 Subject: [PATCH 10/12] tidy: Linting/style --- .../Microsoft.FSharp.Control/AsyncType.fs | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs index d0452d1c2b9..b8e4df54785 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs @@ -83,7 +83,7 @@ type AsyncType() = match sleepType with | "int32" -> Async.Sleep(10000000) | "timespan" -> Async.Sleep(10000000.0 |> TimeSpan.FromMilliseconds) - | unknown -> raise (NotImplementedException(unknown)) + | unknown -> failwith $"Unknown {unknown}" let mutable result = "" use cts = new CancellationTokenSource() Async.StartWithContinuations(computation, @@ -105,7 +105,7 @@ type AsyncType() = match sleepType with | "int32" -> Async.Sleep(10) | "timespan" -> Async.Sleep(10.0 |> TimeSpan.FromMilliseconds) - | unknown -> raise (NotImplementedException(unknown)) + | unknown -> failwith $"Unknown {unknown}" for i in 1..100 do let mutable result = "" use completedEvent = new ManualResetEvent(false) @@ -130,7 +130,7 @@ type AsyncType() = do! match sleepType with | "int32" -> Async.Sleep(-100) | "timespan" -> Async.Sleep(-100.0 |> TimeSpan.FromMilliseconds) - | unknown -> raise (NotImplementedException(unknown)) + | unknown -> failwith $"Unknown {unknown}" failwith "Expected ArgumentOutOfRangeException" with | :? ArgumentOutOfRangeException -> () @@ -139,7 +139,7 @@ type AsyncType() = [] member _.AsyncSleepInfinitely() = ignoreSynchCtx (fun () -> - let computation = Async.Sleep(System.Threading.Timeout.Infinite) + let computation = Async.Sleep(Timeout.Infinite) let result = TaskCompletionSource() use cts = new CancellationTokenSource(TimeSpan.FromSeconds(1.0)) // there's a long way from 1 sec to infinity, but it'll have to do. Async.StartWithContinuations(computation, @@ -157,7 +157,7 @@ type AsyncType() = let a = async { return s } let t : Task = Async.StartAsTask a waitForCompletion t - Assert.True (t.IsCompleted) + Assert.True(t.IsCompleted) Assert.AreEqual(s, t.Result) [] @@ -167,7 +167,7 @@ type AsyncType() = let doSpinloop () = while spinloop do () let a = async { asyncStarted.Set() - cts.CancelAfter (100) + cts.CancelAfter(100) doSpinloop() } @@ -180,7 +180,7 @@ type AsyncType() = // Should not finish, we don't eagerly mark the task done just because it's been signaled to cancel. try let result = t.Wait(1000) - Assert.False (result) + Assert.False(result) with :? AggregateException -> Assert.Fail "Task should not finish, yet" spinloop <- false @@ -199,8 +199,8 @@ type AsyncType() = [] member _.``AwaitTask ignores Async cancellation`` () = let cts = new CancellationTokenSource() - let tcs = new TaskCompletionSource() - let innerTcs = new TaskCompletionSource() + let tcs = TaskCompletionSource() + let innerTcs = TaskCompletionSource() let a = innerTcs.Task |> Async.AwaitTask Async.StartWithContinuations(a, tcs.SetResult, tcs.SetException, ignore >> tcs.SetCanceled, cts.Token) @@ -208,7 +208,7 @@ type AsyncType() = cts.CancelAfter(100) try let result = tcs.Task.Wait(300) - Assert.False (result) + Assert.False(result) with :? AggregateException -> Assert.Fail "Should not finish, yet" innerTcs.SetResult () @@ -252,7 +252,7 @@ type AsyncType() = t.Wait() with e -> exceptionThrown <- true - Assert.True (t.IsFaulted) + Assert.True(t.IsFaulted) Assert.True(exceptionThrown) [] @@ -269,7 +269,7 @@ type AsyncType() = try waitForCompletion t with e -> exceptionThrown <- true - Assert.True (exceptionThrown) + Assert.True(exceptionThrown) Assert.True(t.IsCanceled) [] @@ -292,7 +292,7 @@ type AsyncType() = try t.Wait() with e -> exceptionThrown <- true - Assert.True (exceptionThrown) + Assert.True(exceptionThrown) Assert.True(t.IsCanceled) Assert.True(cancelled) @@ -302,7 +302,7 @@ type AsyncType() = let a = async { return s } let t : Task = Async.StartImmediateAsTask a waitForCompletion t - Assert.True (t.IsCompleted) + Assert.True(t.IsCompleted) Assert.AreEqual(s, t.Result) [] @@ -311,7 +311,7 @@ type AsyncType() = let a = async { return s } let t = Async.StartImmediateAsTask a waitForCompletion t - Assert.True (t.IsCompleted) + Assert.True(t.IsCompleted) Assert.AreEqual(s, t.Result) @@ -326,7 +326,7 @@ type AsyncType() = t.Wait() with e -> exceptionThrown <- true - Assert.True (t.IsFaulted) + Assert.True(t.IsFaulted) Assert.True(exceptionThrown) [] @@ -341,7 +341,7 @@ type AsyncType() = try t.Wait() with e -> exceptionThrown <- true - Assert.True (exceptionThrown) + Assert.True(exceptionThrown) Assert.True(t.IsCanceled) [] @@ -364,7 +364,7 @@ type AsyncType() = try t.Wait() with e -> exceptionThrown <- true - Assert.True (exceptionThrown) + Assert.True(exceptionThrown) Assert.True(t.IsCanceled) Assert.True(cancelled) @@ -500,7 +500,7 @@ type AsyncType() = let cts = new CancellationTokenSource() let token = cts.Token let mutable hasThrown = false - token.Register(fun () -> ewh.Set() |> ignore) |> ignore + token.Register(fun () -> ewh.Set()) |> ignore let a = async { try while true do token.ThrowIfCancellationRequested() @@ -639,7 +639,7 @@ type AsyncType() = [] member _.``Await(Task<'T>) happy path``() = let a = async { - let! v = Async.Await(System.Threading.Tasks.Task.FromResult(42)) + let! v = Async.Await(Task.FromResult(42)) return v = 42 } let ok = Async.RunSynchronously a @@ -648,7 +648,7 @@ type AsyncType() = [] member _.``Await(Task) happy path``() = let a = async { - do! Async.Await(System.Threading.Tasks.Task.CompletedTask) + do! Async.Await(Task.CompletedTask) return true } let ok = Async.RunSynchronously a @@ -1029,5 +1029,6 @@ module AsyncAwaitStackTraceTests = let ``Await task-like via SRTP overload: all three levels visible in stack trace`` () = let e = runAndCaptureException (async { do! Async.Await(TaskWrapper(level2Task())) }) - // 4 instead of 3 as current impl has an outer "at FSharp.Core.UnitTests.Control.AsyncAwaitStackTraceTests.e@836-9.Invoke(Tuple`3 tupledArg) + // 4 instead of 3 as current impl has an outer + // at FSharp.Core.UnitTests.Control.AsyncAwaitStackTraceTests.e@836-9.Invoke(Tuple`3 tupledArg) checkTrace 4 e \ No newline at end of file From a8468e056061355086bdd209d87ab15a814d00e1 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Thu, 13 Aug 2026 18:46:13 +0100 Subject: [PATCH 11/12] cleanup: Use/cover RunSynchronouslyImmediate --- .../Microsoft.FSharp.Control/AsyncType.fs | 86 ++++++++----------- 1 file changed, 36 insertions(+), 50 deletions(-) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs index b8e4df54785..e77154860d1 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs @@ -55,20 +55,21 @@ type AsyncType() = let result = t.Wait(TimeSpan.FromSeconds(30.0)) Assert.True(result, "Task did not finish after waiting for 30 seconds.") - [] - member _.AsyncRunSynchronouslyReusesThreadPoolThread() = + [] + member _.AsyncRunSynchronouslyReusesThreadPoolThread(immediate) = + let run a = if immediate then Async.RunSynchronouslyImmediate(a) else Async.RunSynchronously(a) let action _ = async { return async { return Thread.CurrentThread.ManagedThreadId } - |> Async.RunSynchronously + |> run } // This test needs approximately 1000 ThreadPool threads // if Async.RunSynchronously doesn't reuse them. let usedThreads = Seq.init 1000 action |> Async.Parallel - |> Async.RunSynchronously + |> run |> Set.ofArray printfn $"RunSynchronously used {usedThreads.Count} threads. Environment.ProcessorCount is {Environment.ProcessorCount}." // Some arbitrary large number but in practice it should not use more threads than there are CPU cores. @@ -92,7 +93,7 @@ type AsyncType() = (fun _ -> result <- "Cancel"), cts.Token) cts.Cancel() - Async.Sleep(1000) |> Async.RunSynchronously + Async.Sleep(1000) |> Async.RunSynchronouslyImmediate Assert.AreEqual("Cancel", result) ) @@ -134,7 +135,7 @@ type AsyncType() = failwith "Expected ArgumentOutOfRangeException" with | :? ArgumentOutOfRangeException -> () - } |> Async.RunSynchronously + } |> Async.RunSynchronouslyImmediate [] member _.AsyncSleepInfinitely() = @@ -147,7 +148,7 @@ type AsyncType() = (fun _ -> result.TrySetResult("Exception") |> ignore), (fun _ -> result.TrySetResult("Cancel") |> ignore), cts.Token) - let result = result.Task |> Async.AwaitTask |> Async.RunSynchronously + let result = result.Task |> Async.Await |> Async.RunSynchronouslyImmediate Assert.AreEqual("Cancel", result) ) @@ -233,12 +234,12 @@ type AsyncType() = let cancelled = try - Async.RunSynchronously(a, cancellationToken = cts.Token) |> ignore + Async.RunSynchronouslyImmediate(a, cancellationToken = cts.Token) |> ignore false with :? OperationCanceledException as o -> true | _ -> false - Assert.True (cancelled, "Task is not cancelled") + Assert.True(cancelled, "Task is not cancelled") [] member _.ExceptionPropagatesToTask () = @@ -376,8 +377,7 @@ type AsyncType() = let! s1 = t |> if newAwait then Async.Await else Async.AwaitTask return s = s1 } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) [] member _.AwaitTaskCancellation(newAwait: bool) = @@ -389,8 +389,7 @@ type AsyncType() = return false with :? OperationCanceledException -> return true } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) [] member _.AwaitCompletedTask() = @@ -400,8 +399,7 @@ type AsyncType() = let threadIdAfter = Thread.CurrentThread.ManagedThreadId return threadIdBefore = threadIdAfter } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) [] member _.AwaitTaskCancellationUntyped(newAwait: bool) = @@ -413,8 +411,7 @@ type AsyncType() = return false with :? OperationCanceledException -> return true } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) [] member _.TaskAsyncValueException(newAwait: bool) = @@ -424,8 +421,7 @@ type AsyncType() = return false with e -> return true } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) [] member _.TaskAsyncValueCancellation(newAwait: bool) = @@ -458,7 +454,7 @@ type AsyncType() = do! t |> if newAwait then Async.Await else Async.AwaitTask return true } - let ok = Async.RunSynchronously a + let ok = Async.RunSynchronouslyImmediate a Assert.True(hasBeenCalled && ok) [] @@ -470,8 +466,7 @@ type AsyncType() = return false with e -> return true } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) [] member _.NonGenericTaskAsyncValueCancellation(newAwait: bool) = @@ -520,7 +515,7 @@ type AsyncType() = return! loop(x+1) } - try Async.RunSynchronously (loop 0) + try Async.RunSynchronously(loop 0) hasThrown <- false with Failure "finish" -> hasThrown <- true @@ -562,8 +557,7 @@ type AsyncType() = return false with :? AggregateException as ae -> return ae.InnerExceptions.Count = 2 } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) [] member _.``Await and AwaitTask(Task) valid AggregateException is surfaced``(newAwait) = @@ -575,8 +569,7 @@ type AsyncType() = return false with :? AggregateException as ae -> return ae.InnerExceptions.Count = 2 } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) (* Async.Await behavioral differences @@ -592,8 +585,7 @@ type AsyncType() = return false with :? AggregateException -> return true } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) // ... whereas Async.Await(Task) surfaces the inner exception directly. [] @@ -605,8 +597,7 @@ type AsyncType() = return false with :? ArgumentException as ae -> return ae.Message = "original" } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) // Async.AwaitTask(Task<'T>) surfaces the wrapping AggregateException ... [] @@ -618,8 +609,7 @@ type AsyncType() = return false with :? AggregateException -> return true } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) // ... whereas Async.Await(Task<'T>) surfaces the inner exception directly. [] @@ -631,8 +621,7 @@ type AsyncType() = return false with :? ArgumentException as ae -> return ae.Message = "original" } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) (* Await(Task/Task<'T>) overloads happy path *) @@ -642,8 +631,7 @@ type AsyncType() = let! v = Async.Await(Task.FromResult(42)) return v = 42 } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) [] member _.``Await(Task) happy path``() = @@ -651,8 +639,7 @@ type AsyncType() = do! Async.Await(Task.CompletedTask) return true } - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) (* StartTaskImmediate(Task/Task<'T>) *) @@ -813,20 +800,20 @@ module AsyncTaskLikeAwaitTests = [] let ``Await(task-like) happy path with result``() = - let result = + let a = async { - let! v = Async.Await(MyTask(Task.FromResult 99)) + let! v = Async.Await(MyTask(Task.FromResult 42)) return v } - |> Async.RunSynchronously - Assert.Equal(99, result) + Assert.Equal(42, Async.RunSynchronouslyImmediate a) + [] let ``Await(task-like) happy path unit``() = async { do! Async.Await(MyUnitTask(Task.CompletedTask)) } - |> Async.RunSynchronously + |> Async.RunSynchronouslyImmediate [] let ``Await(task-like) deferred completion``() = @@ -878,8 +865,7 @@ module AsyncTaskLikeAwaitTests = return e.Message = "boom" } tcs.SetException(InvalidOperationException "boom") - let ok = Async.RunSynchronously a - Assert.True ok + Assert.True(Async.RunSynchronouslyImmediate a) [] let ``Await(YieldAwaitable) yields and resumes``() = @@ -890,7 +876,7 @@ module AsyncTaskLikeAwaitTests = do! Async.Await(Task.Yield()) after <- true } - |> Async.RunSynchronously + |> Async.RunSynchronouslyImmediate Assert.True(before && after) [] @@ -901,7 +887,7 @@ module AsyncTaskLikeAwaitTests = let! v = Async.Await(Task.FromResult(42).ConfigureAwait(false)) return v } - |> Async.RunSynchronously + |> Async.RunSynchronouslyImmediate Assert.Equal(42, result) module AsyncStartTaskImmediateTaskLikeTests = @@ -970,9 +956,9 @@ module AsyncAwaitStackTraceTests = // Run via StartImmediateAsTask + .Wait() and return the inner exception. // Using StartImmediateAsTask (not RunSynchronously) ensures that the async-layer // exception machinery goes through TaskCompletionSource.SetException, which preserves - // the stack trace rather than rethrowing synchronously and potentially truncating it. + // the stack trace rather than rethrowing synchronously and/or potentially truncating it. let runAndCaptureException (computation: Async) : exn = - // TODO swap in usage of Async.RunSynchronouslyImmediate + // TODO swap in usage of Async.RunSynchronouslyImmediate and add characterization of the stack trace behavior there as well let t = Async.StartImmediateAsTask computation let ae = Assert.Throws(fun () -> t.Wait()) ae.InnerException From 8f9554f71d64a6a0a2f1bc8353eafc56de44c92a Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Fri, 14 Aug 2026 12:11:24 +0100 Subject: [PATCH 12/12] doc(release notes): Polish --- docs/release-notes/.FSharp.Core/11.0.100.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md index ba44cd453d8..2cf66edaa10 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -8,7 +8,7 @@ ### Added * 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)) -* Add `Async.StartTaskImmediate`, which propagates the ambient `Async.CancellationToken` to a task factory, then awaits the result using `Async.Await` semantics. Overloads for `Task`, `Task<'T>`, `ValueTask`, `ValueTask<'T>` and task-like `.GetAwaiter()` (via SRTP). ([Language Suggestion #1284](https://github.com/fsharp/fslang-suggestions/issues/1284), [PR #20258](https://github.com/dotnet/fsharp/pull/20258)) +* Add `Async.StartTaskImmediate`: passes the ambient `Async.CancellationToken` to a task factory, then await the result using `Async.Await` semantics. Overloads for `Task`, `Task<'T>`, `ValueTask`, `ValueTask<'T>` and task-like `.GetAwaiter()` (via SRTP). ([Language Suggestion #1284](https://github.com/fsharp/fslang-suggestions/issues/1284), [PR #20258](https://github.com/dotnet/fsharp/pull/20258)) * Add `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)) * Add 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)) * Add conversion functions `Task.ofValueTask` and `ValueTask.ofTask`. ([LanguageSuggestion #1466](https://github.com/fsharp/fslang-suggestions/issues/1466), [PR #19844](https://github.com/dotnet/fsharp/pull/19844))