Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eng/net10.0.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<Features>$(Features);FEATURE_METADATA_READER</Features>
<Features>$(Features);FEATURE_MMAP</Features>
<Features>$(Features);FEATURE_NATIVE</Features>
<Features>$(Features);FEATURE_NET_ASYNC</Features>
<Features>$(Features);FEATURE_OSPLATFORMATTRIBUTE</Features>
<Features>$(Features);FEATURE_PIPES</Features>
<Features>$(Features);FEATURE_PROCESS</Features>
Expand Down
1 change: 1 addition & 0 deletions eng/net8.0.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<Features>$(Features);FEATURE_METADATA_READER</Features>
<Features>$(Features);FEATURE_MMAP</Features>
<Features>$(Features);FEATURE_NATIVE</Features>
<Features>$(Features);FEATURE_NET_ASYNC</Features>
<Features>$(Features);FEATURE_OSPLATFORMATTRIBUTE</Features>
<Features>$(Features);FEATURE_PIPES</Features>
<Features>$(Features);FEATURE_PROCESS</Features>
Expand Down
1 change: 1 addition & 0 deletions eng/net9.0.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<Features>$(Features);FEATURE_METADATA_READER</Features>
<Features>$(Features);FEATURE_MMAP</Features>
<Features>$(Features);FEATURE_NATIVE</Features>
<Features>$(Features);FEATURE_NET_ASYNC</Features>
<Features>$(Features);FEATURE_OSPLATFORMATTRIBUTE</Features>
<Features>$(Features);FEATURE_PIPES</Features>
<Features>$(Features);FEATURE_PROCESS</Features>
Expand Down
5 changes: 5 additions & 0 deletions src/core/IronPython/Compiler/Ast/AstMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ internal static class AstMethods {
public static readonly MethodInfo FormatString = GetMethod((Func<CodeContext, string, object, string>)PythonOps.FormatString);
public static readonly MethodInfo GeneratorCheckThrowableAndReturnSendValue = GetMethod((Func<object, object>)PythonOps.GeneratorCheckThrowableAndReturnSendValue);
public static readonly MethodInfo MakeCoroutine = GetMethod((Func<PythonFunction, MutableTuple, object, PythonCoroutine>)PythonOps.MakeCoroutine);
#if FEATURE_NET_ASYNC
public static readonly MethodInfo AsTaskForAwait = GetMethod((Func<object, System.Threading.Tasks.Task<object>>)PythonOps.AsTaskForAwait);
public static readonly MethodInfo MakeAsyncCoroutine = GetMethod((Func<PythonFunction, Func<System.Threading.Tasks.Task<object>>, System.Threading.CancellationTokenSource, System.Runtime.CompilerServices.StrongBox<System.Exception>, PythonCoroutine>)PythonOps.MakeAsyncCoroutine);
public static readonly MethodInfo MakeAsyncGenerator = GetMethod((Func<PythonFunction, System.Collections.Generic.IAsyncEnumerable<object>, System.Runtime.CompilerServices.StrongBox<object>, System.Runtime.CompilerServices.StrongBox<System.Exception>, System.Threading.CancellationTokenSource, PythonAsyncGenerator>)PythonOps.MakeAsyncGenerator);
#endif

// builtins
public static readonly MethodInfo Format = GetMethod((Func<CodeContext, object, string, string>)PythonOps.Format);
Expand Down
29 changes: 28 additions & 1 deletion src/core/IronPython/Compiler/Ast/AwaitExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,41 @@

using MSAst = System.Linq.Expressions;

using IronPython.Runtime.Operations;

using AstUtils = Microsoft.Scripting.Ast.Utils;

namespace IronPython.Compiler.Ast {
using Ast = MSAst.Expression;

/// <summary>
/// Represents an await expression. Implemented as yield from expr.__await__().
/// Represents <c>await expr</c>. Under <c>FEATURE_NET_ASYNC</c> this compiles directly to a DLR async suspension point.
/// Otherwise it is desugared into <c>yield from expr.__await__()</c> against the enclosing generator-shaped coroutine state machine.
/// </summary>
public class AwaitExpression : Expression {
#if FEATURE_NET_ASYNC
public AwaitExpression(Expression expression) {
Expression = expression;
}

public Expression Expression { get; }

public override MSAst.Expression Reduce() {
// await x -> AsyncHelpers-driven suspension on the Task produced by
// PythonOps.AsTaskForAwait(x).
return AstUtils.Await(
Ast.Call(
AstMethods.AsTaskForAwait,
AstUtils.Convert(Expression, typeof(object))));
}

public override void Walk(PythonWalker walker) {
if (walker.Walk(this)) {
Expression?.Walk(walker);
}
walker.PostWalk(this);
}
#else
private readonly Statement _statement;
private readonly NameExpression _result;

Expand Down Expand Up @@ -60,6 +86,7 @@ public override void Walk(PythonWalker walker) {
}
walker.PostWalk(this);
}
#endif

public override string NodeName => "await expression";
}
Expand Down
90 changes: 90 additions & 0 deletions src/core/IronPython/Compiler/Ast/FunctionDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using IronPython.Runtime;
using IronPython.Runtime.Operations;
Expand Down Expand Up @@ -117,7 +118,25 @@ internal override int KwOnlyArgCount {

public Expression ReturnAnnotation { get; internal set; }

#if FEATURE_NET_ASYNC
// Under .NET-async, async functions are compiled directly to a Task<object?> via the DLR's AsyncExpression
// rather than reused through the generator state machine, so IsAsync does not imply generator-shaped emission.
internal override bool IsGeneratorMethod => IsGenerator;

// Async-generator (PEP 525) channels. The StrongBox *values* are per-async-generator instance,
// allocated per stack frame at runtime in the function body.
// Declared/assigned in the body, captured by the generator (its yields read them through Parent),
// and handed to the PythonAsyncGenerator wrapper, which writes them before each resume:
// AsyncSendSlot — the value of `x = yield z` (asend(v); None for __anext__/async for).
// AsyncThrowSlot — an exception to rethrow at the yield resume point (athrow/aclose).
private readonly MSAst.ParameterExpression _asyncSendSlot = MSAst.Expression.Variable(typeof(StrongBox<object>), "$asyncSend");
private readonly MSAst.ParameterExpression _asyncThrowSlot = MSAst.Expression.Variable(typeof(StrongBox<Exception>), "$asyncThrow");

internal MSAst.ParameterExpression AsyncSendSlot => _asyncSendSlot;
internal MSAst.ParameterExpression AsyncThrowSlot => _asyncThrowSlot;
#else
internal override bool IsGeneratorMethod => IsGenerator || IsAsync;
#endif

/// <summary>
/// The function is a generator
Expand Down Expand Up @@ -182,9 +201,15 @@ internal override FunctionAttributes Flags {
fa |= FunctionAttributes.ContainsTryFinally;
}

#if FEATURE_NET_ASYNC
if (IsGenerator) {
fa |= FunctionAttributes.Generator;
}
#else
if (IsGenerator || IsAsync) {
fa |= FunctionAttributes.Generator;
}
#endif

if (IsAsync) {
fa |= FunctionAttributes.Coroutine;
Expand Down Expand Up @@ -357,7 +382,13 @@ internal MSAst.Expression MakeFunctionExpression() {
annotations
)
),
#if FEATURE_NET_ASYNC
// Async generators are lowered via AsyncEnumerableExpression in the body,
// so they must not be wrapped as a PythonGenerator here — only plain (non-async) generators are.
(IsGenerator && !IsAsync) ?
#else
(IsGenerator || IsAsync) ?
#endif
(MSAst.Expression)new PythonGeneratorExpression(code, GlobalParent.PyContext.Options.CompilationThreshold, IsAsync) :
(MSAst.Expression)code
);
Expand Down Expand Up @@ -659,7 +690,13 @@ private LightLambdaExpression CreateFunctionLambda() {
// For generators/coroutines, we need to do a check before the first statement for Generator.Throw() / Generator.Close().
// The exception traceback needs to come from the generator's method body, and so we must do the check and throw
// from inside the generator.
#if FEATURE_NET_ASYNC
// Async generators have no backing PythonGenerator (they lower to IAsyncEnumerable via AsyncEnumerableExpression),
// so skip the $generator.CheckThrowable() prologue for them.
if (IsGenerator && !IsAsync) {
#else
if (IsGenerator || IsAsync) {
#endif
MSAst.Expression s1 = YieldExpression.CreateCheckThrowExpression(SourceSpan.None);
statements.Add(s1);
}
Expand All @@ -681,6 +718,59 @@ private LightLambdaExpression CreateFunctionLambda() {
body = Ast.Block(body, AstUtils.Empty());
body = AddReturnTarget(body);

#if FEATURE_NET_ASYNC
// Under .NET-async, an `async def` body returns a PythonCoroutine wrapping a Task<object?>.
// We pre-allocate a CancellationTokenSource and a StrongBox<Exception?> here
// so the same instances are shared with both AsyncExpression, which threads them into AsyncHelpers.DriveAsync
// and PythonCoroutine, which uses them to implement coro.throw(exc) on a running coroutine:
// write the exception to the box, cancel the CTS, and DriveAsync surfaces that exception in place of OperationCanceledException.
if (IsAsync) {
var cts = MSAst.Expression.Variable(typeof(CancellationTokenSource), "$cts");
var excBox = MSAst.Expression.Variable(typeof(StrongBox<Exception>), "$cancelExc");
var ctToken = MSAst.Expression.Property(cts, nameof(CancellationTokenSource.Token));
if (IsGenerator) {
// Async generator: the body has both `await` and `yield`. Lower it to an
// IAsyncEnumerable<object?> via AsyncEnumerableExpression, sharing the generator label so
// the body's yields and the rewritten awaits land in one generator, then wrap it in a
// PythonAsyncGenerator. The send/throw slots are per-generator StrongBoxes captured by the
// generator (the body's yields read them) AND handed to the wrapper, which writes them
// before each resume — see AsyncSendSlot / AsyncThrowSlot.
var sendSlot = AsyncSendSlot;
var throwSlot = AsyncThrowSlot;
body = MSAst.Expression.Block(
[cts, excBox, sendSlot, throwSlot],
MSAst.Expression.Assign(cts, MSAst.Expression.New(typeof(CancellationTokenSource))),
MSAst.Expression.Assign(excBox, MSAst.Expression.New(typeof(StrongBox<Exception>))),
MSAst.Expression.Assign(sendSlot, MSAst.Expression.New(typeof(StrongBox<object>))),
MSAst.Expression.Assign(throwSlot, MSAst.Expression.New(typeof(StrongBox<Exception>))),
Ast.Call(
AstMethods.MakeAsyncGenerator,
_functionParam,
AstUtils.AsyncEnumerable(Name, body, GeneratorLabel, ctToken, excBox),
sendSlot,
throwSlot,
cts));
} else {
// Plain async def: the body returns a PythonCoroutine wrapping a Task<object?>.
// Lazy start: hand MakeAsyncCoroutine a thunk (Func<Task<object?>>) instead of an already-running Task,
// so the body doesn't execute until the coroutine is first driven (send/AsTask).
// This makes calling an async def side-effect-free (PEP 492) and lets the body's first await capture the driver's SynchronizationContext
// rather than whatever context happened to be current at construction.
body = MSAst.Expression.Block(
[cts, excBox],
MSAst.Expression.Assign(cts, MSAst.Expression.New(typeof(CancellationTokenSource))),
MSAst.Expression.Assign(excBox, MSAst.Expression.New(typeof(StrongBox<Exception>))),
Ast.Call(
AstMethods.MakeAsyncCoroutine,
_functionParam,
MSAst.Expression.Lambda<Func<Task<object>>>(
AstUtils.Async(Name, body, ctToken, excBox)),
cts,
excBox));
}
}
#endif

MSAst.Expression bodyStmt = body;
if (localContext != null) {
var createLocal = CreateLocalContext(_parentContext);
Expand Down
12 changes: 12 additions & 0 deletions src/core/IronPython/Compiler/Ast/ReturnStatement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

using System.Diagnostics;

using MSAst = System.Linq.Expressions;

namespace IronPython.Compiler.Ast {
Expand All @@ -17,6 +19,16 @@ public ReturnStatement(Expression expression) {

public override MSAst.Expression Reduce() {
if (Parent.IsGeneratorMethod) {
#if FEATURE_NET_ASYNC
// An async generator (`async def` with `yield`) lowers through the DLR generator via AsyncEnumerableExpression,
// which doesn't understand IronPython's -2 "generator-return" marker.
// `return` there is always bare (`return value` is a SyntaxError in async generators)
// and simply ends the async iteration — map it to a YieldBreak without a value.
if (Parent is FunctionDefinition { IsAsync: true }) {
Debug.Assert(Expression == null, "async generators should not have a return value");
return GlobalParent.AddDebugInfo(AstUtils.YieldBreak(GeneratorLabel), Span);
}
#endif
// Reduce to a yield return with a marker of -2, this will be interpreted as a yield break with a return value
return GlobalParent.AddDebugInfo(AstUtils.YieldReturn(GeneratorLabel, TransformOrConstantNull(Expression, typeof(object)), -2), Span);
}
Expand Down
43 changes: 38 additions & 5 deletions src/core/IronPython/Compiler/Ast/YieldExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
using MSAst = System.Linq.Expressions;

using System;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Diagnostics;
using Microsoft.Scripting;
using Microsoft.Scripting.Runtime;
Expand All @@ -19,6 +21,13 @@ namespace IronPython.Compiler.Ast {
// x = yield z
// The return value (x) is provided by calling Generator.Send()
public class YieldExpression : Expression {
#if FEATURE_NET_ASYNC
private static readonly System.Reflection.MethodInfo s_captureMethod
= typeof(ExceptionDispatchInfo).GetMethod(nameof(ExceptionDispatchInfo.Capture))!;
private static readonly System.Reflection.MethodInfo s_throwMethod
= typeof(ExceptionDispatchInfo).GetMethod(nameof(ExceptionDispatchInfo.Throw), Type.EmptyTypes)!;
#endif

public YieldExpression(Expression? expression) {
Expression = expression;
}
Expand All @@ -43,16 +52,40 @@ internal static MSAst.Expression CreateCheckThrowExpression(SourceSpan span) {
}

public override MSAst.Expression Reduce() {
MSAst.Expression yieldValue = Expression == null ? AstUtils.Constant(null) : AstUtils.Convert(Expression, typeof(object));

#if FEATURE_NET_ASYNC
// An async generator (`async def` with `yield`) is lowered via AsyncEnumerableExpression
// and has no backing PythonGenerator, so there is no `$generator` to call CheckThrowable() on.
// Instead the resume reads two per-generator cells that PythonAsyncGenerator writes before advancing:
// AsyncThrowSlot — if set (athrow/aclose), rethrow it here (preserving stack);
// cleared first so a body that catches it and yields again doesn't re-throw on the next resume.
// AsyncSendSlot — the value of the yield expression: the asend(v) value, or None.
if (Parent is FunctionDefinition { IsAsync: true } fd) {
MSAst.ParameterExpression sendSlot = fd.AsyncSendSlot;
MSAst.ParameterExpression throwSlot = fd.AsyncThrowSlot;
MSAst.ParameterExpression pending = Ast.Variable(typeof(Exception), "$athrow");
return Ast.Block(
typeof(object),
[pending],
AstUtils.YieldReturn(GeneratorLabel, yieldValue),
Ast.Assign(pending, Ast.Field(throwSlot, nameof(StrongBox<Exception>.Value))),
Ast.Assign(Ast.Field(throwSlot, nameof(StrongBox<Exception>.Value)), Ast.Constant(null, typeof(Exception))),
Ast.IfThen(
Ast.ReferenceNotEqual(pending, Ast.Constant(null, typeof(Exception))),
Ast.Call(Ast.Call(s_captureMethod, pending), s_throwMethod)),
Ast.Field(sendSlot, nameof(StrongBox<object>.Value))
);
}
#endif

// (yield z) becomes:
// .comma (1) {
// .void ( .yield_statement (_expression) ),
// $gen.CheckThrowable() // <-- has return result from send
// $gen.CheckThrowable() // <-- has return result from send
// }
return Ast.Block(
AstUtils.YieldReturn(
GeneratorLabel,
Expression == null ? AstUtils.Constant(null) : AstUtils.Convert(Expression, typeof(object))
),
AstUtils.YieldReturn(GeneratorLabel, yieldValue),
CreateCheckThrowExpression(Span) // emits ($gen.CheckThrowable())
);
}
Expand Down
6 changes: 6 additions & 0 deletions src/core/IronPython/Compiler/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1999,10 +1999,16 @@ private Expression ParseAtomExpr() {
if (current is null || !current.IsAsync) {
ReportSyntaxError("'await' outside async function");
}
#if !FEATURE_NET_ASYNC
// Under the generator-based async path, `await` desugars to `yield from`,
// so the enclosing function must be marked a generator.
// Under FEATURE_NET_ASYNC, the body is lowered through AsyncExpression and is *not* a Python generator
// so this mark assignment is being skipped here.
if (current is not null) {
current.IsGenerator = true;
current.GeneratorStop = GeneratorStop;
}
#endif
}

Expression ret = ParseAtom();
Expand Down
Loading
Loading