Skip to content

Preserve stateful conditional callbacks in runtime async #20577

Description

@T-Gro

Optimized runtime async reconstructs a stateful conditional callback for each fold iteration. Deduplicating [1; 1; 2] returns 4 instead of 3.

Repro steps

Use the compiler and matching net10.0 FSharp.Core built from main e61631709fadcb83a90d336852c650e8c86b0189. Save this as ConditionalFold.fs. It uses the repository's unchanged runtimeTask builder, not raw intrinsics at the call site.

module ConditionalFold

open System.Collections.Generic
open System.Threading.Tasks
open RuntimeTaskBuilder.RuntimeTask

let run deduplicate (ready: Task<int>) =
#if CLASSIC
    task {
#else
    runtimeTask {
#endif
        let! initial = ready
#if HOIST
        let folder =
            if deduplicate then
                let seen = HashSet<int>()
                fun total item -> if seen.Add item then total + item else total
            else
                fun total item -> total + item
#endif
        return
#if LIST
            List.fold
#else
            Array.fold
#endif
#if HOIST
                folder
#else
                (if deduplicate then
                     let seen = HashSet<int>()
                     fun total item -> if seen.Add item then total + item else total
                 else
                     fun total item -> total + item)
#endif
                initial
#if LIST
                [ 1; 1; 2 ]
#else
                [| 1; 1; 2 |]
#endif
    }

[<EntryPoint>]
let main _ =
    for deduplicate in [ true; false ] do
        let gate = TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously)
        let work = run deduplicate gate.Task
        if work.IsCompleted then failwith "Expected suspension"
        gate.SetResult 0
        let expected = if deduplicate then 3 else 4
        let actual = work.GetAwaiter().GetResult()
        if actual <> expected then
            failwith $"deduplicate={deduplicate}: expected {expected}, actual {actual}"
    0

From the built repository root:

fsc=artifacts/bin/fsc/Release/net11.0/fsc.dll
core=artifacts/bin/FSharp.Core/Release/net10.0/FSharp.Core.dll
out=artifacts/repro/runtime-async-fold
mkdir -p "$out"
dotnet "$fsc" --targetprofile:netcore --langversion:preview --optimize+ \
  -r:"$core" --target:library -o:"$out/RuntimeTaskLibrary.dll" \
  tests/FSharp.Compiler.ComponentTests/Language/RuntimeAsync/RuntimeTaskBuilder.fs
dotnet "$fsc" --targetprofile:netcore --langversion:preview --optimize+ \
  -r:"$core" -r:"$out/RuntimeTaskLibrary.dll" --target:exe \
  -o:"$out/Probe.dll" ConditionalFold.fs
cp "$core" "$out/FSharp.Core.dll"
dotnet exec --runtimeconfig artifacts/bin/fsc/Release/net11.0/fsc.runtimeconfig.json "$out/Probe.dll"

Add --define:LIST to the consumer compilation for the List.fold variant.

Expected behavior

The callback is constructed once. Its captured set is shared across iterations, so deduplication returns 3; the non-deduplicating branch returns 4.

Actual behavior

Both optimized Array.fold and List.fold fail the program's assertion:

deduplicate=True: expected 3, actual 4

The set constructor is emitted inside the loop. The failure is a wrong value, not an invalid-IL crash.

Known workarounds

Let-bind the callback before the fold (--define:HOIST), inside or outside the CE. The unoptimized consumer and the classic-task control (--define:CLASSIC) also return the expected values.

Related information

Reproduced after #20235 on main e6163170, with a freshly built compiler/Core and .NET 11.0.0-rc.1.26420.103 on macOS ARM64. Component tests asserting 3 fail for optimized Array and List; the unoptimized, hoisted and classic-task controls pass.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions