Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 20 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ FSharp.Control.TaskSeq is an F# library providing a `taskSeq` computation expres
- `src/FSharp.Control.TaskSeq.Test/` β€” xUnit test project (net9.0)
- `src/FSharp.Control.TaskSeq.SmokeTests/` β€” Smoke/integration tests
- `src/FSharp.Control.TaskSeq.sln` β€” Solution file
- `Version.props` β€” Single source of truth for the package version
- `Version.props` β€” Package version (derived automatically from `release-notes.txt`)
- `build.cmd` β€” Windows build/test script used by CI

## Build
Expand Down Expand Up @@ -102,12 +102,28 @@ All workflows are in `.github/workflows/`:

## Release Notes

**Required**: Every PR that adds features, fixes bugs, or makes user-visible changes **must** include an update to `release-notes.txt`. Add a bullet under the appropriate version heading (currently `0.5.0`). The format is:
`release-notes.txt` is the **single source of truth** for the package version. `Version.props` extracts the version automatically by finding the first line that matches a `X.Y.Z` semver pattern. The `Unreleased` section at the top of the file is skipped because it does not match this pattern.

**Format requirements:**

- The file **must** always begin with a heading line `Unreleased` (after the optional `Release notes:` header). This section holds in-progress changes before they are assigned a version number. It must always be present, even if empty.
- Below `Unreleased`, versioned sections are listed in descending order (`1.0.0`, `0.7.0`, …). The topmost versioned section determines the package version.
- To bump the version, add a new version heading between `Unreleased` and the previous version.

Example:

```
0.5.0
Release notes:

Unreleased
- upcoming change description

1.1.0
- adds TaskSeq.myFunction and TaskSeq.myFunctionAsync, #<issue>
- fixes <description>, #<issue>

1.0.0
- adds TaskSeq.withCancellation, #167
```

If you are bumping to a new version, also update `Version.props`. PRs that touch library source (`src/FSharp.Control.TaskSeq/`) without updating `release-notes.txt` are incomplete.
**Required**: Every PR that adds features, fixes bugs, or makes user-visible changes **must** add a bullet under the `Unreleased` heading in `release-notes.txt`. PRs that touch library source (`src/FSharp.Control.TaskSeq/`) without updating `release-notes.txt` are incomplete.
6 changes: 3 additions & 3 deletions Version.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<!-- updating this version will trigger a publish after merge to 'main' -->
<Version>1.0.0</Version>
<!-- Version is extracted from the first versioned section heading in release-notes.txt -->
<Version>$([System.Text.RegularExpressions.Regex]::Match($([System.IO.File]::ReadAllText(`$(MSBuildThisFileDirectory)release-notes.txt`)), `(?m)^(\d+\.\d+\.\d+)`).Groups.get_Item(1).Value)</Version>
</PropertyGroup>
</Project>
</Project>
9 changes: 7 additions & 2 deletions release-notes.txt
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@

Release notes:

1.0.0
Unreleased

1.1.0
- adds taskSeqDynamic computation expression and TaskSeqDynamic/TaskSeqDynamicInfo types for dynamic (FSI-compatible) resumable code, fixing issue where taskSeq would raise NotImplementedException in F# Interactive, #246
- perf: simplify iter, fold, reduce, mapFold, tryLast, skipOrTake (Drop/Truncate) to use while! and remove manual go-flag and initial MoveNextAsync pre-advance, matching the pattern already used by sum/sumBy/average
- perf: toResizeArrayAsync (and therefore toArrayAsync, toListAsync, toResizeArrayAsync, toIListAsync) uses a direct loop instead of going through iter, avoiding a lambda and DU allocation per call
- perf: tryItem uses a simpler loop that skips the redundant inner index check on every iteration
- perf: TaskSeq.chunkBy and chunkByAsync reuse the ResizeArray buffer between chunks, reducing allocations on sequences with many chunk boundaries
- fixes: TaskSeq.insertAt, insertManyAt, removeAt, removeManyAt, updateAt now raise ArgumentNullException (not NullReferenceException) when given a null source; insertManyAt also validates the values argument
- refactor: simplify lengthBy and lengthBeforeMax to use while! and remove the redundant mutable 'go' and initial MoveNextAsync
- adds TaskSeq.distinctUntilChangedWith and TaskSeq.distinctUntilChangedWithAsync, #345
- adds TaskSeq.withCancellation, #167
- adds TaskSeq.replicateInfinite, replicateInfiniteAsync, replicateUntilNoneAsync, #345
- adds TaskSeq.firstOrDefault, lastOrDefault, #345
- adds TaskSeq.splitAt, #345
Expand All @@ -20,6 +22,9 @@ Release notes:
- docs: adds missing XML <returns> documentation tags to singleton, isEmpty, length, lengthOrMax, lengthBy, and lengthByAsync
- test: adds 70 new tests to TaskSeq.Fold.Tests.fs covering call-count assertions, folder-not-called-on-empty, ordering, null initial state, and fold/foldAsync equivalence

1.0.0
- adds TaskSeq.withCancellation, #167

0.7.0
- performance: TaskSeq.exists, existsAsync, contains no longer allocate an intermediate Option value
- test: adds 67 tests for TaskSeq.lengthOrMax (previously untested)
Expand Down
113 changes: 34 additions & 79 deletions src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
Original file line number Diff line number Diff line change
Expand Up @@ -357,68 +357,49 @@ module internal TaskSeqInternal =

