Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0677d34
feat(Async): Add exception-unwrapping Await
bartelink May 21, 2026
9240ff8
Polish xmldoc
bartelink May 22, 2026
97c31eb
Fix test expectation
bartelink May 22, 2026
3ae385d
hack to silence test fail
bartelink May 22, 2026
76f5bad
Apply majocha correction
bartelink May 22, 2026
cd66660
Remove layer of indirection
bartelink May 22, 2026
ae14fcb
add support for TaskLike
bartelink May 23, 2026
00b5b23
Fix test expectation
bartelink May 23, 2026
76995f1
Add stacktrace validation
bartelink May 24, 2026
22022a0
Add specific TaskLike cases
bartelink May 25, 2026
3bf8d2f
Apply even more feedback goodness
bartelink May 25, 2026
04446a9
Merge remote-tracking branch 'upstream/main' into async-await
bartelink Jul 13, 2026
f62dc2d
fix: Correct #if condition in tests
bartelink Jul 13, 2026
1e853ee
fix: Replace UnsafeOnCompleted with OnCompleted
bartelink Jul 13, 2026
5c12141
doc: Exception semantics details per review
bartelink Jul 13, 2026
e1d0708
docs: reinstate proviso about flowing CT
bartelink Jul 13, 2026
2e532e0
doc: Remove broken reference for netstandard2.0
bartelink Jul 13, 2026
197ff01
review: add comment re role of Await Tasklike
bartelink Jul 13, 2026
95595bd
docs: add missing reference
bartelink Jul 13, 2026
334a40f
test: Regression for UnsafeOnCompletion
bartelink Jul 13, 2026
e9b7c32
Merge remote-tracking branch 'upstream/main' into async-await
bartelink Jul 25, 2026
cb5c378
doc: Polish CL
bartelink Jul 25, 2026
928ca92
fix: Make test compile on ns2.0/net471
bartelink Jul 25, 2026
7d6ee43
fix: Correct #if guard using wrong symbol
bartelink Jul 25, 2026
5e37657
fix: Update tooltip expected to include Await
bartelink Jul 26, 2026
a9dac92
fix: Adjust ifdef'd xmldoc to pass validation
bartelink Jul 26, 2026
13d205f
fix: Update extractXmlDocBlocks to handle cond compilation
bartelink Jul 26, 2026
f52f2ca
fix: Correct error message line numbering
bartelink Jul 26, 2026
3a9a201
fix: Handle netstandard2.0 quickinfo rendering
bartelink Jul 27, 2026
7896572
Merge remote-tracking branch 'upstream/main' into async-await
bartelink Aug 5, 2026
b61a673
chore: Update FSharpPlus regression commit ref
bartelink Aug 10, 2026
8e180b0
Merge remote-tracking branch 'upstream/main' into async-await
bartelink Aug 10, 2026
a87e708
chore: update commit ref to merged result
bartelink Aug 11, 2026
62ee340
Fix recursive inline SRTP resolution truncated by one currying level
T-Gro Aug 12, 2026
ace4881
chore: Move FSharpPlus pin past nowarn choke
bartelink Aug 12, 2026
b327126
Merge remote-tracking branch 'upstream/main' into async-await
bartelink Aug 12, 2026
bd9fd66
Add PR link to memoizeN SRTP fix release note
T-Gro Aug 12, 2026
7199b31
chore: sync all FSharpPlus upstreams
bartelink Aug 12, 2026
199f2e1
Merge remote-tracking branch 'upstream/main' into async-await
bartelink Aug 12, 2026
c8eda9f
Merge remote-tracking branch 'upstream/t-gro-fix-memoizen-srtp-regres…
bartelink Aug 12, 2026
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
15 changes: 7 additions & 8 deletions azure-pipelines-PR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -757,35 +757,34 @@ stages:
displayName: UMX_Slow_Repro
expectLocalCore: true
- repo: fsprojects/FSharpPlus
commit: f614035b75922aba41ed6a36c2fc986a2171d2b8
commit: f42f81885111c652b08218e0880c264447ae56e4
buildScript: build.cmd
displayName: FSharpPlus_Windows
- repo: fsprojects/FSharpPlus
commit: f614035b75922aba41ed6a36c2fc986a2171d2b8
commit: f42f81885111c652b08218e0880c264447ae56e4
buildScript: build.sh
displayName: FSharpPlus_Linux
useVmImage: $(LinuxMachineQueueName)
usePool: $(DncEngPublicBuildPool)
- repo: fsprojects/FSharpPlus
commit: 2648efe
commit: f42f81885111c652b08218e0880c264447ae56e4
buildScript: dotnet build tests/FSharpPlus.Tests/FSharpPlus.Tests.fsproj -c Release -bl
displayName: FsharpPlus_NET10_Build_Lib_Tests
expectLocalCore: true
# remove this before merging
- repo: fsprojects/FSharpPlus
commit: 2648efe
commit: f42f81885111c652b08218e0880c264447ae56e4
buildScript: dotnet msbuild build.proj -t:Build;Test -bl
displayName: FsharpPlus_NET10_Test_Debug
- repo: fsprojects/FSharpPlus
commit: 2648efe
commit: f42f81885111c652b08218e0880c264447ae56e4
buildScript: dotnet msbuild build.proj -t:Build;Test -p:Configuration=Release -bl
displayName: FsharpPlus_NET10_Test_Release
- repo: fsprojects/FSharpPlus
commit: 2648efe
commit: f42f81885111c652b08218e0880c264447ae56e4
buildScript: dotnet msbuild build.proj -t:Build;AllDocs -bl
displayName: FsharpPlus_NET10_Docs
- repo: fsprojects/FSharpPlus
commit: 2648efe
commit: f42f81885111c652b08218e0880c264447ae56e4
buildScript: build.sh
displayName: FsharpPlus_Net10_Linux
useVmImage: $(LinuxMachineQueueName)
Expand Down
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### Fixed

