From 04a73dda645391c995310e3a4456a7ac54b7956d Mon Sep 17 00:00:00 2001 From: Repo Assist Date: Sun, 8 Mar 2026 00:45:20 +0000 Subject: [PATCH 1/2] 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) and YieldFromFinal(Async) 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> --- release-notes.txt | 1 + src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs | 47 +++++++++++++++++++ src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi | 41 ++++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/release-notes.txt b/release-notes.txt index 43967b3a..30baca7c 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -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 diff --git a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs index 6ac6d0e7..ee940b88 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs @@ -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, and other unit-returning task-likes. + // NOT handled: Task<'T> (see HighPriority below for that). + [] + 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) + [] module MediumPriority = @@ -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 yield! appears in a tail-call position within a + /// taskSeq. Currently behaves identically to YieldFrom; the method exists + /// so F# 10+ can call it for tail-positioned yield! 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 yield! appears in a tail-call position over a + /// synchronous sequence. Behaves identically to YieldFrom. + member inline this.YieldFromFinal(source: seq<'T>) : ResumableTSC<'T> = this.YieldFrom(source) + [] module HighPriority = type TaskSeqBuilder with @@ -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 needs its own overload here (at HighPriority) for the same reason Bind does: + // TaskAwaiter.GetResult() -> unit differs from TaskAwaiter.GetResult() -> void. + member inline this.YieldFromFinal(task: Task) : ResumableTSC<'T> = this.Bind(task, (fun () -> this.Zero())) + + member inline this.YieldFromFinal(computation: Async) : ResumableTSC<'T> = this.Bind(computation, (fun () -> this.Zero())) + [] module TaskSeqBuilder = /// Builds an asynchronous task sequence based on IAsyncEnumerable<'T> using computation expression syntax. diff --git a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi index e0607046..11776cbe 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi @@ -180,6 +180,20 @@ module LowPriority = and ^Awaiter: (member get_IsCompleted: unit -> bool) and ^Awaiter: (member GetResult: unit -> 'T) + /// + /// Called by the F# compiler when do! with a unit-returning task-like appears in a + /// tail-call position. Handles non-generic Task, non-generic ValueTask, + /// ValueTask<unit>, and other unit-returning task-likes. Falls back to + /// Bind + Zero, signalling sequence end after awaiting the task. + /// + [] + 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) + /// /// Contains low priority extension methods for the main builder class for the computation expression. /// The , and modules are not meant to be @@ -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> + /// + /// Called by the F# compiler when yield! appears in a tail-call position within a + /// taskSeq computation expression. Currently behaves identically to YieldFrom; + /// the method exists so F# 10+ can recognise and call it for tail-positioned yield! + /// expressions without a compilation error. + /// + member inline YieldFromFinal: source: TaskSeq<'T> -> ResumableTSC<'T> + + /// + /// Called by the F# compiler when yield! appears in a tail-call position within a + /// taskSeq computation expression over a synchronous sequence. Behaves identically + /// to YieldFrom. + /// + member inline YieldFromFinal: source: seq<'T> -> ResumableTSC<'T> + /// /// Contains low priority extension methods for the main builder class for the computation expression. /// The , and modules are not meant to be @@ -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> + + /// + /// Called by the F# compiler when do! with a Task<unit> appears in a + /// tail-call position. Signals sequence end after awaiting the task. + /// + member inline YieldFromFinal: task: Task -> ResumableTSC<'T> + + /// + /// Called by the F# compiler when do! with an Async<unit> appears in a + /// tail-call position. Signals sequence end after the async computation completes. + /// + member inline YieldFromFinal: computation: Async -> ResumableTSC<'T> From e6a75ed8bb8fab962ae518fd1aa5b6712e33e421 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 8 Mar 2026 00:48:00 +0000 Subject: [PATCH 2/2] ci: trigger checks