Skip to content

Commit 22e8f43

Browse files
committed
Create async coroutine cold
1 parent 74df01f commit 22e8f43

4 files changed

Lines changed: 53 additions & 28 deletions

File tree

src/core/IronPython/Compiler/Ast/AstMethods.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ internal static class AstMethods {
8282
public static readonly MethodInfo MakeCoroutine = GetMethod((Func<PythonFunction, MutableTuple, object, PythonCoroutine>)PythonOps.MakeCoroutine);
8383
#if FEATURE_NET_ASYNC
8484
public static readonly MethodInfo AsTaskForAwait = GetMethod((Func<object, System.Threading.Tasks.Task<object>>)PythonOps.AsTaskForAwait);
85-
public static readonly MethodInfo MakeAsyncCoroutine = GetMethod((Func<PythonFunction, System.Threading.Tasks.Task<object>, System.Threading.CancellationTokenSource, System.Runtime.CompilerServices.StrongBox<System.Exception>, PythonCoroutine>)PythonOps.MakeAsyncCoroutine);
85+
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);
8686
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);
8787
#endif
8888

src/core/IronPython/Compiler/Ast/FunctionDefinition.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -760,14 +760,20 @@ private LightLambdaExpression CreateFunctionLambda() {
760760
cts));
761761
} else {
762762
// Plain async def: the body returns a PythonCoroutine wrapping a Task<object?>.
763+
// Lazy start: hand MakeAsyncCoroutine a thunk (Func<Task<object?>>) instead of an
764+
// already-running Task, so the body doesn't execute until the coroutine is first driven
765+
// (send/AsTask). This makes calling an async def side-effect-free (PEP 492) and lets the
766+
// body's first await capture the driver's SynchronizationContext rather than whatever
767+
// context happened to be current at construction.
763768
body = MSAst.Expression.Block(
764769
new[] { cts, excBox },
765770
MSAst.Expression.Assign(cts, MSAst.Expression.New(typeof(System.Threading.CancellationTokenSource))),
766771
MSAst.Expression.Assign(excBox, MSAst.Expression.New(typeof(System.Runtime.CompilerServices.StrongBox<Exception>))),
767772
Ast.Call(
768773
AstMethods.MakeAsyncCoroutine,
769774
_functionParam,
770-
AstUtils.Async(Name, body, ctToken, excBox),
775+
MSAst.Expression.Lambda<Func<System.Threading.Tasks.Task<object>>>(
776+
AstUtils.Async(Name, body, ctToken, excBox)),
771777
cts,
772778
excBox));
773779
}

