Skip to content

Instantly share code, notes, and snippets.

@jcouv
Last active November 14, 2024 20:14
Show Gist options
  • Save jcouv/aa5359db1b15522013cf19ef52dcc18f to your computer and use it in GitHub Desktop.
Save jcouv/aa5359db1b15522013cf19ef52dcc18f to your computer and use it in GitHub Desktop.

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();).

Lowering of await

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

Lowering of yield return

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 */

Lowering of yield break

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:

Example without try/finally (trivial disposal)

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;
        }
    }
}

Example try/finally (some disposal)

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
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment