In src/Eff/Utils/ValueTaskPromise.cs, CaptureThreadContext assigns TaskScheduler.Default and then compares it against TaskScheduler.Default, so the guard is always false and the branch is unreachable:
// src/Eff/Utils/ValueTaskPromise.cs:126-133
else
{
TaskScheduler ts = TaskScheduler.Default; // should be TaskScheduler.Current
if (ts != TaskScheduler.Default)
{
destination.Scheduler = ts;
}
}
The equivalent code in ManualResetValueTaskSourceCore reads TaskScheduler.Current. The SynchronizationContext branch just above it is correct — only the scheduler fallback is affected.
Effect
A custom scheduler is silently dropped, so the continuation resumes on the thread pool instead of on it. Reachable whenever a handler awaits EffStateMachine.TaskAwaiter without ConfigureAwait(false) while running under a non-default scheduler (ConcurrentExclusiveSchedulerPair, a Dataflow block with a custom scheduler, etc.).
EffectHandler.Handle uses ConfigureAwait(false), so the built-in path is unaffected. Two of the samples' handlers do await it bare, though:
samples/Eff.Examples.NonDeterminism/NonDetEffectHandler.cs:73
samples/Eff.Examples.Continuation/ContinuationHandler.cs:77
Low severity: the bug errs towards not capturing, so it cannot deadlock. It just diverges from what a plain await would do.
Fix
- TaskScheduler ts = TaskScheduler.Default;
+ TaskScheduler ts = TaskScheduler.Current;
Verified against a fork: with TaskScheduler.Current, a bare await of TaskAwaiter.Value under a custom scheduler resumes on that scheduler; reverting the one word fails the test. An equivalent ConfigureAwait(false) await correctly keeps resuming on TaskScheduler.Default either way.
Introduced in 5cfb9d6, still present on main (1388385).
In
src/Eff/Utils/ValueTaskPromise.cs,CaptureThreadContextassignsTaskScheduler.Defaultand then compares it againstTaskScheduler.Default, so the guard is always false and the branch is unreachable:The equivalent code in
ManualResetValueTaskSourceCorereadsTaskScheduler.Current. TheSynchronizationContextbranch just above it is correct — only the scheduler fallback is affected.Effect
A custom scheduler is silently dropped, so the continuation resumes on the thread pool instead of on it. Reachable whenever a handler awaits
EffStateMachine.TaskAwaiterwithoutConfigureAwait(false)while running under a non-default scheduler (ConcurrentExclusiveSchedulerPair, a Dataflow block with a custom scheduler, etc.).EffectHandler.HandleusesConfigureAwait(false), so the built-in path is unaffected. Two of the samples' handlers do await it bare, though:samples/Eff.Examples.NonDeterminism/NonDetEffectHandler.cs:73samples/Eff.Examples.Continuation/ContinuationHandler.cs:77Low severity: the bug errs towards not capturing, so it cannot deadlock. It just diverges from what a plain
awaitwould do.Fix
Verified against a fork: with
TaskScheduler.Current, a bare await ofTaskAwaiter.Valueunder a custom scheduler resumes on that scheduler; reverting the one word fails the test. An equivalentConfigureAwait(false)await correctly keeps resuming onTaskScheduler.Defaulteither way.Introduced in 5cfb9d6, still present on
main(1388385).