Skip to content

Commit 04a73dd

Browse files
Repo AssistCopilot
authored andcommitted
Add YieldFromFinal overloads for F# 10 compatibility (#62)
- Add YieldFromFinal(IAsyncEnumerable<'T>) in MediumPriority (= YieldFrom) - Add YieldFromFinal(seq<'T>) in MediumPriority (= YieldFrom) - Add YieldFromFinal for generic unit-returning task-likes in LowPriority - Add YieldFromFinal(Task<unit>) and YieldFromFinal(Async<unit>) in HighPriority - Update TaskSeqBuilder.fsi with matching signatures - Update release-notes.txt The F# 10 compiler (dotnet/fsharp#18804) calls YieldFromFinal instead of YieldFrom when yield! or do! appears in a tail-call position. These overloads ensure taskSeq compiles without errors under F# 10. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent dea5779 commit 04a73dd

File tree

3 files changed

+89
-0
lines changed

3 files changed

+89
-0
lines changed

release-notes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Release notes:
33

44
0.5.0
5+
- adds YieldFromFinal to the taskSeq builder for F# 10 compatibility (tail-positioned yield! and do!), #62
56
- update engineering to .NET 9/10
67
- adds TaskSeq.scan and TaskSeq.scanAsync, #289
78
- adds TaskSeq.pairwise, #289

src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,35 @@ module LowPriority =
570570
sm.Data.current <- ValueNone
571571
false)
572572

573+
// YieldFromFinal for generic task-like in tail call position (handles do! in tail position).
574+
// Handles: non-generic Task, non-generic ValueTask, ValueTask<unit>, and other unit-returning task-likes.
575+
// NOT handled: Task<'T> (see HighPriority below for that).
576+
[<NoEagerConstraintApplication>]
577+
member inline _.YieldFromFinal< ^TaskLike, 'T, ^Awaiter
578+
when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter)
579+
and ^Awaiter :> ICriticalNotifyCompletion
580+
and ^Awaiter: (member get_IsCompleted: unit -> bool)
581+
and ^Awaiter: (member GetResult: unit -> unit)>
582+
(task: ^TaskLike)
583+
: ResumableTSC<'T> =
584+
585+
// Inline the await pattern to avoid constraint propagation issues with Bind.
586+
ResumableTSC<'T>(fun sm ->
587+
let mutable awaiter = (^TaskLike: (member GetAwaiter: unit -> ^Awaiter) (task))
588+
let mutable __stack_fin = true
589+
590+
if not (^Awaiter: (member get_IsCompleted: unit -> bool) awaiter) then
591+
let __stack_fin2 = ResumableCode.Yield().Invoke(&sm)
592+
__stack_fin <- __stack_fin2
593+
594+
if __stack_fin then
595+
(^Awaiter: (member GetResult: unit -> unit) awaiter)
596+
true // zero: signal done, no elements
597+
else
598+
sm.Data.awaiter <- awaiter
599+
sm.Data.current <- ValueNone
600+
false)
601+
573602

574603
[<AutoOpen>]
575604
module MediumPriority =
@@ -607,6 +636,17 @@ module MediumPriority =
607636

