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.
Optimized runtime async reconstructs a stateful conditional callback for each fold iteration. Deduplicating
[1; 1; 2]returns4instead of3.Repro steps
Use the compiler and matching net10.0 FSharp.Core built from main
e61631709fadcb83a90d336852c650e8c86b0189. Save this asConditionalFold.fs. It uses the repository's unchanged runtimeTask builder, not raw intrinsics at the call site.From the built repository root:
Add
--define:LISTto 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 returns4.Actual behavior
Both optimized Array.fold and List.fold fail the program's assertion:
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 .NET11.0.0-rc.1.26420.103on macOS ARM64. Component tests asserting3fail for optimized Array and List; the unoptimized, hoisted and classic-task controls pass.