src/core/IronPython/Runtime/Coroutine.cs

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,34 @@ public sealed class PythonCoroutine : ICodeFormattable, IWeakReferenceable {
3131
// at cancellation time, that exception is surfaced
3232
// to the body instead of OperationCanceledException.
3333
// throw(non-OCE) writes here before cancelling.
34-
private readonly Task<object?> _task;
34+
//
35+
// Lazy start: the body is not executed at construction. _factory is the thunk
36+
// that creates (and thereby starts driving) the Task on first access; _task
37+
// caches the materialized Task. Calling an async def is therefore side-effect
38+
// free until the coroutine is first driven (send/AsTask/GetAwaiter) — PEP 492.
39+
private readonly Func<Task<object?>> _factory;
40+
private Task<object?>? _task;
3541
private readonly string _name;
3642
private readonly FunctionCode? _code;
3743
private readonly CancellationTokenSource _cts;
3844
private readonly StrongBox<Exception?> _cancellationException;
3945
private WeakRefTracker? _tracker;
4046

41-
internal PythonCoroutine(Task<object?> task, string name, FunctionCode? code,
47+
internal PythonCoroutine(Func<Task<object?>> factory, string name, FunctionCode? code,
4248
CancellationTokenSource cts,
4349
StrongBox<Exception?> cancellationException) {
44-
_task = task;
50+
_factory = factory;
4551
_name = name;
4652
_code = code;
4753
_cts = cts;
4854
_cancellationException = cancellationException;
4955
}
5056

57+
/// <summary>
58+
/// Materialize (and start) the underlying Task on first access, caching it thereafter.
59+
/// </summary>
60+
private Task<object?> EnsureTask() => _task ??= _factory();
61+
5162
[LightThrowing]
5263
public object send(object? value) {
5364
// Fast path: task already completed. Dispatch on terminal state without
@@ -58,18 +69,20 @@ public object send(object? value) {
5869
// not pay for exception unwinding (GetAwaiter().GetResult() would observe
5970
// and rethrow cancel/fault). After waiting, the same terminal-state
6071
// dispatch below applies.
61-
if (!_task.IsCompleted) {
62-
((IAsyncResult)_task).AsyncWaitHandle.WaitOne();
72+
var task = EnsureTask();
73+
if (!task.IsCompleted) {
74+
// TODO: coroutines should be steppable via send(), one await at a time.
75+
((IAsyncResult)task).AsyncWaitHandle.WaitOne();
6376
}
64-
if (_task.IsCanceled) {
77+
if (task.IsCanceled) {
6578
// Surfaces as Python CancelledError via the OCE -> CancelledError mapping in PythonExceptions.
6679
// We need an OperationCanceledException instance because the Canceled task carries no Exception object.
67-
return LightExceptions.Throw(new TaskCanceledException(_task));
80+
return LightExceptions.Throw(new TaskCanceledException(task));
6881
}
69-
if (_task.IsFaulted) {
70-
return LightExceptions.Throw(_task.Exception?.InnerException ?? _task.Exception);
82+
if (task.IsFaulted) {
83+
return LightExceptions.Throw(task.Exception?.InnerException ?? task.Exception);
7184
}
72-
return LightExceptions.Throw(new PythonExceptions._StopIteration().InitAndGetClrException(_task.Result!));
85+
return LightExceptions.Throw(new PythonExceptions._StopIteration().InitAndGetClrException(task.Result!));
7386
}
7487

7588
[LightThrowing]
@@ -99,7 +112,8 @@ public object @throw(object? type, object? value, object? traceback) {
99112
Exception ex = PythonOps.MakeExceptionForGenerator(
100113
DefaultContext.Default, type, value, traceback, cause: null);
101114

102-
if (_task.IsCompleted) {
115+
var task = EnsureTask();
116+
if (task.IsCompleted) {
103117
// Regime 1: coroutine has finished. throw() raises the exception immediately —
104118
// the body has nothing left to observe it. Matches CPython.
105119
return LightExceptions.Throw(ex);
@@ -115,18 +129,19 @@ public object @throw(object? type, object? value, object? traceback) {
115129
// Wait for the body to settle. After this the task is in a terminal state — dispatch via
116130
// the same logic as send() so the body's reaction (catch-and-return, propagate, etc.) is
117131
// reflected back to the caller of throw().
118-
((IAsyncResult)_task).AsyncWaitHandle.WaitOne();
132+
((IAsyncResult)task).AsyncWaitHandle.WaitOne();
119133
return SettleCompletedTask();
120134
}
121135

122136
private object SettleCompletedTask() {
123-
if (_task.IsCanceled) {
124-
return LightExceptions.Throw(new TaskCanceledException(_task));
137+
var task = EnsureTask();
138+
if (task.IsCanceled) {
139+
return LightExceptions.Throw(new TaskCanceledException(task));
125140
}
126-
if (_task.IsFaulted) {
127-
return LightExceptions.Throw(_task.Exception?.InnerException ?? _task.Exception);
141+
if (task.IsFaulted) {
142+
return LightExceptions.Throw(task.Exception?.InnerException ?? task.Exception);
128143
}
129-
return LightExceptions.Throw(new PythonExceptions._StopIteration().InitAndGetClrException(_task.Result!));
144+
return LightExceptions.Throw(new PythonExceptions._StopIteration().InitAndGetClrException(task.Result!));
130145
}
131146

132147
[LightThrowing]
@@ -136,21 +151,23 @@ private object SettleCompletedTask() {
136151

137152
public FunctionCode? cr_code => _code;
138153

139-
public int cr_running => _task.IsCompleted ? 0 : 1;
154+
// A not-yet-driven coroutine reports 0 (not running) without materializing the
155+
// Task — merely inspecting cr_running must not start the body.
156+
public int cr_running => _task is null || _task.IsCompleted ? 0 : 1;
140157

141158
public TraceBackFrame? cr_frame => null;
142159

143160
public string __name__ => _name;
144161

145162
public string __qualname__ => _name;
146163

147-
/// <summary>Returns the underlying <see cref="Task{Object}"/>.</summary>
148-
public Task<object?> AsTask() => _task;
164+
/// <summary>Returns the underlying <see cref="Task{Object}"/>, starting the body on first call.</summary>
165+
public Task<object?> AsTask() => EnsureTask();
149166

150167
/// <summary>Enables <c>await coroutine</c> from C# code.</summary>
151-
public TaskAwaiter<object?> GetAwaiter() => _task.GetAwaiter();
168+
public TaskAwaiter<object?> GetAwaiter() => EnsureTask().GetAwaiter();
152169

153-
internal Task<object?> Task => _task;
170+
internal Task<object?> Task => EnsureTask();
154171
#else
155172
[PythonType("coroutine")]
156173
[DontMapIDisposableToContextManager, DontMapIEnumerableToContains]

src/core/IronPython/Runtime/Operations/PythonOps.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3312,19 +3312,21 @@ public static object MakeCoroutineWrapper(PythonGenerator generator) {
33123312
}
33133313

33143314
/// <summary>
3315-
/// Wraps a runtime-async function's <see cref="System.Threading.Tasks.Task"/>
3316-
/// in a <see cref="PythonCoroutine"/> for the caller of <c>async def</c>.
3315+
/// Wraps a runtime-async function's deferred <see cref="System.Threading.Tasks.Task"/>
3316+
/// thunk in a <see cref="PythonCoroutine"/> for the caller of <c>async def</c>.
3317+
/// The thunk is invoked lazily on first drive (send/AsTask), so calling an
3318+
/// <c>async def</c> is side-effect-free until the coroutine is awaited (PEP 492).
33173319
/// The supplied <see cref="System.Threading.CancellationTokenSource"/> and exception-override
33183320
/// box are pre-bound to <see cref="Microsoft.Scripting.Runtime.AsyncHelpers.DriveAsync"/>'s
33193321
/// cancellation channel — the coroutine uses them to implement <c>coro.throw(exc)</c> on a
33203322
/// running body (set the box, cancel the CTS).
33213323
/// </summary>
33223324
public static PythonCoroutine MakeAsyncCoroutine(
33233325
PythonFunction function,
3324-
System.Threading.Tasks.Task<object?> task,
3326+
Func<System.Threading.Tasks.Task<object?>> taskFactory,
33253327
System.Threading.CancellationTokenSource cts,
33263328
System.Runtime.CompilerServices.StrongBox<System.Exception?> cancellationException) {
3327-
return new PythonCoroutine(task, function.__name__, function.__code__, cts, cancellationException);
3329+
return new PythonCoroutine(taskFactory, function.__name__, function.__code__, cts, cancellationException);
33283330
}
33293331

33303332
/// <summary>

0 commit comments

Comments
 (0)