StateMachineEff.ReflectionHelpers locates the state machine's awaiter field by type alone:
// src/Eff/Builders/StateMachineEff.cs
private static readonly FieldInfo? s_smObject = typeof(TStateMachine)
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)
.Where(f => f.FieldType == typeof(object))
.FirstOrDefault();
Roslyn types the awaiter field as object when the awaiter is a reference type, reusing one slot for all such awaiters, so normally <>u__1 is the only candidate and this works. But a hoisted local declared object is also an object-typed field, and Roslyn emits hoisted locals ahead of its own bookkeeping fields — so FirstOrDefault() binds to the user's local.
Fields of a real state machine, identical in Debug and Release:
priv Object <marker>5__1 <- s_smObject binds here
priv Object <>u__1 <- the awaiter field it wanted
UnsafeSetAwaiter then writes the awaiter over the local and never updates the awaiter field, so the clone resumes against the original, already-completed awaiter.
Repro
static async Eff<int> Test()
{
object marker = "ab"; // `object`, live across the await
int x = await NonDetEffect.Choose(1, 2, 3);
return x + marker.ToString()!.Length;
}
await NonDetEffectHandler.Run(Test()); // expected [3, 4, 5]
InvalidOperationException: Awaiter of type NonDetEffect`1 has not been completed.
Declaring marker as string instead gives the correct [3, 4, 5]. Reproduced in both Debug and Release, so it is not specific to class- or struct-shaped state machines.
Impact
Any handler that clones continuations — non-determinism and call/cc, including Eff.Examples.NonDeterminism and Eff.Examples.Continuation — breaks for any eff method holding a local of type exactly object across an await: an object local, a dynamic local, or the iteration variable of a non-generic foreach. EffectHandler never clones and is unaffected.
The exception reads as handler misuse rather than a library defect, which makes it easy to misattribute.
Fix
Match on Roslyn's field naming as well as the type:
private static readonly FieldInfo? s_smObject = typeof(TStateMachine)
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)
+ .Where(f => f.Name.StartsWith("<>u__", StringComparison.Ordinal))
.Where(f => f.FieldType == typeof(object))
.FirstOrDefault();
The adjacent s_smBuilder lookup has the same type-only ambiguity — reachable only via an eff method taking an EffMethodBuilder<T> parameter, so theoretical, but worth the same treatment (<>t__).
Verified on a fork: the repro passes in both configurations and the full suite stays green. Removing the name filter again fails exactly the new regression test and nothing else. The names are compiler implementation details with no documented contract — the same exposure Utils/TraceHelpers.cs already relies on.
StateMachineEff.ReflectionHelperslocates the state machine's awaiter field by type alone:Roslyn types the awaiter field as
objectwhen the awaiter is a reference type, reusing one slot for all such awaiters, so normally<>u__1is the only candidate and this works. But a hoisted local declaredobjectis also anobject-typed field, and Roslyn emits hoisted locals ahead of its own bookkeeping fields — soFirstOrDefault()binds to the user's local.Fields of a real state machine, identical in Debug and Release:
UnsafeSetAwaiterthen writes the awaiter over the local and never updates the awaiter field, so the clone resumes against the original, already-completed awaiter.Repro
Declaring
markerasstringinstead gives the correct[3, 4, 5]. Reproduced in both Debug and Release, so it is not specific to class- or struct-shaped state machines.Impact
Any handler that clones continuations — non-determinism and call/cc, including
Eff.Examples.NonDeterminismandEff.Examples.Continuation— breaks for any eff method holding a local of type exactlyobjectacross an await: anobjectlocal, adynamiclocal, or the iteration variable of a non-genericforeach.EffectHandlernever clones and is unaffected.The exception reads as handler misuse rather than a library defect, which makes it easy to misattribute.
Fix
Match on Roslyn's field naming as well as the type:
private static readonly FieldInfo? s_smObject = typeof(TStateMachine) .GetFields(BindingFlags.Instance | BindingFlags.NonPublic) + .Where(f => f.Name.StartsWith("<>u__", StringComparison.Ordinal)) .Where(f => f.FieldType == typeof(object)) .FirstOrDefault();The adjacent
s_smBuilderlookup has the same type-only ambiguity — reachable only via an eff method taking anEffMethodBuilder<T>parameter, so theoretical, but worth the same treatment (<>t__).Verified on a fork: the repro passes in both configurations and the full suite stays green. Removing the name filter again fails exactly the new regression test and nothing else. The names are compiler implementation details with no documented contract — the same exposure
Utils/TraceHelpers.csalready relies on.