* Fix recursive inline SRTP resolution being truncated by one currying level (e.g. FSharpPlus `memoizeN`), a regression from the function-domain unification order change in [PR #15181](https://github.com/dotnet/fsharp/pull/15181); the contravariant domain now keeps the inference variable that still carries the pending member constraint. ([PR #20247](https://github.com/dotnet/fsharp/pull/20247))
* Fix incorrect `StructLayout(Size = 1)` emission for data-less struct unions where the compiler-generated tag field makes the actual runtime size larger. ([PR #19759](https://github.com/dotnet/fsharp/pull/19759))
* Fix FS0750 "This construct may only be used within computation expressions" incorrectly raised for `let!`/`use!`/`do!` appearing in the right-hand side of a plain `let` binding inside a computation expression. The right-hand side is now desugared as a nested computation of the same builder whose result is bound with `let!`, keeping its bindings correctly scoped. ([Issue #19457](https://github.com/dotnet/fsharp/issues/19457), [PR #19868](https://github.com/dotnet/fsharp/pull/19868))
* Stop leaking a `System.Diagnostics.Metrics.MeterListener` per `Cache` in DEBUG builds. Each cache created a `CacheMetrics.CacheMetricsListener` (which starts a `MeterListener` registered in the process-global metrics registry) and never disposed it, so listeners accumulated for the lifetime of the process. Because every cache hit/miss/add published to all registered listeners, the per-operation cost grew linearly with the number of leaked listeners, so repeated checks (and Debug FCS test runs) slowed down over time. The per-cache `CacheMetricsListener` and the per-instance `cacheId` tag are removed; `DebugDisplay` and tests now read the existing name-aggregated stats populated by the single `ListenToAll` listener, so no per-cache listener is created and no per-operation cost is added. ([PR #19995](https://github.com/dotnet/fsharp/pull/19995))
Expand Down
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Core/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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))
15 changes: 14 additions & 1 deletion src/Compiler/Checking/ConstraintSolver.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1500,7 +1500,20 @@ and SolveFunTypeEqn csenv ndeep m2 trace cxsln domainTy1 domainTy2 rangeTy1 rang
trackErrors {
let g = csenv.g
let domainTy2 = reqTyForArgumentNullnessInference g domainTy1 domainTy2
do! SolveTypeEqualsTypeKeepAbbrevsWithCxsln csenv ndeep m2 trace cxsln domainTy2 domainTy1
// Keep an inference variable that still carries an unsolved SRTP constraint as the
// unification representative: if the required domain absorbs it, the pending recursive
// trait resolution is merged away and recursive SRTP specialization is truncated by one
// currying level. This restores the forward domain order that nullness PR #15181 reversed,
// but only for that case; skipped under MatchingOnly, where only the left type variable may
// be solved (see SolveTypeEqualsType).
let inline isUnsolvedTraitTypar ty =
match tryDestTyparTy g ty with
| ValueSome tp -> tp |> HasConstraint (function TyparConstraint.MayResolveMember(traitInfo, _) -> traitInfo.Solution.IsNone | _ -> false)
| _ -> false
if not csenv.MatchingOnly && isUnsolvedTraitTypar domainTy2 && not (isUnsolvedTraitTypar domainTy1) then
do! SolveTypeEqualsTypeKeepAbbrevsWithCxsln csenv ndeep m2 trace cxsln domainTy1 domainTy2
else
do! SolveTypeEqualsTypeKeepAbbrevsWithCxsln csenv ndeep m2 trace cxsln domainTy2 domainTy1
return! SolveTypeEqualsTypeKeepAbbrevsWithCxsln csenv ndeep m2 trace cxsln rangeTy1 rangeTy2
}

Expand Down
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 @@ -1203,16 +1204,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 @@ -1222,14 +1237,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 @@ -1239,10 +1260,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 @@ -1254,16 +1275,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 @@ -2202,24 +2243,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))
Comment thread
T-Gro marked this conversation as resolved.
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