88using System . Runtime . CompilerServices ;
99using System . Text ;
1010using System . Threading ;
11+ using System . Threading . Tasks ;
1112
1213using IronPython . Runtime ;
1314using IronPython . Runtime . Operations ;
@@ -117,7 +118,25 @@ internal override int KwOnlyArgCount {
117118
118119 public Expression ReturnAnnotation { get ; internal set ; }
119120
121+ #if FEATURE_NET_ASYNC
122+ // Under .NET-async, async functions are compiled directly to a Task<object?> via the DLR's AsyncExpression
123+ // rather than reused through the generator state machine, so IsAsync does not imply generator-shaped emission.
124+ internal override bool IsGeneratorMethod => IsGenerator ;
125+
126+ // Async-generator (PEP 525) channels. The StrongBox *values* are per-async-generator instance,
127+ // allocated per stack frame at runtime in the function body.
128+ // Declared/assigned in the body, captured by the generator (its yields read them through Parent),
129+ // and handed to the PythonAsyncGenerator wrapper, which writes them before each resume:
130+ // AsyncSendSlot — the value of `x = yield z` (asend(v); None for __anext__/async for).
131+ // AsyncThrowSlot — an exception to rethrow at the yield resume point (athrow/aclose).
132+ private readonly MSAst . ParameterExpression _asyncSendSlot = MSAst . Expression . Variable ( typeof ( StrongBox < object > ) , "$asyncSend" ) ;
133+ private readonly MSAst . ParameterExpression _asyncThrowSlot = MSAst . Expression . Variable ( typeof ( StrongBox < Exception > ) , "$asyncThrow" ) ;
134+
135+ internal MSAst . ParameterExpression AsyncSendSlot => _asyncSendSlot ;
136+ internal MSAst . ParameterExpression AsyncThrowSlot => _asyncThrowSlot ;
137+ #else
120138 internal override bool IsGeneratorMethod => IsGenerator || IsAsync ;
139+ #endif
121140
122141 /// <summary>
123142 /// The function is a generator
@@ -182,9 +201,15 @@ internal override FunctionAttributes Flags {
182201 fa |= FunctionAttributes . ContainsTryFinally ;
183202 }
184203
204+ #if FEATURE_NET_ASYNC
205+ if ( IsGenerator ) {
206+ fa |= FunctionAttributes . Generator ;
207+ }
208+ #else
185209 if ( IsGenerator || IsAsync ) {
186210 fa |= FunctionAttributes . Generator ;
187211 }
212+ #endif
188213
189214 if ( IsAsync ) {
190215 fa |= FunctionAttributes . Coroutine ;
@@ -357,7 +382,13 @@ internal MSAst.Expression MakeFunctionExpression() {
357382 annotations
358383 )
359384 ) ,
385+ #if FEATURE_NET_ASYNC
386+ // Async generators are lowered via AsyncEnumerableExpression in the body,
387+ // so they must not be wrapped as a PythonGenerator here — only plain (non-async) generators are.
388+ ( IsGenerator && ! IsAsync ) ?
389+ #else
360390 ( IsGenerator || IsAsync ) ?
391+ #endif
361392 ( MSAst . Expression ) new PythonGeneratorExpression ( code , GlobalParent . PyContext . Options . CompilationThreshold , IsAsync ) :
362393 ( MSAst . Expression ) code
363394 ) ;
@@ -659,7 +690,13 @@ private LightLambdaExpression CreateFunctionLambda() {
659690 // For generators/coroutines, we need to do a check before the first statement for Generator.Throw() / Generator.Close().
660691 // The exception traceback needs to come from the generator's method body, and so we must do the check and throw
661692 // from inside the generator.
693+ #if FEATURE_NET_ASYNC
694+ // Async generators have no backing PythonGenerator (they lower to IAsyncEnumerable via AsyncEnumerableExpression),
695+ // so skip the $generator.CheckThrowable() prologue for them.
696+ if ( IsGenerator && ! IsAsync ) {
697+ #else
662698 if ( IsGenerator || IsAsync ) {
699+ #endif
663700 MSAst . Expression s1 = YieldExpression . CreateCheckThrowExpression ( SourceSpan . None ) ;
664701 statements . Add ( s1 ) ;
665702 }
@@ -681,6 +718,59 @@ private LightLambdaExpression CreateFunctionLambda() {
681718 body = Ast . Block ( body , AstUtils . Empty ( ) ) ;
682719 body = AddReturnTarget ( body ) ;
683720
721+ #if FEATURE_NET_ASYNC
722+ // Under .NET-async, an `async def` body returns a PythonCoroutine wrapping a Task<object?>.
723+ // We pre-allocate a CancellationTokenSource and a StrongBox<Exception?> here
724+ // so the same instances are shared with both AsyncExpression, which threads them into AsyncHelpers.DriveAsync
725+ // and PythonCoroutine, which uses them to implement coro.throw(exc) on a running coroutine:
726+ // write the exception to the box, cancel the CTS, and DriveAsync surfaces that exception in place of OperationCanceledException.
727+ if ( IsAsync ) {
728+ var cts = MSAst. Expression. Variable( typeof ( CancellationTokenSource ) , "$cts") ;
729+ var excBox = MSAst. Expression. Variable( typeof ( StrongBox < Exception > ) , "$cancelExc" ) ;
730+ var ctToken = MSAst. Expression. Property( cts , nameof ( CancellationTokenSource . Token ) ) ;
731+ if ( IsGenerator) {
732+ // Async generator: the body has both `await` and `yield`. Lower it to an
733+ // IAsyncEnumerable<object?> via AsyncEnumerableExpression, sharing the generator label so
734+ // the body's yields and the rewritten awaits land in one generator, then wrap it in a
735+ // PythonAsyncGenerator. The send/throw slots are per-generator StrongBoxes captured by the
736+ // generator (the body's yields read them) AND handed to the wrapper, which writes them
737+ // before each resume — see AsyncSendSlot / AsyncThrowSlot.
738+ var sendSlot = AsyncSendSlot;
739+ var throwSlot = AsyncThrowSlot ;
740+ body = MSAst . Expression . Block (
741+ [ cts , excBox , sendSlot , throwSlot ] ,
742+ MSAst . Expression . Assign ( cts , MSAst . Expression . New ( typeof ( CancellationTokenSource ) ) ) ,
743+ MSAst . Expression . Assign ( excBox , MSAst . Expression . New ( typeof ( StrongBox < Exception > ) ) ) ,
744+ MSAst . Expression . Assign ( sendSlot , MSAst . Expression . New ( typeof ( StrongBox < object > ) ) ) ,
745+ MSAst . Expression . Assign ( throwSlot , MSAst . Expression . New ( typeof ( StrongBox < Exception > ) ) ) ,
746+ Ast . Call (
747+ AstMethods . MakeAsyncGenerator ,
748+ _functionParam ,
749+ AstUtils . AsyncEnumerable ( Name , body , GeneratorLabel , ctToken , excBox ) ,
750+ sendSlot ,
751+ throwSlot ,
752+ cts ) ) ;
753+ } else {
754+ // Plain async def: the body returns a PythonCoroutine wrapping a Task<object?>.
755+ // Lazy start: hand MakeAsyncCoroutine a thunk (Func<Task<object?>>) instead of an already-running Task,
756+ // so the body doesn't execute until the coroutine is first driven (send/AsTask).
757+ // This makes calling an async def side-effect-free (PEP 492) and lets the body's first await capture the driver's SynchronizationContext
758+ // rather than whatever context happened to be current at construction.
759+ body = MSAst. Expression . Block (
760+ [ cts , excBox ] ,
761+ MSAst . Expression . Assign ( cts , MSAst . Expression . New ( typeof ( CancellationTokenSource ) ) ) ,
762+ MSAst . Expression . Assign ( excBox , MSAst . Expression . New ( typeof ( StrongBox < Exception > ) ) ) ,
763+ Ast . Call (
764+ AstMethods . MakeAsyncCoroutine ,
765+ _functionParam ,
766+ MSAst . Expression . Lambda < Func < Task < object > > > (
767+ AstUtils . Async ( Name , body , ctToken , excBox ) ) ,
768+ cts ,
769+ excBox ) ) ;
770+ }
771+ }
772+ #endif
773+
684774 MSAst. Expression bodyStmt = body;
685775 if ( localContext != null ) {
686776 var createLocal = CreateLocalContext( _parentContext) ;
0 commit comments