task {
use e = source.GetAsyncEnumerator CancellationToken.None
let mutable go = true
let! step = e.MoveNextAsync()
go <- step

// this ensures that the inner loop is optimized for the closure
// though perhaps we need to split into individual functions after all to use
// InlineIfLambda?
// Each branch keeps its own while! loop so the match dispatch is hoisted out and
// the JIT sees a tight, single-case loop (same pattern as sum/sumBy etc.).
match action with
| CountableAction action ->
let mutable i = 0

while go do
do action i e.Current
let! step = e.MoveNextAsync()
while! e.MoveNextAsync() do
action i e.Current
i <- i + 1
go <- step

| SimpleAction action ->
while go do
do action e.Current
let! step = e.MoveNextAsync()
go <- step
while! e.MoveNextAsync() do
action e.Current

| AsyncCountableAction action ->
let mutable i = 0

while go do
while! e.MoveNextAsync() do
do! action i e.Current
let! step = e.MoveNextAsync()
i <- i + 1
go <- step

| AsyncSimpleAction action ->
while go do
while! e.MoveNextAsync() do
do! action e.Current
let! step = e.MoveNextAsync()
go <- step
}

let fold folder initial (source: TaskSeq<_>) =
checkNonNull (nameof source) source

task {
use e = source.GetAsyncEnumerator CancellationToken.None
let mutable go = true
let mutable result = initial
let! step = e.MoveNextAsync()
go <- step

match folder with
| FolderAction folder ->
while go do
while! e.MoveNextAsync() do
result <- folder result e.Current
let! step = e.MoveNextAsync()
go <- step

| AsyncFolderAction folder ->
while go do
while! e.MoveNextAsync() do
let! tempResult = folder result e.Current
result <- tempResult
let! step = e.MoveNextAsync()
go <- step

return result
}
Expand Down Expand Up @@ -457,22 +438,16 @@ module internal TaskSeqInternal =
raiseEmptySeq ()

let mutable result = e.Current
let! step = e.MoveNextAsync()
let mutable go = step

match folder with
| FolderAction folder ->
while go do
while! e.MoveNextAsync() do
result <- folder result e.Current
let! step = e.MoveNextAsync()
go <- step

| AsyncFolderAction folder ->
while go do
while! e.MoveNextAsync() do
let! tempResult = folder result e.Current
result <- tempResult
let! step = e.MoveNextAsync()
go <- step

return result
}
Expand All @@ -482,28 +457,21 @@ module internal TaskSeqInternal =

task {
use e = source.GetAsyncEnumerator CancellationToken.None
let mutable go = true
let mutable state = initial
let results = ResizeArray()
let! step = e.MoveNextAsync()
go <- step

match folder with
| MapFolderAction folder ->
while go do
while! e.MoveNextAsync() do
let result, newState = folder state e.Current
results.Add result
state <- newState
let! step = e.MoveNextAsync()
go <- step

| AsyncMapFolderAction folder ->
while go do
while! e.MoveNextAsync() do
let! (result, newState) = folder state e.Current
results.Add result
state <- newState
let! step = e.MoveNextAsync()
go <- step

return results.ToArray(), state
}
Expand Down Expand Up @@ -808,15 +776,10 @@ module internal TaskSeqInternal =

task {
use e = source.GetAsyncEnumerator CancellationToken.None
let mutable go = true
let mutable last = ValueNone
let! step = e.MoveNextAsync()
go <- step

while go do
while! e.MoveNextAsync() do
last <- ValueSome e.Current
let! step = e.MoveNextAsync()
go <- step

match last with
| ValueSome value -> return Some value
Expand Down Expand Up @@ -1237,24 +1200,19 @@ module internal TaskSeqInternal =
else
taskSeq {
use e = source.GetAsyncEnumerator CancellationToken.None
let mutable i = 0
let mutable cont = true

let! step = e.MoveNextAsync()
let mutable cont = step
let mutable pos = 0

// skip, or stop looping if we reached the end
while cont do
pos <- pos + 1

if pos < count then
let! moveNext = e.MoveNextAsync()
cont <- moveNext
else
cont <- false
// advance past 'count' elements; stop early if the source is shorter
while cont && i < count do
let! hasMore = e.MoveNextAsync()
if hasMore then i <- i + 1 else cont <- false

// return the rest
while! e.MoveNextAsync() do
yield e.Current
// return remaining elements; enumerator is at element (count-1) so one
// more MoveNext is needed to reach element (count)
if cont then
while! e.MoveNextAsync() do
yield e.Current

}
| Take ->
Expand All @@ -1281,19 +1239,16 @@ module internal TaskSeqInternal =
else
taskSeq {
use e = source.GetAsyncEnumerator CancellationToken.None
let mutable yielded = 0
let mutable cont = true

let! step = e.MoveNextAsync()
let mutable cont = step
let mutable pos = 0

// return items until we've exhausted the seq
while cont do
yield e.Current
pos <- pos + 1
// yield up to 'count' elements; stop when exhausted or limit reached
while cont && yielded < count do
let! hasMore = e.MoveNextAsync()

if pos < count then
let! moveNext = e.MoveNextAsync()
cont <- moveNext
if hasMore then
yield e.Current
yielded <- yielded + 1
else
cont <- false

Expand Down
Loading