diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md index 447db1c939b..79a6c119abb 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -10,24 +10,23 @@ * Fix array and string slices with extreme reversed bounds to return correctly shaped empty results. ([Issue #20530](https://github.com/dotnet/fsharp/issues/20530), [PR #20557](https://github.com/dotnet/fsharp/pull/20557)) * Fix printf handling of -0.0 (negative zero) values for float, float32, and decimal values ([Issue #15557](https://github.com/dotnet/fsharp/issues/15557) and [Issue #15558](https://github.com/dotnet/fsharp/issues/15558), [PR #18147](https://github.com/dotnet/fsharp/pull/18147)) - ### Added * Add the compiler-recognized `StateMachineHelpers.__runtimeAsyncReturn` intrinsic to the `net10.0` FSharp.Core target for .NET runtime-async methods. ([PR #20235](https://github.com/dotnet/fsharp/pull/20235)) * Add `Unchecked.withNull`, an interop escape hatch that re-types any `'T` to `'T | null` without the usual `not null`/`not struct` constraints, so unconstrained C# nullable-generic APIs (e.g. `T? M()`) can be implemented and consumed from F#. ([Issue #17734](https://github.com/dotnet/fsharp/issues/17734), [PR #20232](https://github.com/dotnet/fsharp/pull/20232)) -* Added generic `print` and `printn` functions (`'T -> unit`) to `ExtraTopLevelOperators` for simple value printing to stdout. ([RFC FS-1125](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1125-print-printn-functions.md), [PR #19265](https://github.com/dotnet/fsharp/pull/19265)) +* Add generic `print` and `printn` functions (`'T -> unit`) to `ExtraTopLevelOperators` for simple value printing to stdout. ([RFC FS-1125](https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1125-print-printn-functions.md), [PR #19265](https://github.com/dotnet/fsharp/pull/19265)) * Ship `FSharp.Core` with an additional `net10.0` target framework (next to `netstandard2.0` and `netstandard2.1`). The `net`-TFM assembly is public-surface-identical to the `netstandard2.1` one; the target version is a pinned, deliberately advanced knob. ([PR #20229](https://github.com/dotnet/fsharp/pull/20229)) * Add `Async.Await`, mirroring `Async.AwaitTask` semantics, but elides egregious `AggregateException` wrapping. Includes `ValueTask` support, and a SRTP-based overload accepting any Task-like value that supports the `GetAwaiter` protocol. ([Language Suggestion #840](https://github.com/fsharp/fslang-suggestions/issues/840), [PR #19785](https://github.com/dotnet/fsharp/pull/19785)) -* `Async.RunSynchronouslyImmediate`: runs work on the calling thread until the first asynchronous suspension (as opposed to `RunSynchronously`, which immediately offloads if not on a background and/or threadpool thread). ([Issue #1042](https://github.com/fsharp/fslang-suggestions/issues/1042), [PR #19804](https://github.com/dotnet/fsharp/pull/19804)) -* Added `AllowOverloadOnReturnTypeAttribute`: applying it to a method causes its return type to be considered during overload resolution, enabling overloads that differ only by return type. ([fslang-suggestions#820](https://github.com/fsharp/fslang-suggestions/issues/820), [PR #19602](https://github.com/dotnet/fsharp/pull/19602)) +* Add `Async.RunSynchronouslyImmediate`: runs work on the calling thread until the first asynchronous suspension (as opposed to `RunSynchronously`, which immediately offloads if not on a background and/or threadpool thread). ([Issue #1042](https://github.com/fsharp/fslang-suggestions/issues/1042), [PR #19804](https://github.com/dotnet/fsharp/pull/19804)) +* Add `AllowOverloadOnReturnTypeAttribute`: applying it to a method causes its return type to be considered during overload resolution, enabling overloads that differ only by return type. ([fslang-suggestions#820](https://github.com/fsharp/fslang-suggestions/issues/820), [PR #19602](https://github.com/dotnet/fsharp/pull/19602)) * Add `Async.StartTaskImmediate`: passes the ambient `Async.CancellationToken` to a task factory, then await the result using `Async.Await` semantics. Overloads for `Task`, `Task<'T>`, `ValueTask`, `ValueTask<'T>` and task-like `.GetAwaiter()` (via SRTP). ([Language Suggestion #1284](https://github.com/fsharp/fslang-suggestions/issues/1284), [PR #20258](https://github.com/dotnet/fsharp/pull/20258)) * Add modules for `Async`, `Task` and `ValueTask` with consistent `result`, `map`, `bind`, `ignore`, `catchWith`, `catch`, and `empty` functions ([LanguageSuggestion #1466](https://github.com/fsharp/fslang-suggestions/issues/1466), [PR #19844](https://github.com/dotnet/fsharp/pull/19844)) * Add conversion functions `Task.ofValueTask` and `ValueTask.ofTask`. ([LanguageSuggestion #1466](https://github.com/fsharp/fslang-suggestions/issues/1466), [PR #19844](https://github.com/dotnet/fsharp/pull/19844)) * Add `parallelLimit` to `Task` and `Async` modules, for bounded-parallelism execution of async/task computations, flowing cancellation and returning the results as an array. ([LanguageSuggestion #1467](https://github.com/fsharp/fslang-suggestions/issues/1467), [PR #20294](https://github.com/dotnet/fsharp/pull/20294)) * Add `parallelDoLimit` to `Task` and `Async` modules, for bounded-parallelism execution of async/task computations, flowing cancellation. ([LanguageSuggestion #1467](https://github.com/fsharp/fslang-suggestions/issues/1467), [PR #20294](https://github.com/dotnet/fsharp/pull/20294)) * Prototype a compiler-services entry and reference host for direct runtime-async sequences on the `net10.0` target, reusing the first enumerator instance. -* Add `sequentialDo` to `Task` and `Async`, running computations one at a time, flowing cancellation, without yielding an egregious `unit[]`. ([LanguageSuggestion #1467](https://github.com/fsharp/fslang-suggestions/issues/1467), [PR #20294](https://github.com/dotnet/fsharp/pull/20294)) -* Add `Task.sequential`, running task computations one at a time, flowing cancellation and returning the results as an array. ([LanguageSuggestion #1467](https://github.com/fsharp/fslang-suggestions/issues/1467), [PR #20294](https://github.com/dotnet/fsharp/pull/20294)) +* Add `sequentialDo` to `Task` and `Async`, running computations one at a time, flowing cancellation, without yielding an egregious `unit[]`. ([LanguageSuggestion #1467](https://github.com/fsharp/fslang-suggestions/issues/1467), [PR #20294](https://github.com/dotnet/fsharp/pull/20294), [PR #20695](https://github.com/dotnet/fsharp/pull/20695)) +* Add `Task.sequential`, running task computations one at a time, flowing cancellation and returning the results as an array. ([LanguageSuggestion #1467](https://github.com/fsharp/fslang-suggestions/issues/1467), [PR #20294](https://github.com/dotnet/fsharp/pull/20294), [PR #20695](https://github.com/dotnet/fsharp/pull/20695)) * Add `Task.startAsyncImmediate`, starting an `Async<'T>` on the current thread and returning a `Task<'T>`, flowing cancellation. ([LanguageSuggestion #1467](https://github.com/fsharp/fslang-suggestions/issues/1467), [PR #20294](https://github.com/dotnet/fsharp/pull/20294)) ### Changed diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 52bbf6f7bc5..062c6f3e159 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -8,7 +8,7 @@ * Go To Definition no longer blocks the UI thread with a bare `Task.Wait`: the synchronous `IFSharpGoToDefinitionService` call now waits through the cancellable threaded-wait dialog, and the editor's `TaskCompletionSource` bridges run their continuations on the thread pool instead of inline on whichever thread finished the check, so repeated F12 on a large solution no longer starves semantic classification and other main-thread work. ([PR #20482](https://github.com/dotnet/fsharp/pull/20482)) * Peek Definition on an F# symbol whose definition lives in metadata no longer deadlocks Visual Studio. Peek holds the main thread in `JoinableTaskFactory.Run` without pumping messages while it asks the language service for the definition, and generating the metadata document needs that same thread; Peek now stops at definitions that already have a document, and Go To Definition, which owns the wait it makes, still opens the generated one. ([PR #20503](https://github.com/dotnet/fsharp/pull/20503)) -* Improve Find All References performance by throttling parallel typechecks. ([PR #20128](https://github.com/dotnet/fsharp/pull/20128)) +* Improve Find All References performance by throttling parallel typechecks. ([PR #20128](https://github.com/dotnet/fsharp/pull/20128), [PR #20695](https://github.com/dotnet/fsharp/pull/20695)) * Fixed Rename incorrectly renaming `get` and `set` keywords for properties with explicit accessors. ([Issue #18270](https://github.com/dotnet/fsharp/issues/18270), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Fixed Find All References crash when F# project contains non-F# files like `.cshtml`. ([Issue #16394](https://github.com/dotnet/fsharp/issues/16394), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Avoid using `cancellableTask` in `DocumentCache`; the editor cache now uses direct `CancellationToken`-aware `task` wrappers, avoiding the background `Task.Run` offload and a larger wrapper closure from the `cancellableTask` builder. ([Issue #20268](https://github.com/dotnet/fsharp/issues/20268)) diff --git a/src/FSharp.Core/tasks.fs b/src/FSharp.Core/tasks.fs index dc8fe9f0dad..2cfe9820eaf 100644 --- a/src/FSharp.Core/tasks.fs +++ b/src/FSharp.Core/tasks.fs @@ -795,6 +795,7 @@ module Task = let mutable results = ArrayCollector<'T>() for f in computations do + ct.ThrowIfCancellationRequested() let! result = f ct results.Add result @@ -805,6 +806,7 @@ module Task = let sequentialDo (ct: CancellationToken) (computations: seq Task>) : Task = task { for f in computations do + ct.ThrowIfCancellationRequested() do! f ct } diff --git a/src/FSharp.Core/tasks.fsi b/src/FSharp.Core/tasks.fsi index 0a80e4363e9..475e71f378d 100644 --- a/src/FSharp.Core/tasks.fsi +++ b/src/FSharp.Core/tasks.fsi @@ -581,7 +581,7 @@ module Task = /// A cancellation token to pass to each task factory. /// A sequence of task start functions accepting a . /// A task yielding an array of the results of computations in the order they were supplied. - /// + /// Thrown if the provided cancellation token is canceled before the last computation starts, even if the computations ignore cancellation. /// /// /// task { @@ -598,7 +598,7 @@ module Task = /// A cancellation token to pass to each task factory. /// A sequence of unit task start functions accepting a . /// A task that runs all inputs in sequence and returns unit. - /// + /// Thrown if the provided cancellation token is canceled before the last computation starts, even if the computations ignore cancellation. /// /// /// task { diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/TaskModuleFunctions.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/TaskModuleFunctions.fs index da28c8c5449..a38e80f9133 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/TaskModuleFunctions.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/TaskModuleFunctions.fs @@ -366,6 +366,30 @@ module TaskModuleFunctionsTests = Assert.Equal(1, maxConcurrent) } + [] + let ``Task.sequential honors cancellation even if the computations do not`` before : Task = + task { + use cts = new CancellationTokenSource() + let computations: (CancellationToken -> Task) list = + if before then + cts.Cancel() + [ fun (_: CancellationToken) -> + task { return failwith "unexpected" }] + else + [ for i in 1..2 do + fun (_: CancellationToken) -> + task { + // whether cancellation during the last task is honored is up to the task in question + // the guarantee is that no further tasks will be started after cancellation is triggered + if i = 1 then + cts.Cancel() + return 42 + } + ] + let run (): Task = Task.sequential cts.Token computations + Assert.ThrowsAsync(run).Result |> ignore + } + [] let ``Task.sequentialDo runs all tasks in order and returns unit`` () : Task = @@ -400,7 +424,30 @@ module TaskModuleFunctionsTests = Assert.Equal(1, maxConcurrent) } - + [] + let ``Task.sequentialDo honors cancellation even if the computations do not`` before : Task = + task { + use cts = new CancellationTokenSource() + let computations: (CancellationToken -> Task) list = + if before then + cts.Cancel() + [ fun (_: CancellationToken) -> + task { return failwith "unexpected" }] + else + [ for i in 1..2 do + fun (_: CancellationToken) -> + task { + // whether cancellation during the last task is honored is up to the task in question + // the guarantee is that no further tasks will be started after cancellation is triggered + if i = 1 then + cts.Cancel() + } + ] + let run (): Task = Task.sequentialDo cts.Token computations + Assert.ThrowsAsync(run).Result |> ignore + } + + [] let ``Task.parallelLimit runs all tasks and collects results`` () : Task = task { diff --git a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs index 43a851ea908..400aa3cd1a7 100644 --- a/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs +++ b/vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs @@ -1102,35 +1102,7 @@ module CancellableTasks = /// Runs the given tasks concurrently, but caps concurrent work to maxDegreeOfParallelism. let inline whenAllThrottled maxDegreeOfParallelism (tasks: CancellableTask<'a> seq) = - cancellableTask { - let! ct = getCancellationToken () - let semaphore = new SemaphoreSlim(maxDegreeOfParallelism: int) - - let started = - [| - for task in tasks do - backgroundTask { - do! semaphore.WaitAsync(ct) - - try - return! start ct task - finally - semaphore.Release() |> ignore - } - |] - - let allTask = Task.WhenAll started - - allTask.ContinueWith( - (fun (_: Task<'a[]>) -> semaphore.Dispose()), - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default - ) - |> ignore - - return! allTask - } + fun ct -> Task.parallelLimit maxDegreeOfParallelism ct tasks let inline whenAllTasks (tasks: CancellableTask seq) = cancellableTask { @@ -1140,14 +1112,7 @@ module CancellableTasks = } let inline sequential (tasks: CancellableTask<'a> seq) = - cancellableTask { - let! ct = getCancellationToken () - let results = ResizeArray() - for task in tasks do - let! result = start ct task - results.Add(result) - return results - } + fun ct -> Task.sequential ct tasks let inline ignore ([] ctask: CancellableTask<_>) = toUnit ctask