Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-33041: Rework compiling an "async for" loop. #6142

Merged
merged 3 commits into from Mar 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions Doc/library/dis.rst
Expand Up @@ -588,6 +588,17 @@ the original TOS1.
.. versionadded:: 3.5


.. opcode:: END_ASYNC_FOR

Terminates an :keyword:`async for` loop. Handles an exception raised
when awaiting a next item. If TOS is :exc:`StopAsyncIteration` pop 7
values from the stack and restore the exception state using the second
three of them. Otherwise re-raise the exception using the three values
from the stack. An exception handler block is removed from the block stack.

.. versionadded:: 3.8


.. opcode:: BEFORE_ASYNC_WITH

Resolves ``__aenter__`` and ``__aexit__`` from the object on top of the
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.8.rst
Expand Up @@ -157,3 +157,7 @@ CPython bytecode changes

(Contributed by Mark Shannon, Antoine Pitrou and Serhiy Storchaka in
:issue:`17611`.)

* Added new opcode :opcode:`END_ASYNC_FOR` for handling exceptions raised
when awaiting a next item in an :keyword:`async for` loop.
(Contributed by Serhiy Storchaka in :issue:`33041`.)
1 change: 1 addition & 0 deletions Include/opcode.h
Expand Up @@ -34,6 +34,7 @@ extern "C" {
#define GET_ANEXT 51
#define BEFORE_ASYNC_WITH 52
#define BEGIN_FINALLY 53
#define END_ASYNC_FOR 54
#define INPLACE_ADD 55
#define INPLACE_SUBTRACT 56
#define INPLACE_MULTIPLY 57
Expand Down
3 changes: 2 additions & 1 deletion Lib/importlib/_bootstrap_external.py
Expand Up @@ -247,6 +247,7 @@ def _write_atomic(path, data, mode=0o666):
# Python 3.7a4 3392 (PEP 552: Deterministic pycs #31650)
# Python 3.7b1 3393 (remove STORE_ANNOTATION opcode #32550)
# Python 3.8a1 3400 (move frame block handling to compiler #17611)
# Python 3.8a1 3401 (add END_ASYNC_FOR #33041)
#
# MAGIC must change whenever the bytecode emitted by the compiler may no
# longer be understood by older implementations of the eval loop (usually
Expand All @@ -255,7 +256,7 @@ def _write_atomic(path, data, mode=0o666):
# Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
# in PC/launcher.c must also be updated.

MAGIC_NUMBER = (3400).to_bytes(2, 'little') + b'\r\n'
MAGIC_NUMBER = (3401).to_bytes(2, 'little') + b'\r\n'
_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c

_PYCACHE = '__pycache__'
Expand Down
2 changes: 1 addition & 1 deletion Lib/opcode.py
Expand Up @@ -88,7 +88,7 @@ def jabs_op(name, op):
def_op('GET_ANEXT', 51)
def_op('BEFORE_ASYNC_WITH', 52)
def_op('BEGIN_FINALLY', 53)

def_op('END_ASYNC_FOR', 54)
def_op('INPLACE_ADD', 55)
def_op('INPLACE_SUBTRACT', 56)
def_op('INPLACE_MULTIPLY', 57)
Expand Down
30 changes: 30 additions & 0 deletions Lib/test/test_coroutines.py
Expand Up @@ -1846,6 +1846,36 @@ async def run_gen():
run_async(run_gen()),
([], [121]))

def test_comp_4_2(self):
async def f(it):
for i in it:
yield i

async def run_list():
return [i + 10 async for i in f(range(5)) if 0 < i < 4]
self.assertEqual(
run_async(run_list()),
([], [11, 12, 13]))

async def run_set():
return {i + 10 async for i in f(range(5)) if 0 < i < 4}
self.assertEqual(
run_async(run_set()),
([], {11, 12, 13}))

async def run_dict():
return {i + 10: i + 100 async for i in f(range(5)) if 0 < i < 4}
self.assertEqual(
run_async(run_dict()),
([], {11: 101, 12: 102, 13: 103}))

async def run_gen():
gen = (i + 10 async for i in f(range(5)) if 0 < i < 4)
return [g + 100 async for g in gen]
self.assertEqual(
run_async(run_gen()),
([], [111, 112, 113]))

