From da9cf92d28361eea80e64d0466a00f8409f3f03f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 00:28:53 +0000 Subject: [PATCH 1/4] =?UTF-8?q?Add=20AsyncSeq.exists2,=20exists2Async,=20f?= =?UTF-8?q?orall2,=20forall2Async=20=E2=80=94=20Seq=20parity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds four new API functions mirroring Seq.exists2 / Seq.forall2: - AsyncSeq.exists2: tests whether any pairwise pair satisfies a synchronous predicate (short-circuits on first match) - AsyncSeq.exists2Async: async-predicate variant of exists2 - AsyncSeq.forall2: tests whether all pairwise pairs satisfy a synchronous predicate (short-circuits on first failure) - AsyncSeq.forall2Async: async-predicate variant of forall2 All four evaluate pairwise up to the shorter of the two sequences, consistent with Seq.exists2 / Seq.forall2 semantics. 12 tests added; 434/434 pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- RELEASE_NOTES.md | 7 ++ src/FSharp.Control.AsyncSeq/AsyncSeq.fs | 50 +++++++++ src/FSharp.Control.AsyncSeq/AsyncSeq.fsi | 18 +++ .../AsyncSeqTests.fs | 104 ++++++++++++++++++ 4 files changed, 179 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index cfd430ca..27194a87 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,10 @@ +### 4.17.0 + +* Added `AsyncSeq.exists2` — asynchronously tests whether any corresponding pair of elements in two async sequences satisfies the predicate. Evaluates pairwise up to the shorter sequence; short-circuits on first match. Mirrors `Seq.exists2`. +* Added `AsyncSeq.exists2Async` — asynchronous-predicate variant of `exists2`. +* Added `AsyncSeq.forall2` — asynchronously tests whether all corresponding pairs of elements in two async sequences satisfy the predicate. Evaluates pairwise up to the shorter sequence; short-circuits on first failure. Mirrors `Seq.forall2`. +* Added `AsyncSeq.forall2Async` — asynchronous-predicate variant of `forall2`. + ### 4.16.0 * Performance: Replaced `ref` cells with `mutable` locals in the `ofSeq`, `tryWith`, and `tryFinally` enumerator state machines. Each call to `ofSeq` (or any async CE block using `try...with` / `try...finally` / `use`) previously heap-allocated a `Ref` wrapper object per enumerator; it now uses a direct mutable field in the generated class, reducing GC pressure. The change is equivalent to the `mutable`-for-`ref` improvement introduced in 4.11.0 for other enumerators. diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs index 06291a0b..fd8dc499 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs @@ -1471,6 +1471,56 @@ module AsyncSeq = let forallAsync f (source : AsyncSeq<'T>) = source |> existsAsync (fun v -> async { let! b = f v in return not b }) |> Async.map not + let exists2Async (predicate: 'T1 -> 'T2 -> Async) (source1: AsyncSeq<'T1>) (source2: AsyncSeq<'T2>) : Async = async { + use ie1 = source1.GetEnumerator() + use ie2 = source2.GetEnumerator() + let! m1 = ie1.MoveNext() + let! m2 = ie2.MoveNext() + let mutable b1 = m1 + let mutable b2 = m2 + let mutable result = false + let mutable isDone = false + while not isDone do + match b1, b2 with + | None, _ | _, None -> isDone <- true + | Some v1, Some v2 -> + let! ok = predicate v1 v2 + if ok then result <- true; isDone <- true + else + let! n1 = ie1.MoveNext() + let! n2 = ie2.MoveNext() + b1 <- n1 + b2 <- n2 + return result } + + let exists2 (predicate: 'T1 -> 'T2 -> bool) (source1: AsyncSeq<'T1>) (source2: AsyncSeq<'T2>) : Async = + exists2Async (fun a b -> async.Return (predicate a b)) source1 source2 + + let forall2Async (predicate: 'T1 -> 'T2 -> Async) (source1: AsyncSeq<'T1>) (source2: AsyncSeq<'T2>) : Async = async { + use ie1 = source1.GetEnumerator() + use ie2 = source2.GetEnumerator() + let! m1 = ie1.MoveNext() + let! m2 = ie2.MoveNext() + let mutable b1 = m1 + let mutable b2 = m2 + let mutable result = true + let mutable isDone = false + while not isDone do + match b1, b2 with + | None, _ | _, None -> isDone <- true + | Some v1, Some v2 -> + let! ok = predicate v1 v2 + if not ok then result <- false; isDone <- true + else + let! n1 = ie1.MoveNext() + let! n2 = ie2.MoveNext() + b1 <- n1 + b2 <- n2 + return result } + + let forall2 (predicate: 'T1 -> 'T2 -> bool) (source1: AsyncSeq<'T1>) (source2: AsyncSeq<'T2>) : Async = + forall2Async (fun a b -> async.Return (predicate a b)) source1 source2 + let compareWithAsync (comparer: 'T -> 'T -> Async) (source1: AsyncSeq<'T>) (source2: AsyncSeq<'T>) : Async = async { use ie1 = source1.GetEnumerator() use ie2 = source2.GetEnumerator() diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi index 98a83981..67b8395e 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi @@ -426,6 +426,24 @@ module AsyncSeq = /// Asynchronously determine if the async predicate returns true for all values in the sequence val forallAsync : predicate:('T -> Async) -> source:AsyncSeq<'T> -> Async + /// Asynchronously determine if any corresponding pair of elements in two async sequences satisfies + /// the predicate. Evaluates pairwise up to the shorter of the two sequences; short-circuits on first match. + /// Mirrors Seq.exists2. + val exists2 : predicate:('T1 -> 'T2 -> bool) -> source1:AsyncSeq<'T1> -> source2:AsyncSeq<'T2> -> Async + + /// Asynchronously determine if any corresponding pair of elements in two async sequences satisfies + /// the async predicate. Evaluates pairwise up to the shorter of the two sequences; short-circuits on first match. + val exists2Async : predicate:('T1 -> 'T2 -> Async) -> source1:AsyncSeq<'T1> -> source2:AsyncSeq<'T2> -> Async + + /// Asynchronously determine if all corresponding pairs of elements in two async sequences satisfy + /// the predicate. Evaluates pairwise up to the shorter of the two sequences; short-circuits on first failure. + /// Mirrors Seq.forall2. + val forall2 : predicate:('T1 -> 'T2 -> bool) -> source1:AsyncSeq<'T1> -> source2:AsyncSeq<'T2> -> Async + + /// Asynchronously determine if all corresponding pairs of elements in two async sequences satisfy + /// the async predicate. Evaluates pairwise up to the shorter of the two sequences; short-circuits on first failure. + val forall2Async : predicate:('T1 -> 'T2 -> Async) -> source1:AsyncSeq<'T1> -> source2:AsyncSeq<'T2> -> Async + /// Compares two async sequences lexicographically using the given synchronous comparison function. /// Returns a negative integer if source1 < source2, 0 if equal, and a positive integer if source1 > source2. val compareWith : comparer:('T -> 'T -> int) -> source1:AsyncSeq<'T> -> source2:AsyncSeq<'T> -> Async diff --git a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs index 6c640d9c..eb47a592 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -4622,3 +4622,107 @@ let ``asyncSeq try-with handler can yield elements`` () = with _ -> yield 42 } let result = s |> AsyncSeq.toArrayAsync |> Async.RunSynchronously Assert.AreEqual([| 42 |], result) + +// ===== exists2 / exists2Async ===== + +[] +let ``AsyncSeq.exists2 returns true when a matching pair exists`` () = + let result = + AsyncSeq.exists2 (=) (AsyncSeq.ofSeq [1;2;3]) (AsyncSeq.ofSeq [0;2;0]) + |> Async.RunSynchronously + Assert.IsTrue(result) + +[] +let ``AsyncSeq.exists2 returns false when no matching pair exists`` () = + let result = + AsyncSeq.exists2 (=) (AsyncSeq.ofSeq [1;2;3]) (AsyncSeq.ofSeq [4;5;6]) + |> Async.RunSynchronously + Assert.IsFalse(result) + +[] +let ``AsyncSeq.exists2 on empty sequences returns false`` () = + let result = + AsyncSeq.exists2 (=) AsyncSeq.empty AsyncSeq.empty + |> Async.RunSynchronously + Assert.IsFalse(result) + +[] +let ``AsyncSeq.exists2 short-circuits on first match`` () = + let count = ref 0 + let result = + AsyncSeq.exists2 + (fun a b -> incr count; a = b) + (AsyncSeq.ofSeq [1;2;3;4;5]) + (AsyncSeq.ofSeq [0;2;0;0;0]) + |> Async.RunSynchronously + Assert.IsTrue(result) + Assert.AreEqual(2, count.Value) // stopped after second pair + +[] +let ``AsyncSeq.exists2 stops at shorter sequence`` () = + let result = + AsyncSeq.exists2 (=) (AsyncSeq.ofSeq [1;2]) (AsyncSeq.ofSeq [3;4;1]) + |> Async.RunSynchronously + Assert.IsFalse(result) // shorter seq ends before (1,1) can match + +[] +let ``AsyncSeq.exists2Async returns true with async predicate`` () = + let result = + AsyncSeq.exists2Async + (fun a b -> async { return a = b }) + (AsyncSeq.ofSeq [1;2;3]) + (AsyncSeq.ofSeq [0;2;0]) + |> Async.RunSynchronously + Assert.IsTrue(result) + +// ===== forall2 / forall2Async ===== + +[] +let ``AsyncSeq.forall2 returns true when all pairs satisfy the predicate`` () = + let result = + AsyncSeq.forall2 (=) (AsyncSeq.ofSeq [1;2;3]) (AsyncSeq.ofSeq [1;2;3]) + |> Async.RunSynchronously + Assert.IsTrue(result) + +[] +let ``AsyncSeq.forall2 returns false when a pair fails`` () = + let result = + AsyncSeq.forall2 (=) (AsyncSeq.ofSeq [1;2;3]) (AsyncSeq.ofSeq [1;9;3]) + |> Async.RunSynchronously + Assert.IsFalse(result) + +[] +let ``AsyncSeq.forall2 on empty sequences returns true`` () = + let result = + AsyncSeq.forall2 (=) AsyncSeq.empty AsyncSeq.empty + |> Async.RunSynchronously + Assert.IsTrue(result) + +[] +let ``AsyncSeq.forall2 short-circuits on first failure`` () = + let count = ref 0 + let result = + AsyncSeq.forall2 + (fun a b -> incr count; a = b) + (AsyncSeq.ofSeq [1;9;3;4;5]) + (AsyncSeq.ofSeq [1;2;3;4;5]) + |> Async.RunSynchronously + Assert.IsFalse(result) + Assert.AreEqual(2, count.Value) // stopped after second pair + +[] +let ``AsyncSeq.forall2 stops at shorter sequence`` () = + let result = + AsyncSeq.forall2 (=) (AsyncSeq.ofSeq [1;2]) (AsyncSeq.ofSeq [1;2;99]) + |> Async.RunSynchronously + Assert.IsTrue(result) // stops when shorter seq ends; all checked pairs passed + +[] +let ``AsyncSeq.forall2Async returns true with async predicate`` () = + let result = + AsyncSeq.forall2Async + (fun a b -> async { return a = b }) + (AsyncSeq.ofSeq [1;2;3]) + (AsyncSeq.ofSeq [1;2;3]) + |> Async.RunSynchronously + Assert.IsTrue(result) From 06e3f41cb26fa694c77ab1daf09a41834e633d98 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 29 Apr 2026 00:28:56 +0000 Subject: [PATCH 2/4] ci: trigger checks From 04c9321769a1bc578f81b8bb9642a73e9d973f65 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 00:25:06 +0000 Subject: [PATCH 3/4] =?UTF-8?q?eng:=20bump=20Microsoft.Bcl.AsyncInterfaces?= =?UTF-8?q?=2010.0.6=20=E2=86=92=2010.0.7=20to=20fix=20NU1605?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit System.Threading.Channels (resolved as 10.0.7 via Version="*") transitively requires Microsoft.Bcl.AsyncInterfaces >= 10.0.7, but the project pinned it at 10.0.6. This produced 5 NU1605 package- downgrade warnings on every restore/build. Bumping the direct reference to 10.0.7 aligns both packages to the same .NET 10.0.7 release, eliminating all 5 NU1605 warnings. Total build warnings: 12 → 7. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/FSharp.Control.AsyncSeq/FSharp.Control.AsyncSeq.fsproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FSharp.Control.AsyncSeq/FSharp.Control.AsyncSeq.fsproj b/src/FSharp.Control.AsyncSeq/FSharp.Control.AsyncSeq.fsproj index 90597602..c1090473 100644 --- a/src/FSharp.Control.AsyncSeq/FSharp.Control.AsyncSeq.fsproj +++ b/src/FSharp.Control.AsyncSeq/FSharp.Control.AsyncSeq.fsproj @@ -24,7 +24,7 @@ - + From cb3626532b141e3effae0d02669b4538845232c0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 May 2026 00:25:07 +0000 Subject: [PATCH 4/4] ci: trigger checks