Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions release-notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Release notes:

0.5.0
- adds YieldFromFinal to the taskSeq builder for F# 10 compatibility (tail-positioned yield! and do!), #62
- update engineering to .NET 9/10
- adds TaskSeq.scan and TaskSeq.scanAsync, #289
- adds TaskSeq.pairwise, #289
Expand Down
47 changes: 47 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,35 @@ module LowPriority =
sm.Data.current <- ValueNone
false)

// YieldFromFinal for generic task-like in tail call position (handles do! in tail position).
// Handles: non-generic Task, non-generic ValueTask, ValueTask<unit>, and other unit-returning task-likes.
// NOT handled: Task<'T> (see HighPriority below for that).
[<NoEagerConstraintApplication>]
member inline _.YieldFromFinal< ^TaskLike, 'T, ^Awaiter
when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter)
and ^Awaiter :> ICriticalNotifyCompletion
and ^Awaiter: (member get_IsCompleted: unit -> bool)
and ^Awaiter: (member GetResult: unit -> unit)>
(task: ^TaskLike)
: ResumableTSC<'T> =

// Inline the await pattern to avoid constraint propagation issues with Bind.
ResumableTSC<'T>(fun sm ->
let mutable awaiter = (^TaskLike: (member GetAwaiter: unit -> ^Awaiter) (task))
let mutable __stack_fin = true

if not (^Awaiter: (member get_IsCompleted: unit -> bool) awaiter) then
let __stack_fin2 = ResumableCode.Yield().Invoke(&sm)
__stack_fin <- __stack_fin2

if __stack_fin then
(^Awaiter: (member GetResult: unit -> unit) awaiter)
true // zero: signal done, no elements
else
sm.Data.awaiter <- awaiter
sm.Data.current <- ValueNone
false)


[<AutoOpen>]
module MediumPriority =
Expand Down Expand Up @@ -607,6 +636,17 @@ module MediumPriority =

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

/// Called by the F# compiler when <c>yield!</c> appears in a tail-call position within a
/// <c>taskSeq</c>. Currently behaves identically to <c>YieldFrom</c>; the method exists
/// so F# 10+ can call it for tail-positioned <c>yield!</c> expressions. A zero-copy
/// tail-delegation optimisation requires additional state-machine driver support and is
/// left as future work.
member inline this.YieldFromFinal(source: IAsyncEnumerable<'T>) : ResumableTSC<'T> = this.YieldFrom(source)

/// Called by the F# compiler when <c>yield!</c> appears in a tail-call position over a
/// synchronous sequence. Behaves identically to <c>YieldFrom</c>.
member inline this.YieldFromFinal(source: seq<'T>) : ResumableTSC<'T> = this.YieldFrom(source)

[<AutoOpen>]
module HighPriority =
type TaskSeqBuilder with
Expand Down Expand Up @@ -681,6 +721,13 @@ module HighPriority =
sm.Data.current <- ValueNone
false)

// YieldFromFinal for Task<'T> and Async<'T> in tail call position (handles do! in tail position).
// Task<unit> needs its own overload here (at HighPriority) for the same reason Bind does:
// TaskAwaiter<unit>.GetResult() -> unit differs from TaskAwaiter.GetResult() -> void.
member inline this.YieldFromFinal(task: Task<unit>) : ResumableTSC<'T> = this.Bind(task, (fun () -> this.Zero()))

member inline this.YieldFromFinal(computation: Async<unit>) : ResumableTSC<'T> = this.Bind(computation, (fun () -> this.Zero()))

[<AutoOpen>]
module TaskSeqBuilder =
/// Builds an asynchronous task sequence based on IAsyncEnumerable<'T> using computation expression syntax.
Expand Down
41 changes: 41 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,20 @@ module LowPriority =
and ^Awaiter: (member get_IsCompleted: unit -> bool)
and ^Awaiter: (member GetResult: unit -> 'T)

/// <summary>
/// Called by the F# compiler when <c>do!</c> with a unit-returning task-like appears in a
/// tail-call position. Handles non-generic <c>Task</c>, non-generic <c>ValueTask</c>,
/// <c>ValueTask&lt;unit&gt;</c>, and other unit-returning task-likes. Falls back to
/// <c>Bind</c> + <c>Zero</c>, signalling sequence end after awaiting the task.
/// </summary>
[<NoEagerConstraintApplication>]
member inline YieldFromFinal< ^TaskLike, 'T, ^Awaiter> :
task: ^TaskLike -> ResumableTSC<'T>
when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter)
and ^Awaiter :> ICriticalNotifyCompletion
and ^Awaiter: (member get_IsCompleted: unit -> bool)
and ^Awaiter: (member GetResult: unit -> unit)

/// <summary>
/// Contains low priority extension methods for the main builder class for the <see cref="taskSeq" /> computation expression.
/// The <see cref="LowPriority" />, <see cref="MediumPriority" /> and <see cref="HighPriority" /> modules are not meant to be
Expand All @@ -198,6 +212,21 @@ module MediumPriority =
member inline For: source: #TaskSeq<'TElement> * body: ('TElement -> ResumableTSC<'T>) -> ResumableTSC<'T>
member inline YieldFrom: source: TaskSeq<'T> -> ResumableTSC<'T>

/// <summary>
/// Called by the F# compiler when <c>yield!</c> appears in a tail-call position within a
/// <c>taskSeq</c> computation expression. Currently behaves identically to <c>YieldFrom</c>;
/// the method exists so F# 10+ can recognise and call it for tail-positioned <c>yield!</c>
/// expressions without a compilation error.
/// </summary>
member inline YieldFromFinal: source: TaskSeq<'T> -> ResumableTSC<'T>

/// <summary>
/// Called by the F# compiler when <c>yield!</c> appears in a tail-call position within a
/// <c>taskSeq</c> computation expression over a synchronous sequence. Behaves identically
/// to <c>YieldFrom</c>.
/// </summary>
member inline YieldFromFinal: source: seq<'T> -> ResumableTSC<'T>

/// <summary>
/// Contains low priority extension methods for the main builder class for the <see cref="taskSeq" /> computation expression.
/// The <see cref="LowPriority" />, <see cref="MediumPriority" /> and <see cref="HighPriority" /> modules are not meant to be
Expand All @@ -209,3 +238,15 @@ module HighPriority =

member inline Bind: task: Task<'T> * continuation: ('T -> ResumableTSC<'U>) -> ResumableTSC<'U>
member inline Bind: computation: Async<'T> * continuation: ('T -> ResumableTSC<'U>) -> ResumableTSC<'U>

/// <summary>
/// Called by the F# compiler when <c>do!</c> with a <c>Task&lt;unit&gt;</c> appears in a
/// tail-call position. Signals sequence end after awaiting the task.
/// </summary>
member inline YieldFromFinal: task: Task<unit> -> ResumableTSC<'T>

/// <summary>
/// Called by the F# compiler when <c>do!</c> with an <c>Async&lt;unit&gt;</c> appears in a
/// tail-call position. Signals sequence end after the async computation completes.
/// </summary>
member inline YieldFromFinal: computation: Async<unit> -> ResumableTSC<'T>