Skip to content

Commit 1a15d20

Browse files
miss-islingtoncarljmcorona10
authored
[3.12] gh-108654: restore comprehension locals before handling exception (GH-108659) (#108700)
gh-108654: restore comprehension locals before handling exception (GH-108659) (cherry picked from commit d52c448) Co-authored-by: Carl Meyer <[email protected]> Co-authored-by: Dong-hee Na <[email protected]>
1 parent 320d398 commit 1a15d20

File tree

3 files changed

+90
-14
lines changed

3 files changed

+90
-14
lines changed

Lib/test/test_listcomps.py

+35
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,41 @@ def test_iter_var_available_in_locals(self):
561561
}
562562
)
563563

564+
def test_comp_in_try_except(self):
565+
template = """
566+
value = ["a"]
567+
try:
568+
[{func}(value) for value in value]
569+
except:
570+
pass
571+
"""
572+
for func in ["str", "int"]:
573+
code = template.format(func=func)
574+
raises = func != "str"
575+
with self.subTest(raises=raises):
576+
self._check_in_scopes(code, {"value": ["a"]})
577+
578+
def test_comp_in_try_finally(self):
579+
code = """
580+
def f(value):
581+
try:
582+
[{func}(value) for value in value]
583+
finally:
584+
return value
585+
ret = f(["a"])
586+
"""
587+
self._check_in_scopes(code, {"ret": ["a"]})
588+
589+
def test_exception_in_post_comp_call(self):
590+
code = """
591+
value = [1, None]
592+
try:
593+
[v for v in value].sort()
594+
except:
595+
pass
596+
"""
597+
self._check_in_scopes(code, {"value": [1, None]})
598+
564599

565600
__test__ = {'doctests' : doctests}
566601

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Restore locals shadowed by an inlined comprehension if the comprehension
2+
raises an exception.

Python/compile.c

+53-14
Original file line numberDiff line numberDiff line change
@@ -5438,6 +5438,8 @@ typedef struct {
54385438
PyObject *pushed_locals;
54395439
PyObject *temp_symbols;
54405440
PyObject *fast_hidden;
5441+
jump_target_label cleanup;
5442+
jump_target_label end;
54415443
} inlined_comprehension_state;
54425444

54435445
static int
@@ -5543,11 +5545,45 @@ push_inlined_comprehension_state(struct compiler *c, location loc,
55435545
// `pushed_locals` on the stack, but this will be reversed when we swap
55445546
// out the comprehension result in pop_inlined_comprehension_state
55455547
ADDOP_I(c, loc, SWAP, PyList_GET_SIZE(state->pushed_locals) + 1);
5548+
5549+
// Add our own cleanup handler to restore comprehension locals in case
5550+
// of exception, so they have the correct values inside an exception
5551+
// handler or finally block.
5552+
NEW_JUMP_TARGET_LABEL(c, cleanup);
5553+
state->cleanup = cleanup;
5554+
NEW_JUMP_TARGET_LABEL(c, end);
5555+
state->end = end;
5556+
5557+
// no need to push an fblock for this "virtual" try/finally; there can't
5558+
// be return/continue/break inside a comprehension
5559+
ADDOP_JUMP(c, loc, SETUP_FINALLY, cleanup);
55465560
}
55475561

55485562
return SUCCESS;
55495563
}
55505564

5565+
static int
5566+
restore_inlined_comprehension_locals(struct compiler *c, location loc,
5567+
inlined_comprehension_state state)
5568+
{
5569+
PyObject *k;
5570+
// pop names we pushed to stack earlier
5571+
Py_ssize_t npops = PyList_GET_SIZE(state.pushed_locals);
5572+
// Preserve the comprehension result (or exception) as TOS. This
5573+
// reverses the SWAP we did in push_inlined_comprehension_state to get
5574+
// the outermost iterable to TOS, so we can still just iterate
5575+
// pushed_locals in simple reverse order
5576+
ADDOP_I(c, loc, SWAP, npops + 1);
5577+
for (Py_ssize_t i = npops - 1; i >= 0; --i) {
5578+
k = PyList_GetItem(state.pushed_locals, i);
5579+
if (k == NULL) {
5580+
return ERROR;
5581+
}
5582+
ADDOP_NAME(c, loc, STORE_FAST_MAYBE_NULL, k, varnames);
5583+
}
5584+
return SUCCESS;
5585+
}
5586+
55515587
static int
55525588
pop_inlined_comprehension_state(struct compiler *c, location loc,
55535589
inlined_comprehension_state state)
@@ -5564,19 +5600,22 @@ pop_inlined_comprehension_state(struct compiler *c, location loc,
55645600
Py_CLEAR(state.temp_symbols);
55655601
}
55665602
if (state.pushed_locals) {
5567-
// pop names we pushed to stack earlier
5568-
Py_ssize_t npops = PyList_GET_SIZE(state.pushed_locals);
5569-
// Preserve the list/dict/set result of the comprehension as TOS. This
5570-
// reverses the SWAP we did in push_inlined_comprehension_state to get
5571-
// the outermost iterable to TOS, so we can still just iterate
5572-
// pushed_locals in simple reverse order
5573-
ADDOP_I(c, loc, SWAP, npops + 1);
5574-
for (Py_ssize_t i = npops - 1; i >= 0; --i) {
5575-
k = PyList_GetItem(state.pushed_locals, i);
5576-
if (k == NULL) {
5577-
return ERROR;
5578-
}
5579-
ADDOP_NAME(c, loc, STORE_FAST_MAYBE_NULL, k, varnames);
5603+
ADDOP(c, NO_LOCATION, POP_BLOCK);
5604+
ADDOP_JUMP(c, NO_LOCATION, JUMP, state.end);
5605+
5606+
// cleanup from an exception inside the comprehension
5607+
USE_LABEL(c, state.cleanup);
5608+
// discard incomplete comprehension result (beneath exc on stack)
5609+
ADDOP_I(c, NO_LOCATION, SWAP, 2);
5610+
ADDOP(c, NO_LOCATION, POP_TOP);
5611+
if (restore_inlined_comprehension_locals(c, loc, state) < 0) {
5612+
return ERROR;
5613+
}
5614+
ADDOP_I(c, NO_LOCATION, RERAISE, 0);
5615+
5616+
USE_LABEL(c, state.end);
5617+
if (restore_inlined_comprehension_locals(c, loc, state) < 0) {
5618+
return ERROR;
55805619
}
55815620
Py_CLEAR(state.pushed_locals);
55825621
}
@@ -5619,7 +5658,7 @@ compiler_comprehension(struct compiler *c, expr_ty e, int type,
56195658
expr_ty val)
56205659
{
56215660
PyCodeObject *co = NULL;
5622-
inlined_comprehension_state inline_state = {NULL, NULL};
5661+
inlined_comprehension_state inline_state = {NULL, NULL, NULL, NO_LABEL, NO_LABEL};
56235662
comprehension_ty outermost;
56245663
int scope_type = c->u->u_scope_type;
56255664
int is_top_level_await = IS_TOP_LEVEL_AWAIT(c);

0 commit comments

Comments
 (0)