Skip to content

[Repo Assist] Add YieldFromFinal overloads for F# 10 compatibility#310

Closed
github-actions[bot] wants to merge 2 commits intomainfrom
repo-assist/feature-62-yield-from-final-4e330425ac5ed39b
Closed

[Repo Assist] Add YieldFromFinal overloads for F# 10 compatibility#310
github-actions[bot] wants to merge 2 commits intomainfrom
repo-assist/feature-62-yield-from-final-4e330425ac5ed39b

Conversation

@github-actions
Copy link
Contributor

@github-actions github-actions bot commented Mar 8, 2026

🤖 This is an automated PR from Repo Assist.

Closes #62

Summary

The F# 10 compiler (dotnet/fsharp#18804) introduces YieldFromFinal as a new builder method. When yield! or do! appears in a tail-call position within a computation expression, the compiler emits a call to YieldFromFinal instead of YieldFrom. Without these overloads, taskSeq expressions would fail to compile under F# 10.

This PR adds YieldFromFinal overloads to the taskSeq builder mirroring the existing priority structure (LowPriority, MediumPriority, HighPriority).

Changes

TaskSeqBuilder.fs / .fsi

Priority Method Behaviour
MediumPriority YieldFromFinal(IAsyncEnumerable<'T>) Delegates to YieldFrom
MediumPriority YieldFromFinal(seq<'T>) Delegates to YieldFrom
HighPriority YieldFromFinal(Task(unit)) Bind(task, fun () -> Zero())
HighPriority YieldFromFinal(Async(unit)) Bind(computation, fun () -> Zero())
LowPriority YieldFromFinal(^TaskLike) Generic unit-returning awaitable (inline, [(NoEagerConstraintApplication)])

The HighPriority and LowPriority overloads are needed because the F# 10 compiler also calls YieldFromFinal for do! in tail position (e.g. the last expression do! someTask() in a taskSeq).

Why not the full tailall transfer?

The tailall transfer optimization — where YieldFromFinal permanently hands off all future MoveNextAsync calls to the inner enumerator — was explored and reverted. The problem is that F# 10 also calls YieldFromFinal for yield! inside loop body lambdas (e.g. for c in source do yield! f c), where yield! f c is tail-call within the loop body fun c -> .... Activating a tail transfer in that case would lose all but the last iteration's elements. There is no way at YieldFromFinal call time to distinguish a genuine final tail call from a within-loop tail call.

The tailall transfer optimization can be revisited in a future PR with a more targeted approach.

Test Status

✅ Build succeeded (dotnet build src/FSharp.Control.TaskSeq.sln -c Release)
✅ All tests passed: 3850 passed, 2 skipped, 0 failed (dotnet test Release)
✅ Formatting checked with dotnet fantomas .

Generated by Repo Assist for issue #62 ·

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/repo-assist.md@ec7d342403c9912c87320110f8822a8fbb817a0c

- 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>
@dsyme
Copy link
Contributor

dsyme commented Mar 8, 2026

Added the F# bug about YieldFromFinal here: dotnet/fsharp#19402

Closing as there's no point implementing this if it's not working properly

@dsyme dsyme closed this Mar 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consider tail recursion

1 participant