608637
member inline this.YieldFrom(source: IAsyncEnumerable<'T>) = this.For(source, (fun v -> this.Yield(v)))
609638

639+
/// Called by the F# compiler when <c>yield!</c> appears in a tail-call position within a
640+
/// <c>taskSeq</c>. Currently behaves identically to <c>YieldFrom</c>; the method exists
641+
/// so F# 10+ can call it for tail-positioned <c>yield!</c> expressions. A zero-copy
642+
/// tail-delegation optimisation requires additional state-machine driver support and is
643+
/// left as future work.
644+
member inline this.YieldFromFinal(source: IAsyncEnumerable<'T>) : ResumableTSC<'T> = this.YieldFrom(source)
645+
646+
/// Called by the F# compiler when <c>yield!</c> appears in a tail-call position over a
647+
/// synchronous sequence. Behaves identically to <c>YieldFrom</c>.
648+
member inline this.YieldFromFinal(source: seq<'T>) : ResumableTSC<'T> = this.YieldFrom(source)
649+
610650
[<AutoOpen>]
611651
module HighPriority =
612652
type TaskSeqBuilder with
@@ -681,6 +721,13 @@ module HighPriority =
681721
sm.Data.current <- ValueNone
682722
false)
683723

724+
// YieldFromFinal for Task<'T> and Async<'T> in tail call position (handles do! in tail position).
725+
// Task<unit> needs its own overload here (at HighPriority) for the same reason Bind does:
726+
// TaskAwaiter<unit>.GetResult() -> unit differs from TaskAwaiter.GetResult() -> void.
727+
member inline this.YieldFromFinal(task: Task<unit>) : ResumableTSC<'T> = this.Bind(task, (fun () -> this.Zero()))
728+
729+
member inline this.YieldFromFinal(computation: Async<unit>) : ResumableTSC<'T> = this.Bind(computation, (fun () -> this.Zero()))
730+
684731
[<AutoOpen>]
685732
module TaskSeqBuilder =
686733
/// Builds an asynchronous task sequence based on IAsyncEnumerable<'T> using computation expression syntax.

src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,20 @@ module LowPriority =
180180
and ^Awaiter: (member get_IsCompleted: unit -> bool)
181181
and ^Awaiter: (member GetResult: unit -> 'T)
182182

183+
/// <summary>
184+
/// Called by the F# compiler when <c>do!</c> with a unit-returning task-like appears in a
185+
/// tail-call position. Handles non-generic <c>Task</c>, non-generic <c>ValueTask</c>,
186+
/// <c>ValueTask&lt;unit&gt;</c>, and other unit-returning task-likes. Falls back to
187+
/// <c>Bind</c> + <c>Zero</c>, signalling sequence end after awaiting the task.
188+
/// </summary>
189+
[<NoEagerConstraintApplication>]
190+
member inline YieldFromFinal< ^TaskLike, 'T, ^Awaiter> :
191+
task: ^TaskLike -> ResumableTSC<'T>
192+
when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter)
193+
and ^Awaiter :> ICriticalNotifyCompletion
194+
and ^Awaiter: (member get_IsCompleted: unit -> bool)
195+
and ^Awaiter: (member GetResult: unit -> unit)
196+
183197
/// <summary>
184198
/// Contains low priority extension methods for the main builder class for the <see cref="taskSeq" /> computation expression.
185199
/// The <see cref="LowPriority" />, <see cref="MediumPriority" /> and <see cref="HighPriority" /> modules are not meant to be
@@ -198,6 +212,21 @@ module MediumPriority =
198212
member inline For: source: #TaskSeq<'TElement> * body: ('TElement -> ResumableTSC<'T>) -> ResumableTSC<'T>
199213
member inline YieldFrom: source: TaskSeq<'T> -> ResumableTSC<'T>
200214

215+
/// <summary>
216+
/// Called by the F# compiler when <c>yield!</c> appears in a tail-call position within a
217+
/// <c>taskSeq</c> computation expression. Currently behaves identically to <c>YieldFrom</c>;
218+
/// the method exists so F# 10+ can recognise and call it for tail-positioned <c>yield!</c>
219+
/// expressions without a compilation error.
220+
/// </summary>
221+
member inline YieldFromFinal: source: TaskSeq<'T> -> ResumableTSC<'T>
222+
223+
/// <summary>
224+
/// Called by the F# compiler when <c>yield!</c> appears in a tail-call position within a
225+
/// <c>taskSeq</c> computation expression over a synchronous sequence. Behaves identically
226+
/// to <c>YieldFrom</c>.
227+
/// </summary>
228+
member inline YieldFromFinal: source: seq<'T> -> ResumableTSC<'T>
229+
201230
/// <summary>
202231
/// Contains low priority extension methods for the main builder class for the <see cref="taskSeq" /> computation expression.
203232
/// The <see cref="LowPriority" />, <see cref="MediumPriority" /> and <see cref="HighPriority" /> modules are not meant to be
@@ -209,3 +238,15 @@ module HighPriority =
209238

210239
member inline Bind: task: Task<'T> * continuation: ('T -> ResumableTSC<'U>) -> ResumableTSC<'U>
211240
member inline Bind: computation: Async<'T> * continuation: ('T -> ResumableTSC<'U>) -> ResumableTSC<'U>
241+
242+
/// <summary>
243+
/// Called by the F# compiler when <c>do!</c> with a <c>Task&lt;unit&gt;</c> appears in a
244+
/// tail-call position. Signals sequence end after awaiting the task.
245+
/// </summary>
246+
member inline YieldFromFinal: task: Task<unit> -> ResumableTSC<'T>
247+
248+
/// <summary>
249+
/// Called by the F# compiler when <c>do!</c> with an <c>Async&lt;unit&gt;</c> appears in a
250+
/// tail-call position. Signals sequence end after the async computation completes.
251+
/// </summary>
252+
member inline YieldFromFinal: computation: Async<unit> -> ResumableTSC<'T>

0 commit comments

Comments
 (0)