From ccd36414f5c493a08d4d3a69fa9ddaea5572859e Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Sun, 19 Apr 2026 01:26:25 +0000
Subject: [PATCH 1/2] fix: correct Async.bind signature and implementation
The previous signature was (Async<'T> -> Async<'U>) -> Async<'T> -> Async<'U>,
which passed the entire Async<'T> computation to the binder without first
awaiting it. This made the function essentially equivalent to plain function
application, not a monadic bind.
The correct signature is ('T -> Async<'U>) -> Async<'T> -> Async<'U>, matching
Task.bind (which had the same bug fixed in commit 8486e1b) and standard
monadic bind semantics.
Updated CompatibilitySuppressions.xml to suppress the CP0002 baseline-breaking
change diagnostic, since this is an intentional fix of an incorrect public API.
Added Utils.Tests.fs with tests covering the corrected behavior of
Async.bind, Task.bind, Async.map, and Task.map.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
release-notes.txt | 1 +
.../FSharp.Control.TaskSeq.Test.fsproj | 1 +
.../Utils.Tests.fs | 116 ++++++++++++++++++
.../CompatibilitySuppressions.xml | 7 ++
src/FSharp.Control.TaskSeq/Utils.fs | 5 +-
src/FSharp.Control.TaskSeq/Utils.fsi | 2 +-
6 files changed, 130 insertions(+), 2 deletions(-)
create mode 100644 src/FSharp.Control.TaskSeq.Test/Utils.Tests.fs
diff --git a/release-notes.txt b/release-notes.txt
index 20ee8fb0..49639e6e 100644
--- a/release-notes.txt
+++ b/release-notes.txt
@@ -2,6 +2,7 @@
Release notes:
Unreleased
+ - fixes: `Async.bind` signature corrected from `(Async<'T> -> Async<'U>)` to `('T -> Async<'U>)` to match standard monadic bind semantics (same as `Task.bind`); the previous signature made the function effectively equivalent to direct application
1.1.1
- perf: use while! in groupBy, countBy, partition, except, exceptOfSeq to eliminate redundant mutable 'go' variables and initial MoveNextAsync calls
diff --git a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
index 4fcf36c8..0284354e 100644
--- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
+++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
@@ -80,6 +80,7 @@
+
diff --git a/src/FSharp.Control.TaskSeq.Test/Utils.Tests.fs b/src/FSharp.Control.TaskSeq.Test/Utils.Tests.fs
new file mode 100644
index 00000000..c89765c5
--- /dev/null
+++ b/src/FSharp.Control.TaskSeq.Test/Utils.Tests.fs
@@ -0,0 +1,116 @@
+module TaskSeq.Tests.Utils
+
+open System
+open System.Threading.Tasks
+open Xunit
+open FsUnit.Xunit
+
+open FSharp.Control
+
+
+module AsyncBind =
+ []
+ let ``Async.bind awaits the async and passes the value to the binder`` () =
+ let result =
+ async { return 21 }
+ |> Async.bind (fun n -> async { return n * 2 })
+ |> Async.RunSynchronously
+
+ result |> should equal 42
+
+ []
+ let ``Async.bind propagates exceptions from the source async`` () =
+ let run () =
+ async { return raise (InvalidOperationException "source error") }
+ |> Async.bind (fun (_: int) -> async { return 0 })
+ |> Async.RunSynchronously
+
+ (fun () -> run () |> ignore)
+ |> should throw typeof
+
+ []
+ let ``Async.bind propagates exceptions from the binder`` () =
+ let run () =
+ async { return 1 }
+ |> Async.bind (fun _ -> async { return raise (InvalidOperationException "binder error") })
+ |> Async.RunSynchronously
+
+ (fun () -> run () |> ignore)
+ |> should throw typeof
+
+ []
+ let ``Async.bind chains correctly`` () =
+ let result =
+ async { return 1 }
+ |> Async.bind (fun n -> async { return n + 10 })
+ |> Async.bind (fun n -> async { return n + 100 })
+ |> Async.RunSynchronously
+
+ result |> should equal 111
+
+ []
+ let ``Async.bind passes the unwrapped value, not the Async wrapper`` () =
+ // This test specifically verifies the bug fix: binder receives 'T, not Async<'T>
+ let mutable receivedType = typeof
+
+ async { return 42 }
+ |> Async.bind (fun (n: int) ->
+ receivedType <- n.GetType()
+ async { return () })
+ |> Async.RunSynchronously
+
+ receivedType |> should equal typeof
+
+
+module TaskBind =
+ []
+ let ``Task.bind awaits the task and passes the value to the binder`` () = task {
+ let result =
+ task { return 21 }
+ |> Task.bind (fun n -> task { return n * 2 })
+
+ let! v = result
+ v |> should equal 42
+ }
+
+ []
+ let ``Task.bind chains correctly`` () = task {
+ let result =
+ task { return 1 }
+ |> Task.bind (fun n -> task { return n + 10 })
+ |> Task.bind (fun n -> task { return n + 100 })
+
+ let! v = result
+ v |> should equal 111
+ }
+
+
+module AsyncMap =
+ []
+ let ``Async.map transforms the result`` () =
+ let result =
+ async { return 21 }
+ |> Async.map (fun n -> n * 2)
+ |> Async.RunSynchronously
+
+ result |> should equal 42
+
+ []
+ let ``Async.map chains correctly`` () =
+ let result =
+ async { return 1 }
+ |> Async.map (fun n -> n + 10)
+ |> Async.map (fun n -> n + 100)
+ |> Async.RunSynchronously
+
+ result |> should equal 111
+
+
+module TaskMap =
+ []
+ let ``Task.map transforms the result`` () = task {
+ let result = task { return 21 } |> Task.map (fun n -> n * 2)
+
+ let! v = result
+ v |> should equal 42
+ }
diff --git a/src/FSharp.Control.TaskSeq/CompatibilitySuppressions.xml b/src/FSharp.Control.TaskSeq/CompatibilitySuppressions.xml
index 488c87ac..c0e8a357 100644
--- a/src/FSharp.Control.TaskSeq/CompatibilitySuppressions.xml
+++ b/src/FSharp.Control.TaskSeq/CompatibilitySuppressions.xml
@@ -1,6 +1,13 @@
+
+ CP0002
+ M:FSharp.Control.Async.bind``2(Microsoft.FSharp.Core.FSharpFunc{Microsoft.FSharp.Control.FSharpAsync{``0},Microsoft.FSharp.Control.FSharpAsync{``1}},Microsoft.FSharp.Control.FSharpAsync{``0})
+ lib/netstandard2.1/FSharp.Control.TaskSeq.dll
+ lib/netstandard2.1/FSharp.Control.TaskSeq.dll
+ true
+
CP0002
M:FSharp.Control.LowPriority.TaskSeqBuilder#Bind``5(FSharp.Control.TaskSeqBuilder,``0,Microsoft.FSharp.Core.FSharpFunc{``1,Microsoft.FSharp.Core.CompilerServices.ResumableCode{FSharp.Control.TaskSeqStateMachineData{``2},Microsoft.FSharp.Core.Unit}})
diff --git a/src/FSharp.Control.TaskSeq/Utils.fs b/src/FSharp.Control.TaskSeq/Utils.fs
index ec076f20..147734bf 100644
--- a/src/FSharp.Control.TaskSeq/Utils.fs
+++ b/src/FSharp.Control.TaskSeq/Utils.fs
@@ -72,4 +72,7 @@ module Async =
return mapper result
}
- let inline bind binder (async: Async<'T>) : Async<'U> = ExtraTopLevelOperators.async { return! binder async }
+ let inline bind binder (async: Async<'T>) : Async<'U> = ExtraTopLevelOperators.async {
+ let! result = async
+ return! binder result
+ }
diff --git a/src/FSharp.Control.TaskSeq/Utils.fsi b/src/FSharp.Control.TaskSeq/Utils.fsi
index b1717204..d252d45a 100644
--- a/src/FSharp.Control.TaskSeq/Utils.fsi
+++ b/src/FSharp.Control.TaskSeq/Utils.fsi
@@ -103,4 +103,4 @@ module Async =
val inline map: mapper: ('T -> 'U) -> async: Async<'T> -> Async<'U>
/// Bind an Async<'T>
- val inline bind: binder: (Async<'T> -> Async<'U>) -> async: Async<'T> -> Async<'U>
+ val inline bind: binder: ('T -> Async<'U>) -> async: Async<'T> -> Async<'U>
From fd6146ad9eeefb9cbbc6d8d4394fea86f9d14c3e Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Sun, 19 Apr 2026 01:26:27 +0000
Subject: [PATCH 2/2] ci: trigger checks