@@ -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 ]
0 commit comments