The vsintegration cancellableTask computation expression cannot bind ValueTask

Open
#20,596 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
48/100
Issue type
Feature
Clarity
Clearly specified
Activity status
Active

Research direction

Start with vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs, especially Source, MergeSources, Using, and getCancellationToken, then compare the relevant patterns in src/FSharp.Core/tasks.fs. Review the referenced call sites and compiler handling of while!; done means native ValueTask binding, async disposal and async enumeration support across the listed CE positions, with tests added for each because no existing CE test is mentioned.

Written by the indexing model from the issue text.

Description

Needs-Triage

Is your feature request related to a problem? Please describe.

vsintegration/src/FSharp.Editor/Common/CancellableTasks.fs defines the cancellableTask / foregroundCancellableTask builder that ~260 blocks across FSharp.Editor are written in. It has explicit Source overloads for Task<'T>, unit -> Task<'T>, CancellableTask<'T>, Async<'T> and TaskAwaiter<'T> — and no mention of ValueTask anywhere in the file.

Consequences today:

  1. Binding a ValueTask<'T> requires converting it, allocating a Task for no reason. The only such call site in the repo, Copilot/CopilotContextProvider.fs:439:

    use! rental = documentContexts.GetProxyAsync<ICopilotDocumentContextProvider>(CopilotDescriptors.Context.Document, ct).AsTask()
    

    The same ServiceBrokerClient.GetProxyAsync call binds with a bare let! in LanguageService/LanguageService.fs:552, because that block is FSharp.Core's task { }, whose LowPriority.Bind<^TaskLike, …> awaits any structural awaitable directly (src/FSharp.Core/tasks.fs:317-363). The gap is specific to cancellableTask.

  2. and! does not work for anything that reaches the builder as a bare awaiter. MergeSources (line 1201) accepts only CancellationToken -> 'Awaiter, while the generic Source(task: 'Awaitable) (line 702) yields a bare 'Awaiter. So and! is unavailable for ValueTask<'T>, ValueTask, non-generic Task (which has no Source overload of its own either), ConfiguredTaskAwaitable<'T> and any other GetAwaiter-shaped type. It has simply never been noticed: and! is used nowhere in vsintegration.

  3. CancellableTask.getCancellationToken () (line 962) is typed CancellationToken -> Task<CancellationToken> and implemented as Task.FromResult ct. Every one of its ~100 call sites across 48 files is let! ct = CancellableTask.getCancellationToken (), so the editor allocates a Task per computation for a value it already holds.

  4. There is no async-disposal path (Using is IDisposable-only, line 750; DisposeAsync returns ValueTask) and no for … in over IAsyncEnumerable<'T> (MoveNextAsync returns ValueTask<bool>).

Describe the solution you'd like

Bind ValueTask natively — through its own ValueTaskAwaiter, never via .AsTask() — in every position the CE supports: let!, do!, match!, while!, and!, return!, use!, for.

  • Source overloads for ValueTask<'T>, ValueTask, unit -> ValueTask<'T> and CancellationToken -> ValueTask<'T>, in HighPriority, each returning the CancellationToken -> 'Awaiter function shape the existing Task overloads use (lines 880-897). That shape is what MergeSources needs, so it fixes and! at the same time; Source is inline and Bind's getAwaiter is [<InlineIfLambda>], so the lambda is inlined away and nothing is allocated. Adding Source(task: Task) alongside closes the same and! hole for non-generic Task.
  • getCancellationToken returns CancellationToken -> ValueTask<CancellationToken> (ValueTask<CancellationToken>(ct), allocation-free, still takes the synchronous IsCompleted shortcut in Bind). No call site changes.
  • TryFinallyAsync + an IAsyncDisposable Using as intrinsic members, so it outranks the extension IDisposable one exactly as FSharp.Core arranges it (src/FSharp.Core/tasks.fs:91-140 vs tasks.fs:352-361).
  • For over IAsyncEnumerable<'T>, over a WhileAsync whose guard awaits MoveNextAsync()'s ValueTask<bool>.

while! needs no new builder member: the compiler desugars it into let! + a plain while (src/Compiler/Checking/Expressions/CheckComputationExpressions.fs:1528-1628), so it starts working as soon as the guard's ValueTask<bool> binds.

There is no test for this CE anywhere in the repo today, so the change should come with one covering each of the above.

Describe alternatives you've considered

  • Keep .AsTask() at the call sites. Allocates a Task per call purely to satisfy the builder, and leaves and! broken.
  • Rely on the existing generic Source(task: 'Awaitable) (line 702). Even where it resolves, it produces a bare awaiter, which MergeSources rejects — so and! stays broken and the behaviour differs from every other bindable type.
  • Route through FSharp.Core's Task.ofValueTask / ValueTask module. Not available: both are inside #if NETSTANDARD2_1 || NET (src/FSharp.Core/tasks.fs:872-960), and FSharp.Editor is net472, which resolves FSharp.Core's netstandard2.0 asset.
  • Make the builder produce ValueTask as well. Out of scope; this is only about consuming them.

Additional context

FSharp.Editor targets net472 only (vsintegration/Directory.Build.props:4). Everything needed is already a compile-time reference, transitively, with no new PackageReference: ValueTask, ValueTask<'T>, ValueTaskAwaiter(<'T>) and IValueTaskSource<'T> from System.Threading.Tasks.Extensions/4.6.3 (lib/net462), and IAsyncDisposable / IAsyncEnumerable<'T> from Microsoft.Bcl.AsyncInterfaces/10.0.10 (lib/net462).

One net472 caveat for the implementation: the ValueTask.FromResult / ValueTask.CompletedTask / ValueTask.FromCanceled statics are .NET 5+ and do not exist there — the constructors ValueTask<'T>(v) and ValueTask() are what CopilotContextProvider.fs:563-569 already uses.

The file is derived from IcedTasks, which has the same gap upstream.

Dominant language
F#
Stars
4.3k
Forks
877
Avg merge
5d 17h
Merged PRs (30d)
139

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/fsharp

All issues in dotnet/fsharp

Similar issues

More DevTools issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.