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
4 changes: 4 additions & 0 deletions docs/release-notes/.FSharp.Core/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@
* Fix `Array.exists2` documentation examples to use equal-length arrays; the previous examples would throw `ArgumentException` at runtime instead of returning the documented `false`/`true` values. ([PR #19672](https://github.com/dotnet/fsharp/pull/19672))
* Move `Async.StartChild` to the "Starting Async Computations" docs category alongside `Async.StartChildAsTask`. ([Issue #19667](https://github.com/dotnet/fsharp/issues/19667))
* Add `InlineIfLambda` to `Array.init` ([PR #19869](https://github.com/dotnet/fsharp/pull/19869))

### Added

* 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))
123 changes: 99 additions & 24 deletions src/FSharp.Core/async.fs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ open System.Runtime.ExceptionServices
open System.Threading
open System.Threading.Tasks
open Microsoft.FSharp.Core
open Microsoft.FSharp.Core.CompilerServices
open Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicOperators
open Microsoft.FSharp.Control
open Microsoft.FSharp.Collections
Expand Down Expand Up @@ -1210,16 +1211,30 @@ module AsyncPrimitives =

task

// Used by Async.Await path to elide egregious AggregateException wrapping
[<DebuggerHidden>]
let UnwrapExn (exn: AggregateException) =
if exn.InnerExceptions.Count = 1 then
exn.InnerExceptions[0]
else
exn

// Call the appropriate continuation on completion of a task
[<DebuggerHidden>]
let OnTaskCompleted (completedTask: Task<'T>) (ctxt: AsyncActivation<'T>) =
let OnTaskCompleted unwrap (completedTask: Task<'T>) (ctxt: AsyncActivation<'T>) =
assert completedTask.IsCompleted

if completedTask.IsCanceled then
let edi = ExceptionDispatchInfo.Capture(TaskCanceledException completedTask)
ctxt.econt edi
elif completedTask.IsFaulted then
let edi = ExceptionDispatchInfo.RestoreOrCapture completedTask.Exception
let e =
if unwrap then
UnwrapExn completedTask.Exception
else
completedTask.Exception

let edi = ExceptionDispatchInfo.RestoreOrCapture e
ctxt.econt edi
else
ctxt.cont completedTask.Result
Expand All @@ -1229,14 +1244,20 @@ module AsyncPrimitives =
// the overall async (they may be governed by different cancellation tokens, or
// the task may not have a cancellation token at all).
[<DebuggerHidden>]
let OnUnitTaskCompleted (completedTask: Task) (ctxt: AsyncActivation<unit>) =
let OnUnitTaskCompleted unwrap (completedTask: Task) (ctxt: AsyncActivation<unit>) =
assert completedTask.IsCompleted

if completedTask.IsCanceled then
let edi = ExceptionDispatchInfo.Capture(TaskCanceledException(completedTask))
ctxt.econt edi
elif completedTask.IsFaulted then
let edi = ExceptionDispatchInfo.RestoreOrCapture completedTask.Exception
let e =
if unwrap then
UnwrapExn completedTask.Exception
else
completedTask.Exception

let edi = ExceptionDispatchInfo.RestoreOrCapture e
ctxt.econt edi
else
ctxt.cont ()
Expand All @@ -1246,10 +1267,10 @@ module AsyncPrimitives =
// completing the task. This will install a new trampoline on that thread and continue the
// execution of the async there.
[<DebuggerHidden>]
let AttachContinuationToTask (task: Task<'T>) (ctxt: AsyncActivation<'T>) =
let AttachContinuationToTask unwrap (task: Task<'T>) (ctxt: AsyncActivation<'T>) =
task.ContinueWith(
Action<Task<'T>>(fun completedTask ->
ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnTaskCompleted completedTask ctxt)
ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnTaskCompleted unwrap completedTask ctxt)
|> unfake),
TaskContinuationOptions.ExecuteSynchronously
)
Expand All @@ -1261,16 +1282,36 @@ module AsyncPrimitives =
// completing the task. This will install a new trampoline on that thread and continue the
// execution of the async there.
[<DebuggerHidden>]
let AttachContinuationToUnitTask (task: Task) (ctxt: AsyncActivation<unit>) =
let AttachContinuationToUnitTask unwrap (task: Task) (ctxt: AsyncActivation<unit>) =
task.ContinueWith(
Action<Task>(fun completedTask ->
ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnUnitTaskCompleted completedTask ctxt)
ctxt.trampolineHolder.ExecuteWithTrampoline(fun () -> OnUnitTaskCompleted unwrap completedTask ctxt)
|> unfake),
TaskContinuationOptions.ExecuteSynchronously
)
|> ignore
|> fake

