Skip to content

Commit a1b0b15

Browse files
authored
Merge branch 'main' into repo-assist/perf-take-skip-enumerators-b793dd82cef306c6
2 parents 5efa087 + 4ed6953 commit a1b0b15

8 files changed

Lines changed: 762 additions & 603 deletions

File tree

.github/aw/actions-lock.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@
1010
"version": "v8",
1111
"sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd"
1212
},
13-
"github/gh-aw/actions/[email protected]": {
13+
"github/gh-aw-actions/[email protected]": {
14+
"repo": "github/gh-aw-actions/setup",
15+
"version": "v0.61.1",
16+
"sha": "be0029bbbaeef8c6bea6825f31d9593219b2dc28"
17+
},
18+
"github/gh-aw/actions/[email protected]": {
1419
"repo": "github/gh-aw/actions/setup",
15-
"version": "v0.56.2",
16-
"sha": "f1073c5498ee46fec1530555a7c953445417c69b"
20+
"version": "v0.61.2",
21+
"sha": "d6f6273a03402cd530be35455a7823494b846d66"
1722
}
1823
}
1924
}

.github/workflows/repo-assist.lock.yml

Lines changed: 252 additions & 588 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/repo-assist.md

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description: |
3-
A friendly repository assistant that runs 4 times a day to support contributors and maintainers.
3+
A friendly repository assistant that runs 2 times a day to support contributors and maintainers.
44
Can also be triggered on-demand via '/repo-assist <instructions>' to perform specific tasks.
55
- Labels and triages open issues
66
- Comments helpfully on open issues to unblock contributors and onboard newcomers
@@ -33,6 +33,19 @@ network:
3333
- rust
3434
- java
3535

36+
checkout:
37+
fetch: ["*"] # fetch all remote branches to allow working on PR branches
38+
fetch-depth: 0 # fetch full history
39+
40+
tools:
41+
web-fetch:
42+
github:
43+
toolsets: [all]
44+
min-integrity: none # This workflow is allowed to examine and comment on any issues or PRs
45+
repos: all
46+
bash: true
47+
repo-memory: true
48+
3649
safe-outputs:
3750
add-comment:
3851
max: 10
@@ -65,13 +78,6 @@ safe-outputs:
6578
max: 5
6679
target: "*"
6780

68-
tools:
69-
web-fetch:
70-
github:
71-
toolsets: [all]
72-
bash: true
73-
repo-memory: true
74-
7581
steps:
7682
- name: Fetch repo data for task weighting
7783
env:
@@ -165,7 +171,7 @@ steps:
165171
json.dump(result, f, indent=2)
166172
EOF
167173
168-
source: githubnext/agentics/workflows/repo-assist.md@346204513ecfa08b81566450d7d599556807389f
174+
source: githubnext/agentics/workflows/repo-assist.md@d1d884596e62351dd652ae78465885dd32f0dd7d
169175
---
170176

171177
# Repo Assist

