Skip to content

Commit b04dfbb

Browse files
authored
bpo-46409: Make generators in bytecode (GH-30633)
* Add RETURN_GENERATOR and JUMP_NO_INTERRUPT opcodes. * Trim frame and generator by word each. * Minor refactor of frame.c * Update test.test_sys to account for smaller frames. * Treat generator functions as normal functions when evaluating and specializing.
1 parent d05a663 commit b04dfbb

18 files changed

+235
-204
lines changed

Doc/library/dis.rst

+15
Original file line numberDiff line numberDiff line change
@@ -942,6 +942,13 @@ All of the following opcodes use their arguments.
942942
Set bytecode counter to *target*.
943943

944944

945+
.. opcode:: JUMP_NO_INTERRUPT (target)
946+
947+
Set bytecode counter to *target*. Do not check for interrupts.
948+
949+
.. versionadded:: 3.11
950+
951+
945952
.. opcode:: FOR_ITER (delta)
946953

947954
TOS is an :term:`iterator`. Call its :meth:`~iterator.__next__` method. If
@@ -1220,6 +1227,14 @@ All of the following opcodes use their arguments.
12201227
.. versionadded:: 3.11
12211228

12221229

1230+
.. opcode:: RETURN_GENERATOR
1231+
1232+
Create a generator, coroutine, or async generator from the current frame.
1233+
Clear the current frame and return the newly created generator.
1234+
1235+
.. versionadded:: 3.11
1236+
1237+
12231238
.. opcode:: HAVE_ARGUMENT
12241239

12251240
This is not really an opcode. It identifies the dividing line between

Include/cpython/genobject.h

