The vsintegration cancellableTask computation expression cannot bind ValueTask
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
- Domain
- developer-experience, tooling
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
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:
-
Binding a
ValueTask<'T>requires converting it, allocating aTaskfor 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.GetProxyAsynccall binds with a barelet!inLanguageService/LanguageService.fs:552, because that block is FSharp.Core'stask { }, whoseLowPriority.Bind<^TaskLike, …>awaits any structural awaitable directly (src/FSharp.Core/tasks.fs:317-363). The gap is specific tocancellableTask. -
and!does not work for anything that reaches the builder as a bare awaiter.MergeSources(line 1201) accepts onlyCancellationToken -> 'Awaiter, while the genericSource(task: 'Awaitable)(line 702) yields a bare'Awaiter. Soand!is unavailable forValueTask<'T>,ValueTask, non-genericTask(which has noSourceoverload of its own either),ConfiguredTaskAwaitable<'T>and any otherGetAwaiter-shaped type. It has simply never been noticed:and!is used nowhere invsintegration. -
CancellableTask.getCancellationToken ()(line 962) is typedCancellationToken -> Task<CancellationToken>and implemented asTask.FromResult ct. Every one of its ~100 call sites across 48 files islet! ct = CancellableTask.getCancellationToken (), so the editor allocates aTaskper computation for a value it already holds. -
There is no async-disposal path (
UsingisIDisposable-only, line 750;DisposeAsyncreturnsValueTask) and nofor … inoverIAsyncEnumerable<'T>(MoveNextAsyncreturnsValueTask<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.
Sourceoverloads forValueTask<'T>,ValueTask,unit -> ValueTask<'T>andCancellationToken -> ValueTask<'T>, inHighPriority, each returning theCancellationToken -> 'Awaiterfunction shape the existingTaskoverloads use (lines 880-897). That shape is whatMergeSourcesneeds, so it fixesand!at the same time;SourceisinlineandBind'sgetAwaiteris[<InlineIfLambda>], so the lambda is inlined away and nothing is allocated. AddingSource(task: Task)alongside closes the sameand!hole for non-genericTask.getCancellationTokenreturnsCancellationToken -> ValueTask<CancellationToken>(ValueTask<CancellationToken>(ct), allocation-free, still takes the synchronousIsCompletedshortcut inBind). No call site changes.TryFinallyAsync+ anIAsyncDisposableUsingas intrinsic members, so it outranks the extensionIDisposableone exactly as FSharp.Core arranges it (src/FSharp.Core/tasks.fs:91-140vstasks.fs:352-361).ForoverIAsyncEnumerable<'T>, over aWhileAsyncwhose guard awaitsMoveNextAsync()'sValueTask<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 aTaskper call purely to satisfy the builder, and leavesand!broken. - Rely on the existing generic
Source(task: 'Awaitable)(line 702). Even where it resolves, it produces a bare awaiter, whichMergeSourcesrejects — soand!stays broken and the behaviour differs from every other bindable type. - Route through FSharp.Core's
Task.ofValueTask/ValueTaskmodule. Not available: both are inside#if NETSTANDARD2_1 || NET(src/FSharp.Core/tasks.fs:872-960), andFSharp.Editoris net472, which resolves FSharp.Core's netstandard2.0 asset. - Make the builder produce
ValueTaskas 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from dotnet/fsharp
-
Needs-Triage
Difficulty 1/5 Under an hour Newbie friendliness 90/100
-
Needs-Triage
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Bug Needs-Triage
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Needs-Triage
Difficulty 3/5 1-2 days Newbie friendliness 68/100
-
Bug Needs-Triage
Difficulty 4/5 3-5 days Newbie friendliness 35/100
Similar issues
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
babalae/bettergi-scripts-list#3674 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
caddyserver/caddy#8046 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
zilliztech/memsearch#759 ·
-
comp/cron P2 sweeper:risk-automation type/bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
NousResearch/hermes-agent#117792 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
bancolombia/sentinel#20 ·