RELEASE_NOTES.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
### 4.11.0
2+
3+
* Design parity with FSharp.Control.TaskSeq (#277, batch 2):
4+
* Added `AsyncSeq.tryTail` — returns `None` if the sequence is empty; otherwise returns `Some` of the tail. Safe counterpart to `tail`. Mirrors `TaskSeq.tryTail`.
5+
* Added `AsyncSeq.where` / `AsyncSeq.whereAsync` — aliases for `filter` / `filterAsync`, mirroring the naming convention in `TaskSeq` and F# 8 collection expressions.
6+
* Added `AsyncSeq.lengthBy` / `AsyncSeq.lengthByAsync` — counts elements satisfying a predicate. Mirrors `TaskSeq.lengthBy` / `TaskSeq.lengthByAsync`.
7+
* Added `AsyncSeq.compareWith` / `AsyncSeq.compareWithAsync` — lexicographically compares two async sequences using a comparison function. Mirrors `TaskSeq.compareWith` / `TaskSeq.compareWithAsync`.
8+
* Added `AsyncSeq.takeWhileInclusiveAsync` — async variant of the existing `takeWhileInclusive`. Mirrors `TaskSeq.takeWhileInclusiveAsync`.
9+
* Added `AsyncSeq.skipWhileInclusive` / `AsyncSeq.skipWhileInclusiveAsync` — skips elements while predicate holds and also skips the first non-matching boundary element. Mirrors `TaskSeq.skipWhileInclusive` / `TaskSeq.skipWhileInclusiveAsync`.
10+
* Added `AsyncSeq.appendSeq` — appends a synchronous `seq<'T>` after an async sequence. Mirrors `TaskSeq.appendSeq`.
11+
* Added `AsyncSeq.prependSeq` — prepends a synchronous `seq<'T>` before an async sequence. Mirrors `TaskSeq.prependSeq`.
12+
* Added `AsyncSeq.delay` — defers sequence creation to enumeration time by calling a factory function each time `GetAsyncEnumerator` is called. Mirrors `TaskSeq.delay`.
13+
* Added `AsyncSeq.collectAsync` — like `collect` but the mapping function is asynchronous (`'T -> Async<AsyncSeq<'U>>`). Mirrors `TaskSeq.collectAsync`.
14+
* Added `AsyncSeq.partition` / `AsyncSeq.partitionAsync` — splits a sequence into two arrays using a (optionally async) predicate; the first array contains matching elements, the second non-matching. Mirrors `TaskSeq.partition` / `TaskSeq.partitionAsync`.
15+
16+
### 4.10.0
17+
18+
* Added `AsyncSeq.withCancellation` — returns a new `AsyncSeq` that passes the given `CancellationToken` to `GetAsyncEnumerator`, overriding whatever token would otherwise be supplied. Mirrors `TaskSeq.withCancellation` and is useful when consuming sequences from libraries (e.g. Entity Framework) that accept a cancellation token through `GetAsyncEnumerator`. Part of ongoing design-parity work with FSharp.Control.TaskSeq (see #277).
19+
120
### 4.9.0
221

322
* Performance: `filterAsync` — replaced `asyncSeq`-builder implementation with a direct optimised enumerator, reducing allocation and generator overhead.

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -492,8 +492,8 @@ module AsyncSeq =
492492
| Some enum -> dispose enum
493493
| None -> () }) :> AsyncSeq<'T>
494494

