|
4 | 4 |
|
5 | 5 | #nullable enable |
6 | 6 |
|
| 7 | +using System; |
7 | 8 | using System.Runtime.CompilerServices; |
| 9 | +using System.Threading; |
8 | 10 | using System.Threading.Tasks; |
9 | 11 |
|
10 | 12 | using Microsoft.Scripting.Runtime; |
|
14 | 16 | using IronPython.Runtime.Types; |
15 | 17 |
|
16 | 18 | namespace IronPython.Runtime { |
| 19 | +#if FEATURE_NET_ASYNC |
| 20 | + [PythonType("coroutine")] |
| 21 | + [DontMapIDisposableToContextManager, DontMapIEnumerableToContains] |
| 22 | + public sealed class PythonCoroutine : ICodeFormattable, IWeakReferenceable { |
| 23 | + // Under .NET async, `async def` is compiled directly to a Task<object?> |
| 24 | + // via the DLR's AsyncExpression + AsyncHelpers. PythonCoroutine is a |
| 25 | + // facade over the Task with two cancellation channels pre-bound at the |
| 26 | + // codegen level (see FunctionDefinition): |
| 27 | + // _cts — CancellationTokenSource whose Token was passed |
| 28 | + // into AsyncExpression. throw(exc) cancels this. |
| 29 | + // _cancellationException — StrongBox<Exception?> shared with DriveAsync's |
| 30 | + // `cancellationException` parameter; when non-null |
| 31 | + // at cancellation time, that exception is surfaced |
| 32 | + // to the body instead of OperationCanceledException. |
| 33 | + // throw(non-OCE) writes here before cancelling. |
| 34 | + private readonly Task<object?> _task; |
| 35 | + private readonly string _name; |
| 36 | + private readonly FunctionCode? _code; |
| 37 | + private readonly CancellationTokenSource _cts; |
| 38 | + private readonly StrongBox<Exception?> _cancellationException; |
| 39 | + private WeakRefTracker? _tracker; |
| 40 | + |
| 41 | + internal PythonCoroutine(Task<object?> task, string name, FunctionCode? code, |
| 42 | + CancellationTokenSource cts, |
| 43 | + StrongBox<Exception?> cancellationException) { |
| 44 | + _task = task; |
| 45 | + _name = name; |
| 46 | + _code = code; |
| 47 | + _cts = cts; |
| 48 | + _cancellationException = cancellationException; |
| 49 | + } |
| 50 | + |
| 51 | + [LightThrowing] |
| 52 | + public object send(object? value) { |
| 53 | + // Fast path: task already completed. Dispatch on terminal state without |
| 54 | + // any try/catch — Result on a Faulted/Canceled task throws, so we must |
| 55 | + // check IsCanceled / IsFaulted before reading it. |
| 56 | + // |
| 57 | + // Slow path: not completed. Block via AsyncWaitHandle.WaitOne() so we do |
| 58 | + // not pay for exception unwinding (GetAwaiter().GetResult() would observe |
| 59 | + // and rethrow cancel/fault). After waiting, the same terminal-state |
| 60 | + // dispatch below applies. |
| 61 | + if (!_task.IsCompleted) { |
| 62 | + ((IAsyncResult)_task).AsyncWaitHandle.WaitOne(); |
| 63 | + } |
| 64 | + if (_task.IsCanceled) { |
| 65 | + // Surfaces as Python CancelledError via the OCE -> CancelledError mapping in PythonExceptions. |
| 66 | + // We need an OperationCanceledException instance because the Canceled task carries no Exception object. |
| 67 | + return LightExceptions.Throw(new TaskCanceledException(_task)); |
| 68 | + } |
| 69 | + if (_task.IsFaulted) { |
| 70 | + return LightExceptions.Throw(_task.Exception?.InnerException ?? _task.Exception); |
| 71 | + } |
| 72 | + return LightExceptions.Throw(new PythonExceptions._StopIteration().InitAndGetClrException(_task.Result!)); |
| 73 | + } |
| 74 | + |
| 75 | + [LightThrowing] |
| 76 | + public object @throw(object? type) => @throw(type, null, null); |
| 77 | + |
| 78 | + [LightThrowing] |
| 79 | + public object @throw(object? type, object? value) => @throw(type, value, null); |
| 80 | + |
| 81 | + [LightThrowing] |
| 82 | + public object @throw(object? type, object? value, object? traceback) { |
| 83 | + // Validate the shape of (type, value, traceback) up front. Mirrors PythonGenerator.@throw |
| 84 | + // so we don't mutate any state on bad input. |
| 85 | + if (type is Exception || type is PythonExceptions.BaseException) { |
| 86 | + if (value is not null) |
| 87 | + return LightExceptions.Throw(PythonOps.TypeError("instance exception may not have a separate value")); |
| 88 | + } else if (type is PythonType pt && typeof(PythonExceptions.BaseException).IsAssignableFrom(pt.UnderlyingSystemType)) { |
| 89 | + // ok — class form, MakeExceptionForGenerator will construct. |
| 90 | + } else { |
| 91 | + return LightExceptions.Throw(PythonOps.TypeError( |
| 92 | + "exceptions must be classes or instances deriving from BaseException, not {0}", |
| 93 | + PythonOps.GetPythonTypeName(type))); |
| 94 | + } |
| 95 | + |
| 96 | + // Construct the actual exception object. DefaultContext.Default suffices here — throw()'s |
| 97 | + // CodeContext role is limited to resolving class-form exception construction, and the |
| 98 | + // coroutine's own context will re-bind the chain when the exception unwinds inside the body. |
| 99 | + Exception ex = PythonOps.MakeExceptionForGenerator( |
| 100 | + DefaultContext.Default, type, value, traceback, cause: null); |
| 101 | + |
| 102 | + if (_task.IsCompleted) { |
| 103 | + // Regime 1: coroutine has finished. throw() raises the exception immediately — |
| 104 | + // the body has nothing left to observe it. Matches CPython. |
| 105 | + return LightExceptions.Throw(ex); |
| 106 | + } |
| 107 | + |
| 108 | + // Regime 2: body is still running (or suspended at an await). Stash the exception in the |
| 109 | + // shared StrongBox so DriveAsync surfaces it (instead of OCE) the moment it observes the |
| 110 | + // cancellation, then cancel. The body sees `ex` rethrown at its next resume point via the |
| 111 | + // rewriter's per-await ExceptionDispatchInfo block. |
| 112 | + _cancellationException.Value = ex; |
| 113 | + _cts.Cancel(); |
| 114 | + |
| 115 | + // Wait for the body to settle. After this the task is in a terminal state — dispatch via |
| 116 | + // the same logic as send() so the body's reaction (catch-and-return, propagate, etc.) is |
| 117 | + // reflected back to the caller of throw(). |
| 118 | + ((IAsyncResult)_task).AsyncWaitHandle.WaitOne(); |
| 119 | + return SettleCompletedTask(); |
| 120 | + } |
| 121 | + |
| 122 | + private object SettleCompletedTask() { |
| 123 | + if (_task.IsCanceled) { |
| 124 | + return LightExceptions.Throw(new TaskCanceledException(_task)); |
| 125 | + } |
| 126 | + if (_task.IsFaulted) { |
| 127 | + return LightExceptions.Throw(_task.Exception?.InnerException ?? _task.Exception); |
| 128 | + } |
| 129 | + return LightExceptions.Throw(new PythonExceptions._StopIteration().InitAndGetClrException(_task.Result!)); |
| 130 | + } |
| 131 | + |
| 132 | + [LightThrowing] |
| 133 | + public object? close() => null; |
| 134 | + |
| 135 | + public object __await__() => new CoroutineWrapper(this); |
| 136 | + |
| 137 | + public FunctionCode? cr_code => _code; |
| 138 | + |
| 139 | + public int cr_running => _task.IsCompleted ? 0 : 1; |
| 140 | + |
| 141 | + public TraceBackFrame? cr_frame => null; |
| 142 | + |
| 143 | + public string __name__ => _name; |
| 144 | + |
| 145 | + public string __qualname__ => _name; |
| 146 | + |
| 147 | + /// <summary>Returns the underlying <see cref="Task{Object}"/>.</summary> |
| 148 | + public Task<object?> AsTask() => _task; |
| 149 | + |
| 150 | + /// <summary>Enables <c>await coroutine</c> from C# code.</summary> |
| 151 | + public TaskAwaiter<object?> GetAwaiter() => _task.GetAwaiter(); |
| 152 | + |
| 153 | + internal Task<object?> Task => _task; |
| 154 | +#else |
17 | 155 | [PythonType("coroutine")] |
18 | 156 | [DontMapIDisposableToContextManager, DontMapIEnumerableToContains] |
19 | 157 | public sealed class PythonCoroutine : ICodeFormattable, IWeakReferenceable { |
@@ -98,6 +236,7 @@ public string __qualname__ { |
98 | 236 | public TaskAwaiter<object?> GetAwaiter() => AsTask().GetAwaiter(); |
99 | 237 |
|
100 | 238 | internal PythonGenerator Generator => _generator; |
| 239 | +#endif |
101 | 240 |
|
102 | 241 | #region ICodeFormattable Members |
103 | 242 |
|
|
0 commit comments