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