-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ extern "C" {
1313
and coroutine objects. */
1414
#define _PyGenObject_HEAD(prefix) \
1515
PyObject_HEAD \
16-
/* Note: gi_frame can be NULL if the generator is "finished" */ \
1716
/* The code object backing the generator */ \
1817
PyCodeObject *prefix##_code; \
1918
/* List of weak reference. */ \

Include/internal/pycore_frame.h

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ typedef struct _interpreter_frame {
4141
PyObject *f_locals; /* Strong reference, may be NULL */
4242
PyCodeObject *f_code; /* Strong reference */
4343
PyFrameObject *frame_obj; /* Strong reference, may be NULL */
44-
PyObject *generator; /* Borrowed reference, may be NULL */
4544
struct _interpreter_frame *previous;
4645
int f_lasti; /* Last instruction if called */
4746
int stacktop; /* Offset of TOS from localsplus */
4847
PyFrameState f_state; /* What state the frame is in */
4948
bool is_entry; // Whether this is the "root" frame for the current CFrame.
49+
bool is_generator;
5050
PyObject *localsplus[1];
5151
} InterpreterFrame;
5252

@@ -100,10 +100,10 @@ _PyFrame_InitializeSpecials(
100100
frame->f_locals = Py_XNewRef(locals);
101101
frame->stacktop = nlocalsplus;
102102
frame->frame_obj = NULL;
103-
frame->generator = NULL;
104103
frame->f_lasti = -1;
105104
frame->f_state = FRAME_CREATED;
106105
frame->is_entry = false;
106+
frame->is_generator = false;
107107
}
108108

109109
/* Gets the pointer to the locals array

Include/opcode.h

+15-13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/importlib/_bootstrap_external.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ def _write_atomic(path, data, mode=0o666):
380380
# Python 3.11a4 3472 (bpo-46009: replace GEN_START with POP_TOP)
381381
# Python 3.11a4 3473 (Add POP_JUMP_IF_NOT_NONE/POP_JUMP_IF_NONE opcodes)
382382
# Python 3.11a4 3474 (Add RESUME opcode)
383+
# Python 3.11a5 3475 (Add RETURN_GENERATOR opcode)
383384

384385
# Python 3.12 will start with magic number 3500
385386

@@ -393,7 +394,7 @@ def _write_atomic(path, data, mode=0o666):
393394
# Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
394395
# in PC/launcher.c must also be updated.
395396

396-
MAGIC_NUMBER = (3474).to_bytes(2, 'little') + b'\r\n'
397+
MAGIC_NUMBER = (3475).to_bytes(2, 'little') + b'\r\n'
397398
_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c
398399

399400
_PYCACHE = '__pycache__'

Lib/inspect.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -1819,11 +1819,11 @@ def getgeneratorstate(generator):
18191819
"""
18201820
if generator.gi_running:
18211821
return GEN_RUNNING
1822+
if generator.gi_suspended:
1823+
return GEN_SUSPENDED
18221824
if generator.gi_frame is None:
18231825
return GEN_CLOSED
1824-
if generator.gi_frame.f_lasti == -1:
1825-
return GEN_CREATED
1826-
return GEN_SUSPENDED
1826+
return GEN_CREATED
18271827

18281828

18291829
def getgeneratorlocals(generator):
@@ -1861,11 +1861,11 @@ def getcoroutinestate(coroutine):
18611861
"""
18621862
if coroutine.cr_running:
18631863
return CORO_RUNNING
1864+
if coroutine.cr_suspended:
1865+
return CORO_SUSPENDED
18641866
if coroutine.cr_frame is None:
18651867
return CORO_CLOSED
1866-
if coroutine.cr_frame.f_lasti == -1:
1867-
return CORO_CREATED
1868-
return CORO_SUSPENDED
1868+
return CORO_CREATED
18691869

18701870

18711871
def getcoroutinelocals(coroutine):

Lib/opcode.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def jabs_op(name, op):
9494

9595
def_op('GET_AWAITABLE', 73)
9696
def_op('LOAD_ASSERTION_ERROR', 74)
97+
def_op('RETURN_GENERATOR', 75)
9798

9899
def_op('LIST_TO_TUPLE', 82)
99100
def_op('RETURN_VALUE', 83)
@@ -155,7 +156,7 @@ def jabs_op(name, op):
155156

156157
def_op('MAKE_FUNCTION', 132) # Flags
157158
def_op('BUILD_SLICE', 133) # Number of items
158-
159+
jabs_op('JUMP_NO_INTERRUPT', 134) # Target byte offset from beginning of code
159160
def_op('MAKE_CELL', 135)
160161
hasfree.append(135)
161162
def_op('LOAD_CLOSURE', 136)

Lib/test/test_compile.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -954,7 +954,7 @@ def return_genexp():
954954
x
955955
in
956956
y)
957-
genexp_lines = [None, 1, 3, 1]
957+
genexp_lines = [1, 3, 1]
958958

959959
genexp_code = return_genexp.__code__.co_consts[1]
960960
code_lines = [ None if line is None else line-return_genexp.__code__.co_firstlineno
@@ -967,7 +967,7 @@ async def test(aseq):
967967
async for i in aseq:
968968
body
969969

970-
expected_lines = [None, 0, 1, 2, 1]
970+
expected_lines = [0, 1, 2, 1]
971971
code_lines = [ None if line is None else line-test.__code__.co_firstlineno
972972
for (_, _, line) in test.__code__.co_lines() ]
973973
self.assertEqual(expected_lines, code_lines)

Lib/test/test_generators.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,7 @@ def b():
897897
>>> type(i)
898898
<class 'generator'>
899899
>>> [s for s in dir(i) if not s.startswith('_')]
900-
['close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']
900+
['close', 'gi_code', 'gi_frame', 'gi_running', 'gi_suspended', 'gi_yieldfrom', 'send', 'throw']
901901
>>> from test.support import HAVE_DOCSTRINGS
902902
>>> print(i.__next__.__doc__ if HAVE_DOCSTRINGS else 'Implement next(self).')
903903
Implement next(self).

Lib/test/test_sys.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1386,7 +1386,7 @@ class C(object): pass
13861386
def func():
13871387
return sys._getframe()
13881388
x = func()
1389-
check(x, size('3Pi3c8P2ic?P'))
1389+
check(x, size('3Pi3c7P2ic??P'))
13901390
# function
13911391
def func(): pass
13921392
check(func, size('14Pi'))
@@ -1403,7 +1403,7 @@ def bar(cls):
14031403
check(bar, size('PP'))
14041404
# generator
14051405
def get_gen(): yield 1
1406-
check(get_gen(), size('P2P4P4c8P2ic?P'))
1406+
check(get_gen(), size('P2P4P4c7P2ic??P'))
14071407
# iterator
14081408
check(iter('abc'), size('lP'))
14091409
# callable-iterator
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Add new ``RETURN_GENERATOR`` bytecode to make generators.
2+
Simplifies calling Python functions in the VM, as they no
3+
longer any need to special case generator functions.
4+
5+
Also add ``JUMP_NO_INTERRUPT`` bytecode that acts like
6+
``JUMP_ABSOLUTE``, but does not check for interrupts.

Objects/frameobject.c

+7-3
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ mark_stacks(PyCodeObject *code_obj, int len)
242242
break;
243243
}
244244
case JUMP_ABSOLUTE:
245+
case JUMP_NO_INTERRUPT:
245246
j = get_arg(code, i);
246247
assert(j < len);
247248
if (stacks[j] == UNINITIALIZED && j < i) {
@@ -625,7 +626,7 @@ frame_dealloc(PyFrameObject *f)
625626
{
626627
/* It is the responsibility of the owning generator/coroutine
627628
* to have cleared the generator pointer */
628-
assert(f->f_frame->generator == NULL);
629+
assert(!f->f_frame->is_generator);
629630

630631
if (_PyObject_GC_IS_TRACKED(f)) {
631632
_PyObject_GC_UNTRACK(f);
@@ -698,8 +699,11 @@ frame_clear(PyFrameObject *f, PyObject *Py_UNUSED(ignored))
698699
"cannot clear an executing frame");
699700
return NULL;
700701
}
701-
if (f->f_frame->generator) {
702-
_PyGen_Finalize(f->f_frame->generator);
702+
if (f->f_frame->is_generator) {
703+
assert(!f->f_owns_frame);
704+
size_t offset_in_gen = offsetof(PyGenObject, gi_iframe);
705+
PyObject *gen = (PyObject *)(((char *)f->f_frame) - offset_in_gen);
706+
_PyGen_Finalize(gen);
703707
}
704708
(void)frame_tp_clear(f);
705709
Py_RETURN_NONE;

0 commit comments

Comments
 (0)