let AwaitTask unwrap (task: Task<'T>) =
MakeAsyncWithCancelCheck(fun ctxt ->
if task.IsCompleted then
// Run synchronously without installing new trampoline
OnTaskCompleted unwrap task ctxt
else
// Continue asynchronously, via syncContext if necessary, installing new trampoline
let ctxt = DelimitSyncContext ctxt
ctxt.ProtectCode(fun () -> AttachContinuationToTask unwrap task ctxt))

let AwaitUnitTask unwrap (task: Task) =
MakeAsyncWithCancelCheck(fun ctxt ->
if task.IsCompleted then
// Continue synchronously without installing new trampoline
OnUnitTaskCompleted unwrap task ctxt
else
// Continue asynchronously, via syncContext if necessary, installing new trampoline
let ctxt = DelimitSyncContext ctxt
ctxt.ProtectCode(fun () -> AttachContinuationToUnitTask unwrap task ctxt))

/// Removes a registration places on a cancellation token
let DisposeCancellationRegistration (registration: byref<CancellationTokenRegistration option>) =
match registration with
Expand Down Expand Up @@ -2203,24 +2244,58 @@ type Async =
CreateWhenCancelledAsync compensation computation

static member AwaitTask(task: Task<'T>) : Async<'T> =
MakeAsyncWithCancelCheck(fun ctxt ->
if task.IsCompleted then
// Run synchronously without installing new trampoline
OnTaskCompleted task ctxt
else
// Continue asynchronously, via syncContext if necessary, installing new trampoline
let ctxt = DelimitSyncContext ctxt
ctxt.ProtectCode(fun () -> AttachContinuationToTask task ctxt))
AwaitTask false task

static member AwaitTask(task: Task) : Async<unit> =
MakeAsyncWithCancelCheck(fun ctxt ->
if task.IsCompleted then
// Continue synchronously without installing new trampoline
OnUnitTaskCompleted task ctxt
else
// Continue asynchronously, via syncContext if necessary, installing new trampoline
let ctxt = DelimitSyncContext ctxt
ctxt.ProtectCode(fun () -> AttachContinuationToUnitTask task ctxt))
AwaitUnitTask false task

static member Await(task: Task<'T>) : Async<'T> =
AwaitTask true task

static member Await(task: Task) : Async<unit> =
AwaitUnitTask true task

#if NETSTANDARD2_1
static member Await(task: ValueTask<'T>) : Async<'T> =
if task.IsCompletedSuccessfully then
CreateReturnAsync(task.GetAwaiter().GetResult())
else
AwaitTask true (task.AsTask())

static member Await(task: ValueTask) : Async<unit> =
if task.IsCompletedSuccessfully then
CreateReturnAsync(task.GetAwaiter().GetResult())
else
AwaitUnitTask true (task.AsTask())
#endif

module AsyncTaskLikeExtensions =

type Async with

[<NoEagerConstraintApplication>]
static member inline Await< ^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)>
(task: ^TaskLike)
: Async<'T> =
Async.FromContinuations(fun (cont, econt, _ccont) ->
let mutable awaiter = (^TaskLike: (member GetAwaiter: unit -> ^Awaiter) task)

if (^Awaiter: (member get_IsCompleted: unit -> bool) awaiter) then
try
cont ((^Awaiter: (member GetResult: unit -> 'T) awaiter))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-inner faults surface differently depending on which Async.Await binds:

let tcs = TaskCompletionSource<int>()
tcs.SetException [ ArgumentException(); InvalidOperationException() ]
Async.Await tcs.Task              // typed
Async.Await (Wrapper tcs.Task)    // SRTP
  • typed → AggregateException (UnwrapExn unwraps only Count=1)
  • SRTP → ArgumentException (raw GetResult(), first inner)

Full alignment isn't possible for arbitrary awaiters, so just document it — append to the SRTP <remarks> in async.fsi:

  /// with <c>IsCompleted</c> and <c>GetResult()</c> members. Exceptions thrown by <c>GetResult()</c> are
- /// propagated directly.
+ /// propagated directly. Unlike the <see cref="T:System.Threading.Tasks.Task"/> overloads, an
+ /// <see cref="T:System.AggregateException"/> carrying multiple inner exceptions is not preserved:
+ /// the first inner exception surfaces (standard <c>GetResult()</c> semantics).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

with e ->
econt e
else
(awaiter :> ICriticalNotifyCompletion)
.OnCompleted(fun () ->
try
cont ((^Awaiter: (member GetResult: unit -> 'T) awaiter))
with e ->
econt e))

module CommonExtensions =

Expand Down
Loading
Loading