def test_comp_5(self):
async def f(it):
for i in it:
Expand Down
3 changes: 1 addition & 2 deletions Lib/test/test_dis.py
Expand Up @@ -747,8 +747,7 @@ async def async_def():
1: 1
Names:
0: b
1: StopAsyncIteration
2: c
1: c
Variable names:
0: a
1: d"""
Expand Down
55 changes: 55 additions & 0 deletions Lib/test/test_sys_settrace.py
Expand Up @@ -33,6 +33,10 @@ async def __aenter__(self):
async def __aexit__(self, *exc_info):
self.output.append(-self.value)

async def asynciter(iterable):
"""Convert an iterable to an asynchronous iterator."""
for x in iterable:
yield x


# A very basic example. If this fails, we're in deep trouble.
Expand Down Expand Up @@ -720,6 +724,23 @@ def test_jump_out_of_block_backwards(output):
output.append(6)
output.append(7)

@async_jump_test(4, 5, [3, 5])
async def test_jump_out_of_async_for_block_forwards(output):
for i in [1]:
async for i in asynciter([1, 2]):
output.append(3)
output.append(4)
output.append(5)

@async_jump_test(5, 2, [2, 4, 2, 4, 5, 6])
async def test_jump_out_of_async_for_block_backwards(output):
for i in [1]:
output.append(2)
async for i in asynciter([1]):
output.append(4)
output.append(5)
output.append(6)

@jump_test(1, 2, [3])
def test_jump_to_codeless_line(output):
output.append(1)
Expand Down Expand Up @@ -1030,6 +1051,17 @@ def test_jump_over_for_block_before_else(output):
output.append(7)
output.append(8)

@async_jump_test(1, 7, [7, 8])
async def test_jump_over_async_for_block_before_else(output):
output.append(1)
if not output: # always false
async for i in asynciter([3]):
output.append(4)
else:
output.append(6)
output.append(7)
output.append(8)

# The second set of 'jump' tests are for things that are not allowed:

@jump_test(2, 3, [1], (ValueError, 'after'))
Expand Down Expand Up @@ -1081,12 +1113,24 @@ def test_no_jump_forwards_into_for_block(output):
for i in 1, 2:
output.append(3)

@async_jump_test(1, 3, [], (ValueError, 'into'))
async def test_no_jump_forwards_into_async_for_block(output):
output.append(1)
async for i in asynciter([1, 2]):
output.append(3)

@jump_test(3, 2, [2, 2], (ValueError, 'into'))
def test_no_jump_backwards_into_for_block(output):
for i in 1, 2:
output.append(2)
output.append(3)

@async_jump_test(3, 2, [2, 2], (ValueError, 'into'))
async def test_no_jump_backwards_into_async_for_block(output):
async for i in asynciter([1, 2]):
output.append(2)
output.append(3)

@jump_test(1, 3, [], (ValueError, 'into'))
def test_no_jump_forwards_into_with_block(output):
output.append(1)
Expand Down Expand Up @@ -1220,6 +1264,17 @@ def test_no_jump_into_for_block_before_else(output):
output.append(7)
output.append(8)

@async_jump_test(7, 4, [1, 6], (ValueError, 'into'))
async def test_no_jump_into_async_for_block_before_else(output):
output.append(1)
if not output: # always false
async for i in asynciter([3]):
output.append(4)
else:
output.append(6)
output.append(7)
output.append(8)

def test_no_jump_to_non_integers(self):
self.run_test(no_jump_to_non_integers, 2, "Spam", [True])

Expand Down
@@ -0,0 +1,6 @@
Added new opcode :opcode:`END_ASYNC_FOR` and fixes the following issues:

* Setting global :exc:`StopAsyncIteration` no longer breaks ``async for``
loops.
* Jumping into an ``async for`` loop is now disabled.
* Jumping out of an ``async for`` loop no longer corrupts the stack.
21 changes: 9 additions & 12 deletions Objects/frameobject.c
Expand Up @@ -100,8 +100,7 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
int line = 0; /* (ditto) */
int addr = 0; /* (ditto) */
int delta_iblock = 0; /* Scanning the SETUPs and POPs */
int for_loop_delta = 0; /* (ditto) */
int delta;
int delta = 0;
int blockstack[CO_MAXBLOCKS]; /* Walking the 'finally' blocks */
int blockstack_top = 0; /* (ditto) */

Expand Down Expand Up @@ -256,14 +255,16 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
return -1;
}
if (first_in && !second_in) {
if (op == FOR_ITER && !delta_iblock) {
for_loop_delta++;
}
if (op != FOR_ITER) {
if (op != FOR_ITER && code[target_addr] != END_ASYNC_FOR) {
delta_iblock++;
}
else if (!delta_iblock) {
/* Pop the iterators of any 'for' and 'async for' loop
* we're jumping out of. */
delta++;
}
}
if (op != FOR_ITER) {
if (op != FOR_ITER && code[target_addr] != END_ASYNC_FOR) {
blockstack[blockstack_top++] = target_addr;
}
break;
Expand All @@ -289,21 +290,17 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno)
assert(blockstack_top == 0);

/* Pop any blocks that we're jumping out of. */
delta = 0;
if (delta_iblock > 0) {
f->f_iblock -= delta_iblock;
PyTryBlock *b = &f->f_blockstack[f->f_iblock];
delta = (f->f_stacktop - f->f_valuestack) - b->b_level;
delta += (f->f_stacktop - f->f_valuestack) - b->b_level;
if (b->b_type == SETUP_FINALLY &&
code[b->b_handler] == WITH_CLEANUP_START)
{
/* Pop the exit function. */
delta++;
}
}
/* Pop the iterators of any 'for' loop we're jumping out of. */
delta += for_loop_delta;

while (delta > 0) {
PyObject *v = (*--f->f_stacktop);
Py_DECREF(v);
Expand Down
20 changes: 20 additions & 0 deletions Python/ceval.c
Expand Up @@ -1944,6 +1944,26 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
}
}

TARGET(END_ASYNC_FOR) {
PyObject *exc = POP();
assert(PyExceptionClass_Check(exc));
if (PyErr_GivenExceptionMatches(exc, PyExc_StopAsyncIteration)) {
PyTryBlock *b = PyFrame_BlockPop(f);
assert(b->b_type == EXCEPT_HANDLER);
Py_DECREF(exc);
UNWIND_EXCEPT_HANDLER(b);
Py_DECREF(POP());
JUMPBY(oparg);
FAST_DISPATCH();
}
else {
PyObject *val = POP();
PyObject *tb = POP();
PyErr_Restore(exc, val, tb);
goto exception_unwind;
}
}

TARGET(LOAD_BUILD_CLASS) {
_Py_IDENTIFIER(__build_class__);

Expand Down