Skip to content

Commit ae3087c

Browse files
markshannonpitrou
authored andcommitted
Move exc state to generator. Fixes bpo-25612 (#1773)
Move exception state information from frame objects to coroutine (generator/thread) object where it belongs.
1 parent 91dc64b commit ae3087c

13 files changed

+188
-164
lines changed

Include/frameobject.h

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,6 @@ typedef struct _frame {
3030
char f_trace_lines; /* Emit per-line trace events? */
3131
char f_trace_opcodes; /* Emit per-opcode trace events? */
3232

33-
/* In a generator, we need to be able to swap between the exception
34-
state inside the generator and the exception state of the calling
35-
frame (which shouldn't be impacted when the generator "yields"
36-
from an except handler).
37-
These three fields exist exactly for that, and are unused for
38-
non-generator frames. See the save_exc_state and swap_exc_state
39-
functions in ceval.c for details of their use. */
40-
PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;
4133
/* Borrowed reference to a generator, or NULL */
4234
PyObject *f_gen;
4335

Include/genobject.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ struct _frame; /* Avoid including frameobject.h */
2525
/* Name of the generator. */ \
2626
PyObject *prefix##_name; \
2727
/* Qualified name of the generator. */ \
28-
PyObject *prefix##_qualname;
28+
PyObject *prefix##_qualname; \
29+
_PyErr_StackItem prefix##_exc_state;
2930

3031
typedef struct {
3132
/* The gi_ prefix is intended to remind of generator-iterator. */

Include/pyerrors.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ PyAPI_FUNC(void) PyErr_SetNone(PyObject *);
7878
PyAPI_FUNC(void) PyErr_SetObject(PyObject *, PyObject *);
7979
#ifndef Py_LIMITED_API
8080
PyAPI_FUNC(void) _PyErr_SetKeyError(PyObject *);
81+
_PyErr_StackItem *_PyErr_GetTopmostException(PyThreadState *tstate);
8182
#endif
8283
PyAPI_FUNC(void) PyErr_SetString(
8384
PyObject *exception,

Include/pystate.h

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,21 @@ typedef int (*Py_tracefunc)(PyObject *, struct _frame *, int, PyObject *);
123123
#ifdef Py_LIMITED_API
124124
typedef struct _ts PyThreadState;
125125
#else
126+
127+
typedef struct _err_stackitem {
128+
/* This struct represents an entry on the exception stack, which is a
129+
* per-coroutine state. (Coroutine in the computer science sense,
130+
* including the thread and generators).
131+
* This ensures that the exception state is not impacted by "yields"
132+
* from an except handler.
133+
*/
134+
PyObject *exc_type, *exc_value, *exc_traceback;
135+
136+
struct _err_stackitem *previous_item;
137+
138+
} _PyErr_StackItem;
139+
140+
126141
typedef struct _ts {
127142
/* See Python/ceval.c for comments explaining most fields */
128143

@@ -147,13 +162,19 @@ typedef struct _ts {
147162
PyObject *c_profileobj;
148163
PyObject *c_traceobj;
149164

165+
/* The exception currently being raised */
150166
PyObject *curexc_type;
151167
PyObject *curexc_value;
152168
PyObject *curexc_traceback;
153169

154-
PyObject *exc_type;
155-
PyObject *exc_value;
156-
PyObject *exc_traceback;
170+
/* The exception currently being handled, if no coroutines/generators
171+
* are present. Always last element on the stack referred to be exc_info.
172+
*/
173+
_PyErr_StackItem exc_state;
174+
175+
/* Pointer to the top of the stack of the exceptions currently
176+
* being handled */
177+
_PyErr_StackItem *exc_info;
157178

158179
PyObject *dict; /* Stores per-thread state */
159180

Lib/test/test_exceptions.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,6 +1097,62 @@ def test_unhandled(self):
10971097
self.assertIn("test message", report)
10981098
self.assertTrue(report.endswith("\n"))
10991099

1100+
def test_yield_in_nested_try_excepts(self):
1101+
#Issue #25612
1102+
class MainError(Exception):
1103+
pass
1104+
1105+
class SubError(Exception):
1106+
pass
1107+
1108+
def main():
1109+
try:
1110+
raise MainError()
1111+
except MainError:
1112+
try:
1113+
yield
1114+
except SubError:
1115+
pass
1116+
raise
1117+
1118+
coro = main()
1119+
coro.send(None)
1120+
with self.assertRaises(MainError):
1121+
coro.throw(SubError())
1122+
1123+
def test_generator_doesnt_retain_old_exc2(self):
1124+
#Issue 28884#msg282532
1125+
def g():
1126+
try:
1127+
raise ValueError
1128+
except ValueError:
1129+
yield 1
1130+
self.assertEqual(sys.exc_info(), (None, None, None))
1131+
yield 2
1132+
1133+
gen = g()
1134+
1135+
try:
1136+
raise IndexError
1137+
except IndexError:
1138+
self.assertEqual(next(gen), 1)
1139+
self.assertEqual(next(gen), 2)
1140+
1141+
def test_raise_in_generator(self):
1142+
#Issue 25612#msg304117
1143+
def g():
1144+
yield 1
1145+
raise
1146+
yield 2
1147+
1148+
with self.assertRaises(ZeroDivisionError):
1149+
i = g()
1150+
try:
1151+
1/0
1152+
except:
1153+
next(i)
1154+
next(i)
1155+
11001156

11011157
class ImportErrorTests(unittest.TestCase):
11021158

Lib/test/test_sys.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -971,7 +971,7 @@ class C(object): pass
971971
nfrees = len(x.f_code.co_freevars)
972972
extras = x.f_code.co_stacksize + x.f_code.co_nlocals +\
973973
ncells + nfrees - 1
974-
check(x, vsize('8P2c4P3ic' + CO_MAXBLOCKS*'3i' + 'P' + extras*'P'))
974+
check(x, vsize('5P2c4P3ic' + CO_MAXBLOCKS*'3i' + 'P' + extras*'P'))
975975
# function
976976
def func(): pass
977977
check(func, size('12P'))
@@ -988,7 +988,7 @@ def bar(cls):
988988
check(bar, size('PP'))
989989
# generator
990990
def get_gen(): yield 1
991-
check(get_gen(), size('Pb2PPP'))
991+
check(get_gen(), size('Pb2PPP4P'))
992992
# iterator
993993
check(iter('abc'), size('lP'))
994994
# callable-iterator
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Move the current exception state from the frame object to the co-routine.
2+
This simplifies the interpreter and fixes a couple of obscure bugs caused by
3+
having swap exception state when entering or exiting a generator.

Objects/frameobject.c

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,7 @@ static PyGetSetDef frame_getsetlist[] = {
379379
380380
* ob_type, ob_size, f_code, f_valuestack;
381381
382-
* f_locals, f_trace,
383-
f_exc_type, f_exc_value, f_exc_traceback are NULL;
382+
* f_locals, f_trace are NULL;
384383
385384
* f_localsplus does not require re-allocation and
386385
the local variables in f_localsplus are NULL.
@@ -438,9 +437,6 @@ frame_dealloc(PyFrameObject *f)
438437
Py_DECREF(f->f_globals);
439438
Py_CLEAR(f->f_locals);
440439
Py_CLEAR(f->f_trace);
441-
Py_CLEAR(f->f_exc_type);
442-
Py_CLEAR(f->f_exc_value);
443-
Py_CLEAR(f->f_exc_traceback);
444440

445441
co = f->f_code;
446442
if (co->co_zombieframe == NULL)
@@ -469,9 +465,6 @@ frame_traverse(PyFrameObject *f, visitproc visit, void *arg)
469465
Py_VISIT(f->f_globals);
470466
Py_VISIT(f->f_locals);
471467
Py_VISIT(f->f_trace);
472-
Py_VISIT(f->f_exc_type);
473-
Py_VISIT(f->f_exc_value);
474-
Py_VISIT(f->f_exc_traceback);
475468

476469
/* locals */
477470
slots = f->f_code->co_nlocals + PyTuple_GET_SIZE(f->f_code->co_cellvars) + PyTuple_GET_SIZE(f->f_code->co_freevars);
@@ -502,9 +495,6 @@ frame_tp_clear(PyFrameObject *f)
502495
f->f_stacktop = NULL;
503496
f->f_executing = 0;
504497

505-
Py_CLEAR(f->f_exc_type);
506-
Py_CLEAR(f->f_exc_value);
507-
Py_CLEAR(f->f_exc_traceback);
508498
Py_CLEAR(f->f_trace);
509499

510500
/* locals */
@@ -698,7 +688,6 @@ _PyFrame_New_NoTrack(PyThreadState *tstate, PyCodeObject *code,
698688
f->f_localsplus[i] = NULL;
699689
f->f_locals = NULL;
700690
f->f_trace = NULL;
701-
f->f_exc_type = f->f_exc_value = f->f_exc_traceback = NULL;
702691
}
703692
f->f_stacktop = f->f_valuestack;
704693
f->f_builtins = builtins;

Objects/genobject.c

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,23 @@ static char *NON_INIT_CORO_MSG = "can't send non-None value to a "
1616
static char *ASYNC_GEN_IGNORED_EXIT_MSG =
1717
"async generator ignored GeneratorExit";
1818

19+
static inline int
20+
exc_state_traverse(_PyErr_StackItem *exc_state, visitproc visit, void *arg)
21+
{
22+
Py_VISIT(exc_state->exc_type);
23+
Py_VISIT(exc_state->exc_value);
24+
Py_VISIT(exc_state->exc_traceback);
25+
return 0;
26+
}
27+
1928
static int
2029
gen_traverse(PyGenObject *gen, visitproc visit, void *arg)
2130
{
2231
Py_VISIT((PyObject *)gen->gi_frame);
2332
Py_VISIT(gen->gi_code);
2433
Py_VISIT(gen->gi_name);
2534
Py_VISIT(gen->gi_qualname);
26-
return 0;
35+
return exc_state_traverse(&gen->gi_exc_state, visit, arg);
2736
}
2837

2938
void
@@ -87,6 +96,21 @@ _PyGen_Finalize(PyObject *self)
8796
PyErr_Restore(error_type, error_value, error_traceback);
8897
}
8998

99+
static inline void
100+
exc_state_clear(_PyErr_StackItem *exc_state)
101+
{
102+
PyObject *t, *v, *tb;
103+
t = exc_state->exc_type;
104+
v = exc_state->exc_value;
105+
tb = exc_state->exc_traceback;
106+
exc_state->exc_type = NULL;
107+
exc_state->exc_value = NULL;
108+
exc_state->exc_traceback = NULL;
109+
Py_XDECREF(t);
110+
Py_XDECREF(v);
111+
Py_XDECREF(tb);
112+
}
113+
90114
static void
91115
gen_dealloc(PyGenObject *gen)
92116
{
@@ -116,6 +140,7 @@ gen_dealloc(PyGenObject *gen)
116140
Py_CLEAR(gen->gi_code);
117141
Py_CLEAR(gen->gi_name);
118142
Py_CLEAR(gen->gi_qualname);
143+
exc_state_clear(&gen->gi_exc_state);
119144
PyObject_GC_Del(gen);
120145
}
121146

@@ -187,7 +212,11 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing)
187212
f->f_back = tstate->frame;
188213

189214
gen->gi_running = 1;
215+
gen->gi_exc_state.previous_item = tstate->exc_info;
216+
tstate->exc_info = &gen->gi_exc_state;
190217
result = PyEval_EvalFrameEx(f, exc);
218+
tstate->exc_info = gen->gi_exc_state.previous_item;
219+
gen->gi_exc_state.previous_item = NULL;
191220
gen->gi_running = 0;
192221

193222
/* Don't keep the reference to f_back any longer than necessary. It
@@ -281,16 +310,7 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing)
281310
if (!result || f->f_stacktop == NULL) {
282311
/* generator can't be rerun, so release the frame */
283312
/* first clean reference cycle through stored exception traceback */
284-
PyObject *t, *v, *tb;
285-
t = f->f_exc_type;
286-
v = f->f_exc_value;
287-
tb = f->f_exc_traceback;
288-
f->f_exc_type = NULL;
289-
f->f_exc_value = NULL;
290-
f->f_exc_traceback = NULL;
291-
Py_XDECREF(t);
292-
Py_XDECREF(v);
293-
Py_XDECREF(tb);
313+
exc_state_clear(&gen->gi_exc_state);
294314
gen->gi_frame->f_gen = NULL;
295315
gen->gi_frame = NULL;
296316
Py_DECREF(f);
@@ -810,6 +830,10 @@ gen_new_with_qualname(PyTypeObject *type, PyFrameObject *f,
810830
gen->gi_code = (PyObject *)(f->f_code);
811831
gen->gi_running = 0;
812832
gen->gi_weakreflist = NULL;
833+
gen->gi_exc_state.exc_type = NULL;
834+
gen->gi_exc_state.exc_value = NULL;
835+
gen->gi_exc_state.exc_traceback = NULL;
836+
gen->gi_exc_state.previous_item = NULL;
813837
if (name != NULL)
814838
gen->gi_name = name;
815839
else

0 commit comments

Comments
 (0)