Skip to content

Commit 96a7fb9

Browse files
gh-132775: Add _PyCode_ReturnsOnlyNone() (gh-132981)
The function indicates whether or not the function has a return statement. This is used by a later change related treating some functions like scripts.
1 parent 75cbb8d commit 96a7fb9

File tree

6 files changed

+123
-1
lines changed

6 files changed

+123
-1
lines changed

Include/internal/pycore_code.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,10 @@ extern void _Py_ClearTLBCIndex(_PyThreadStateImpl *tstate);
561561
extern int _Py_ClearUnusedTLBC(PyInterpreterState *interp);
562562
#endif
563563

564+
565+
PyAPI_FUNC(int) _PyCode_ReturnsOnlyNone(PyCodeObject *);
566+
567+
564568
#ifdef __cplusplus
565569
}
566570
#endif

Include/internal/pycore_opcode_utils.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ extern "C" {
5454
(opcode) == RAISE_VARARGS || \
5555
(opcode) == RERAISE)
5656

57+
#define IS_RETURN_OPCODE(opcode) \
58+
(opcode == RETURN_VALUE)
59+
5760

5861
/* Flags used in the oparg for MAKE_FUNCTION */
5962
#define MAKE_FUNCTION_DEFAULTS 0x01

Lib/test/test_code.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@
216216
from test.support.bytecode_helper import instructions_with_positions
217217
from opcode import opmap, opname
218218
from _testcapi import code_offset_to_line
219+
try:
220+
import _testinternalcapi
221+
except ModuleNotFoundError:
222+
_testinternalcapi = None
219223

220224
COPY_FREE_VARS = opmap['COPY_FREE_VARS']
221225

@@ -425,6 +429,61 @@ def func():
425429
with self.assertWarns(DeprecationWarning):
426430
func.__code__.co_lnotab
427431

432+
@unittest.skipIf(_testinternalcapi is None, '_testinternalcapi is missing')
433+
def test_returns_only_none(self):
434+
value = True
435+
436+
def spam1():
437+
pass
438+
def spam2():
439+
return
440+
def spam3():
441+
return None
442+
def spam4():
443+
if not value:
444+
return
445+
...
446+
def spam5():
447+
if not value:
448+
return None
449+
...
450+
lambda1 = (lambda: None)
451+
for func in [
452+
spam1,
453+
spam2,
454+
spam3,
455+
spam4,
456+
spam5,
457+
lambda1,
458+
]:
459+
with self.subTest(func):
460+
res = _testinternalcapi.code_returns_only_none(func.__code__)
461+
self.assertTrue(res)
462+
463+
def spam6():
464+
return True
465+
def spam7():
466+
return value
467+
def spam8():
468+
if value:
469+
return None
470+
return True
471+
def spam9():
472+
if value:
473+
return True
474+
return None
475+
lambda2 = (lambda: True)
476+
for func in [
477+
spam6,
478+
spam7,
479+
spam8,
480+
spam9,
481+
lambda2,
482+
]:
483+
with self.subTest(func):
484+
res = _testinternalcapi.code_returns_only_none(func.__code__)
485+
self.assertFalse(res)
486+
428487
def test_invalid_bytecode(self):
429488
def foo():
430489
pass

Modules/_testinternalcapi.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,18 @@ iframe_getlasti(PyObject *self, PyObject *frame)
945945
return PyLong_FromLong(PyUnstable_InterpreterFrame_GetLasti(f));
946946
}
947947

948+
static PyObject *
949+
code_returns_only_none(PyObject *self, PyObject *arg)
950+
{
951+
if (!PyCode_Check(arg)) {
952+
PyErr_SetString(PyExc_TypeError, "argument must be a code object");
953+
return NULL;
954+
}
955+
PyCodeObject *code = (PyCodeObject *)arg;
956+
int res = _PyCode_ReturnsOnlyNone(code);
957+
return PyBool_FromLong(res);
958+
}
959+
948960
static PyObject *
949961
get_co_framesize(PyObject *self, PyObject *arg)
950962
{
@@ -2074,6 +2086,7 @@ static PyMethodDef module_functions[] = {
20742086
{"iframe_getcode", iframe_getcode, METH_O, NULL},
20752087
{"iframe_getline", iframe_getline, METH_O, NULL},
20762088
{"iframe_getlasti", iframe_getlasti, METH_O, NULL},
2089+
{"code_returns_only_none", code_returns_only_none, METH_O, NULL},
20772090
{"get_co_framesize", get_co_framesize, METH_O, NULL},
20782091
{"jit_enabled", jit_enabled, METH_NOARGS, NULL},
20792092
#ifdef _Py_TIER2

Objects/codeobject.c

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1689,6 +1689,49 @@ PyCode_GetFreevars(PyCodeObject *code)
16891689
return _PyCode_GetFreevars(code);
16901690
}
16911691

1692+
1693+
/* Here "value" means a non-None value, since a bare return is identical
1694+
* to returning None explicitly. Likewise a missing return statement
1695+
* at the end of the function is turned into "return None". */
1696+
int
1697+
_PyCode_ReturnsOnlyNone(PyCodeObject *co)
1698+
{
1699+
// Look up None in co_consts.
1700+
Py_ssize_t nconsts = PyTuple_Size(co->co_consts);
1701+
int none_index = 0;
1702+
for (; none_index < nconsts; none_index++) {
1703+
if (PyTuple_GET_ITEM(co->co_consts, none_index) == Py_None) {
1704+
break;
1705+
}
1706+
}
1707+
if (none_index == nconsts) {
1708+
// None wasn't there, which means there was no implicit return,
1709+
// "return", or "return None". That means there must be
1710+
// an explicit return (non-None).
1711+
return 0;
1712+
}
1713+
1714+
// Walk the bytecode, looking for RETURN_VALUE.
1715+
Py_ssize_t len = Py_SIZE(co);
1716+
for (int i = 0; i < len; i++) {
1717+
_Py_CODEUNIT inst = _Py_GetBaseCodeUnit(co, i);
1718+
if (IS_RETURN_OPCODE(inst.op.code)) {
1719+
assert(i != 0);
1720+
// Ignore it if it returns None.
1721+
_Py_CODEUNIT prev = _Py_GetBaseCodeUnit(co, i-1);
1722+
if (prev.op.code == LOAD_CONST) {
1723+
// We don't worry about EXTENDED_ARG for now.
1724+
if (prev.op.arg == none_index) {
1725+
continue;
1726+
}
1727+
}
1728+
return 0;
1729+
}
1730+
}
1731+
return 1;
1732+
}
1733+
1734+
16921735
#ifdef _Py_TIER2
16931736

16941737
static void

Python/flowgraph.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ dump_instr(cfg_instr *i)
295295
static inline int
296296
basicblock_returns(const basicblock *b) {
297297
cfg_instr *last = basicblock_last_instr(b);
298-
return last && last->i_opcode == RETURN_VALUE;
298+
return last && IS_RETURN_OPCODE(last->i_opcode);
299299
}
300300

301301
static void

0 commit comments

Comments
 (0)