I've been thinking about how to implement async-iterators on top of runtime-async.
An async-iterator is currently implemented as a core MoveNext()
method (containing the user code) with MoveNextAsync()
and DiposeAsync()
entry points that call it.
With runtime-async, we can simplify this down to a MoveNextAsync()
method (containing the user code) with a secondary DisposeAsync()
entry point (essentially does disposeMode = true; await MoveNextAsync();
).
await
is left as an await2
(ie. no longer introduces states into the compiler-generated state machine):
// an await in user code becomes:
await2 ... // ie. compiler just uses special calling convention, but leaves this up to runtime to handle
yield return
is lowered as a suspension of the state machine (essentially __current = ...; return true;
with a way of resuming execution after the return
):
// a `yield return 42;` in user code becomes:
__state = stateForThisYieldReturn;
__current = 42;
return true; // in an ValueTask<bool>-returning async2 method, we need only return a boolean
labelForThisYieldReturn:
__state = RunningState;
if (__disposeMode) /* jump to enclosing finally or exit */
yield break
is lowered as continuing execution in disposal mode:
// a `yield break;` in user code becomes:
disposeMode = true;
/* jump to enclosing finally or exit */
Note: this design uses a single method for both normal and disposal execution (like async-iterators currently do), but it would also be closer to fit this into the existing iterator rewriter design (normal execution is MoveNext and disposal is Dispose, shared code is extracted to helper methods).
Below are two examples to illustrate the building blocks in context:
class ScenarioWithoutTryFinally
{
// Original C# code
public static async IAsyncEnumerable<int> M()
{
Console.Write(1);
await Task.Delay(1);
Console.Write(2);
yield return 42;
Console.Write(3);
}
// Generated IAsyncEnumerable implementation
// Note: each yield return gets assigned a state (< -3)
private class Unspeakable : IAsyncEnumerable<int>, IAsyncEnumerator<int>, IAsyncDisposable
{
public int __state;
int __current;
int __initialThreadId;
bool __disposeMode;
//CancellationTokenSource __combinedTokens; // UNDONE
const int NotStartedStateMachine = -3;
const int FinishedState = -2;
const int RunningState = 0;
public Unspeakable(int state)
{
__state = state;
__initialThreadId = Environment.CurrentManagedThreadId;
}
IAsyncEnumerator<int> IAsyncEnumerable<int>.GetAsyncEnumerator(CancellationToken token)
{
Unspeakable result;
if (__state == FinishedState && __initialThreadId == Environment.CurrentManagedThreadId)
{
__state = NotStartedStateMachine;
__disposeMode = false;
result = this;
}
else
{
result = new Unspeakable(NotStartedStateMachine);
}
return result;
}
int IAsyncEnumerator<int>.Current => __current;
private const int stateForYieldReturn42 = -4;
public async2 ValueTask<bool> MoveNextAsync() // This is "async2" so the generated code need only return a boolean
{
// dispatch block
#region dispatch block
switch (__state)
{
case stateForYieldReturn42:
goto labelYieldReturn42;
}
#endregion
// entry
if (__disposeMode) goto setResultFalseLabel;
__state = RunningState;
// start of user code
Console.Write(1);
// await
__current = default; // TODO2 we may need to tweak where this goes
await2 Task.Delay(1); // This is "async2 so the generated call is using the special calling convention
// end of await
Console.Write(2);
// yield return 42;
__state = stateForYieldReturn42;
__current = 42;
return true;
labelYieldReturn42:
__state = RunningState;
if (__disposeMode) goto setResultFalseLabel;
// end of yield return 42;
Console.Write(3);
setResultFalseLabel:
__state = FinishedState;
__current = default;
return false;
}
async2 ValueTask IAsyncDisposable.DisposeAsync() // This is "async2" so the generated code need only return
{
if (__state >= NotStartedStateMachine)
{
// running
throw new NotSupportedException();
}
if (__state == FinishedState)
{
// already disposed
return;
}
__disposeMode = true;
bool finished = !await2 MoveNextAsync(); // This is "async2 so the generated call is using the special calling convention and we get an unwrapped boolean value back
Debug.Assert(finished);
return;
}
}
}
class ScenarioWithTryFinally
{
// Original C# code
public static async IAsyncEnumerable<int> M()
{
Console.Write(0);
try
{
Console.Write(1);
await Task.Delay(1);
Console.Write(2);
yield return 42;
Console.Write(3);
}
finally
{
Console.Write(4);
}
Console.Write(5);
}
// Generated IAsyncEnumerable implementation
// Note: each yield return gets assigned a state (< -3)
private class Unspeakable : IAsyncEnumerable<int>, IAsyncEnumerator<int>, IAsyncDisposable
{
public int __state;
int __current;
int __initialThreadId;
bool __disposeMode;
//CancellationTokenSource __combinedTokens; // TODO2
Exception? __exception;
const int NotStartedStateMachine = -3;
const int FinishedState = -2;
const int RunningState = 0;
IAsyncEnumerator<int> IAsyncEnumerable<int>.GetAsyncEnumerator(CancellationToken token) => throw null; // same as above
int IAsyncEnumerator<int>.Current => __current;
private const int stateForYieldReturn42 = -4;
public async2 ValueTask<bool> MoveNextAsync()
{
// dispatch block
#region dispatch block
switch (__state)
{
case stateForYieldReturn42:
goto tryDispatchLabel;
}
#endregion
// entry
if (__disposeMode) goto setResultFalseLabel;
__state = RunningState;
// start of user code
Console.Write(0);
tryDispatchLabel:
try
{
// dispatch block
#region dispatch block
switch (__state)
{
case stateForYieldReturn42:
goto labelYieldReturn42;
}
#endregion
Console.Write(1);
// await
__current = default;
await2 Task.Delay(1);
// end of await
Console.Write(2);
// yield return 42;
__state = stateForYieldReturn42;
__current = 42;
return true;
labelYieldReturn42:
__state = RunningState;
if (__disposeMode) goto disposeEnumeratorLabel;
// end of yield return 42;
Console.Write(3);
}
catch (Exception e)
{
__exception = e;
}
// extracted finally
disposeEnumeratorLabel:
object? exception = __exception;
if (exception is Exception temp)
{
// simplified
ExceptionDispatchInfo.Capture(temp).Throw();
}
// user code from finally
{
Console.Write(4);
}
// extract finally epilogue
if (__disposeMode) goto setResultFalseLabel;
Console.Write(5);
setResultFalseLabel:
__state = FinishedState;
__current = default;
return false;
}
ValueTask IAsyncDisposable.DisposeAsync() => throw null; // same as above
}
}