diff --git a/AGENTS.md b/AGENTS.md index f40a85b..8debae2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 @@ -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, # - fixes , # + +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. diff --git a/Version.props b/Version.props index c424649..1bea20e 100644 --- a/Version.props +++ b/Version.props @@ -1,6 +1,6 @@ - - 1.0.0 + + $([System.Text.RegularExpressions.Regex]::Match($([System.IO.File]::ReadAllText(`$(MSBuildThisFileDirectory)release-notes.txt`)), `(?m)^(\d+\.\d+\.\d+)`).Groups.get_Item(1).Value) - \ No newline at end of file + diff --git a/release-notes.txt b/release-notes.txt index cfe2d7d..87ae35d 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -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 @@ -20,6 +22,9 @@ Release notes: - docs: adds missing XML 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) diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index b6f77a3..5232903 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -357,43 +357,31 @@ 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<_>) = @@ -401,24 +389,17 @@ module internal TaskSeqInternal = 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 } @@ -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 } @@ -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 } @@ -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 @@ -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 -> @@ -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