495-
let inline delay (f: unit -> AsyncSeq<'T>) : AsyncSeq<'T> =
496-
AsyncGenerator.delay f
495+
let delay (f: unit -> AsyncSeq<'T>) : AsyncSeq<'T> =
496+
AsyncSeqImpl(fun () -> (f()).GetEnumerator()) :> AsyncSeq<'T>
497497

498498
let bindAsync (f:'T -> AsyncSeq<'U>) (inp:Async<'T>) : AsyncSeq<'U> =
499499
AsyncSeqImpl(fun () ->
@@ -703,6 +703,9 @@ module AsyncSeq =
703703
let collect (f: 'T -> AsyncSeq<'U>) (inp: AsyncSeq<'T>) : AsyncSeq<'U> =
704704
AsyncSeqImpl(fun () -> new OptimizedCollectEnumerator<'T, 'U>(f, inp) :> IAsyncSeqEnumerator<'U>) :> AsyncSeq<'U>
705705

706+
let collectAsync (mapping: 'T -> Async<AsyncSeq<'U>>) (source: AsyncSeq<'T>) : AsyncSeq<'U> =
707+
collect (fun x -> bindAsync id (mapping x)) source
708+
706709
// let collect (f: 'T -> AsyncSeq<'U>) (inp: AsyncSeq<'T>) : AsyncSeq<'U> =
707710
// AsyncGenerator.collect f inp
708711

@@ -787,6 +790,12 @@ module AsyncSeq =
787790
dispose e
788791
| _ -> () }) :> AsyncSeq<'T>
789792

793+
let appendSeq (seq2: seq<'T>) (source: AsyncSeq<'T>) : AsyncSeq<'T> =
794+
append source (ofSeq seq2)
795+
796+
let prependSeq (seq1: seq<'T>) (source: AsyncSeq<'T>) : AsyncSeq<'T> =
797+
append (ofSeq seq1) source
798+
790799
// Optimized iterAsync implementation to reduce allocations
791800
type internal OptimizedIterAsyncEnumerator<'T>(enumerator: IAsyncSeqEnumerator<'T>, f: 'T -> Async<unit>) =
792801
let mutable disposed = false
@@ -1361,6 +1370,33 @@ module AsyncSeq =
13611370
let forallAsync f (source : AsyncSeq<'T>) =
13621371
source |> existsAsync (fun v -> async { let! b = f v in return not b }) |> Async.map not
13631372

1373+
let compareWithAsync (comparer: 'T -> 'T -> Async<int>) (source1: AsyncSeq<'T>) (source2: AsyncSeq<'T>) : Async<int> = async {
1374+
use ie1 = source1.GetEnumerator()
1375+
use ie2 = source2.GetEnumerator()
1376+
let! m1 = ie1.MoveNext()
1377+
let! m2 = ie2.MoveNext()
1378+
let b1 = ref m1
1379+
let b2 = ref m2
1380+
let result = ref 0
1381+
let isDone = ref false
1382+
while not isDone.Value do
1383+
match b1.Value, b2.Value with
1384+
| None, None -> isDone := true
1385+
| None, Some _ -> result := -1; isDone := true
1386+
| Some _, None -> result := 1; isDone := true
1387+
| Some v1, Some v2 ->
1388+
let! c = comparer v1 v2
1389+
if c <> 0 then result := c; isDone := true
1390+
else
1391+
let! n1 = ie1.MoveNext()
1392+
let! n2 = ie2.MoveNext()
1393+
b1 := n1
1394+
b2 := n2
1395+
return result.Value }
1396+
1397+
let compareWith (comparer: 'T -> 'T -> int) (source1: AsyncSeq<'T>) (source2: AsyncSeq<'T>) : Async<int> =
1398+
compareWithAsync (fun a b -> comparer a b |> async.Return) source1 source2
1399+
13641400
let foldAsync f (state:'State) (source : AsyncSeq<'T>) =
13651401
match source with
13661402
| :? AsyncSeqOp<'T> as source -> source.FoldAsync f state
@@ -1418,6 +1454,12 @@ module AsyncSeq =
14181454
let length (source : AsyncSeq<'T>) =
14191455
fold (fun st _ -> st + 1L) 0L source
14201456

1457+
let lengthByAsync (predicate: 'T -> Async<bool>) (source: AsyncSeq<'T>) : Async<int64> =
1458+
foldAsync (fun acc x -> async { let! ok = predicate x in return if ok then acc + 1L else acc }) 0L source
1459+
1460+
let lengthBy (predicate: 'T -> bool) (source: AsyncSeq<'T>) : Async<int64> =
1461+
lengthByAsync (predicate >> async.Return) source
1462+
14211463
let inline sum (source : AsyncSeq<'T>) : Async<'T> =
14221464
(LanguagePrimitives.GenericZero, source) ||> fold (+)
14231465

@@ -1548,6 +1590,12 @@ module AsyncSeq =
15481590
let filter f (source : AsyncSeq<'T>) =
15491591
filterAsync (f >> async.Return) source
15501592

1593+
let where (predicate: 'T -> bool) (source: AsyncSeq<'T>) : AsyncSeq<'T> =
1594+
filter predicate source
1595+
1596+
let whereAsync (predicate: 'T -> Async<bool>) (source: AsyncSeq<'T>) : AsyncSeq<'T> =
1597+
filterAsync predicate source
1598+
15511599
let except (excluded : seq<'T>) (source : AsyncSeq<'T>) : AsyncSeq<'T> =
15521600
let s = System.Collections.Generic.HashSet(excluded)
15531601
source |> filter (fun x -> not (s.Contains(x)))
@@ -1898,6 +1946,24 @@ module AsyncSeq =
18981946
interface System.IDisposable with
18991947
member _.Dispose() = en.Dispose() }) :> AsyncSeq<'a>
19001948

1949+
let takeWhileInclusiveAsync (predicate: 'T -> Async<bool>) (source: AsyncSeq<'T>) : AsyncSeq<'T> =
1950+
AsyncSeqImpl(fun () ->
1951+
let en = source.GetEnumerator()
1952+
let fin = ref false
1953+
{ new IAsyncSeqEnumerator<'T> with
1954+
member _.MoveNext() = async {
1955+
if !fin then return None
1956+
else
1957+
let! next = en.MoveNext()
1958+
match next with
1959+
| None -> return None
1960+
| Some a ->
1961+
let! ok = predicate a
1962+
if ok then return Some a
1963+
else fin := true; return Some a }
1964+
interface System.IDisposable with
1965+
member _.Dispose() = en.Dispose() }) :> AsyncSeq<'T>
1966+
19011967
let skipWhileAsync p (source : AsyncSeq<'T>) : AsyncSeq<_> = asyncSeq {
19021968
use ie = source.GetEnumerator()
19031969
let! move = ie.MoveNext()
@@ -1915,6 +1981,27 @@ module AsyncSeq =
19151981
let! moven = ie.MoveNext()
19161982
b := moven }
19171983

1984+
let skipWhileInclusiveAsync (predicate: 'T -> Async<bool>) (source: AsyncSeq<'T>) : AsyncSeq<'T> = asyncSeq {
1985+
use ie = source.GetEnumerator()
1986+
let! move = ie.MoveNext()
1987+
let b = ref move
1988+
let doneSkipping = ref false
1989+
while b.Value.IsSome do
1990+
let v = b.Value.Value
1991+
if doneSkipping.Value then
1992+
yield v
1993+
let! moven = ie.MoveNext()
1994+
b := moven
1995+
else
1996+
let! test = predicate v
1997+
if not test then
1998+
doneSkipping := true // skip this boundary element; do not yield it
1999+
let! moven = ie.MoveNext()
2000+
b := moven }
2001+
2002+
let skipWhileInclusive (predicate: 'T -> bool) (source: AsyncSeq<'T>) : AsyncSeq<'T> =
2003+
skipWhileInclusiveAsync (predicate >> async.Return) source
2004+
19182005
#if !FABLE_COMPILER
19192006
let skipUntilSignal (signal:Async<unit>) (source:AsyncSeq<'T>) : AsyncSeq<'T> = asyncSeq {
19202007
use ie = source.GetEnumerator()
@@ -1959,6 +2046,25 @@ module AsyncSeq =
19592046

19602047
let tail (source : AsyncSeq<'T>) : AsyncSeq<'T> = skip 1 source
19612048

2049+
let tryTail (source: AsyncSeq<'T>) : Async<AsyncSeq<'T> option> = async {
2050+
let ie = source.GetEnumerator()
2051+
let! first = ie.MoveNext()
2052+
match first with
2053+
| None ->
2054+
ie.Dispose()
2055+
return None
2056+
| Some _ ->
2057+
return Some (asyncSeq {
2058+
try
2059+
let! next = ie.MoveNext()
2060+
let b = ref next
2061+
while b.Value.IsSome do
2062+
yield b.Value.Value
2063+
let! moven = ie.MoveNext()
2064+
b := moven
2065+
finally
2066+
ie.Dispose() }) }
2067+
19622068
/// Splits an async sequence at the given index, returning the first `count` elements as an array
19632069
/// and the remaining elements as a new AsyncSeq. The source is enumerated once.
19642070
let splitAt (count: int) (source: AsyncSeq<'T>) : Async<'T array * AsyncSeq<'T>> = async {
@@ -2005,6 +2111,17 @@ module AsyncSeq =
20052111
let toArraySynchronously (source:AsyncSeq<'T>) = toArrayAsync source |> Async.RunSynchronously
20062112
#endif
20072113

2114+
let partitionAsync (predicate: 'T -> Async<bool>) (source: AsyncSeq<'T>) : Async<'T[] * 'T[]> = async {
2115+
let trues = ResizeArray<'T>()
2116+
let falses = ResizeArray<'T>()
2117+
do! source |> iterAsync (fun x -> async {
2118+
let! ok = predicate x
2119+
(if ok then trues else falses).Add(x) })
2120+
return trues.ToArray(), falses.ToArray() }
2121+
2122+
let partition (predicate: 'T -> bool) (source: AsyncSeq<'T>) : Async<'T[] * 'T[]> =
2123+
partitionAsync (predicate >> async.Return) source
2124+
20082125
let concatSeq (source:AsyncSeq<#seq<'T>>) : AsyncSeq<'T> = asyncSeq {
20092126
use ie = source.GetEnumerator()
20102127
let! move = ie.MoveNext()
@@ -2551,6 +2668,13 @@ module AsyncSeq =
25512668
(emptyAsync fillChannelTask)
25522669
}
25532670

2671+
/// Returns a new AsyncSeq that passes the given CancellationToken to GetAsyncEnumerator,
2672+
/// overriding whatever token would otherwise be used. Useful when consuming sequences from
2673+
/// libraries (such as Entity Framework) that accept a CancellationToken through GetAsyncEnumerator.
2674+
let withCancellation (cancellationToken: CancellationToken) (source: AsyncSeq<'T>) : AsyncSeq<'T> =
2675+
{ new IAsyncEnumerable<'T> with
2676+
member _.GetAsyncEnumerator(_ct) = source.GetAsyncEnumerator(cancellationToken) }
2677+
25542678
#endif
25552679

25562680

0 commit comments

Comments
 (0)