https://github.com/python/cpython/commit/96a7fb93a8e31eae368b9efee945f6959355e8ba commit: 96a7fb93a8e31eae368b9efee945f6959355e8ba branch: main author: Eric Snow <ericsnowcurren...@gmail.com> committer: ericsnowcurrently <ericsnowcurren...@gmail.com> date: 2025-04-28T20:12:52-06:00 summary:
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. files: M Include/internal/pycore_code.h M Include/internal/pycore_opcode_utils.h M Lib/test/test_code.py M Modules/_testinternalcapi.c M Objects/codeobject.c M Python/flowgraph.c diff --git a/Include/internal/pycore_code.h b/Include/internal/pycore_code.h index 2839b9b7ebe0fb..eced772493ee30 100644 --- a/Include/internal/pycore_code.h +++ b/Include/internal/pycore_code.h @@ -561,6 +561,10 @@ extern void _Py_ClearTLBCIndex(_PyThreadStateImpl *tstate); extern int _Py_ClearUnusedTLBC(PyInterpreterState *interp); #endif + +PyAPI_FUNC(int) _PyCode_ReturnsOnlyNone(PyCodeObject *); + + #ifdef __cplusplus } #endif diff --git a/Include/internal/pycore_opcode_utils.h b/Include/internal/pycore_opcode_utils.h index b3056e7bb84c69..62af06dc01c125 100644 --- a/Include/internal/pycore_opcode_utils.h +++ b/Include/internal/pycore_opcode_utils.h @@ -54,6 +54,9 @@ extern "C" { (opcode) == RAISE_VARARGS || \ (opcode) == RERAISE) +#define IS_RETURN_OPCODE(opcode) \ + (opcode == RETURN_VALUE) + /* Flags used in the oparg for MAKE_FUNCTION */ #define MAKE_FUNCTION_DEFAULTS 0x01 diff --git a/Lib/test/test_code.py b/Lib/test/test_code.py index 2f459a46b5ad70..0c76d38feaae87 100644 --- a/Lib/test/test_code.py +++ b/Lib/test/test_code.py @@ -216,6 +216,10 @@ from test.support.bytecode_helper import instructions_with_positions from opcode import opmap, opname from _testcapi import code_offset_to_line +try: + import _testinternalcapi +except ModuleNotFoundError: + _testinternalcapi = None COPY_FREE_VARS = opmap['COPY_FREE_VARS'] @@ -425,6 +429,61 @@ def func(): with self.assertWarns(DeprecationWarning): func.__code__.co_lnotab + @unittest.skipIf(_testinternalcapi is None, '_testinternalcapi is missing') + def test_returns_only_none(self): + value = True + + def spam1(): + pass + def spam2(): + return + def spam3(): + return None + def spam4(): + if not value: + return + ... + def spam5(): + if not value: + return None + ... + lambda1 = (lambda: None) + for func in [ + spam1, + spam2, + spam3, + spam4, + spam5, + lambda1, + ]: + with self.subTest(func): + res = _testinternalcapi.code_returns_only_none(func.__code__) + self.assertTrue(res) + + def spam6(): + return True + def spam7(): + return value + def spam8(): + if value: + return None + return True + def spam9(): + if value: + return True + return None + lambda2 = (lambda: True) + for func in [ + spam6, + spam7, + spam8, + spam9, + lambda2, + ]: + with self.subTest(func): + res = _testinternalcapi.code_returns_only_none(func.__code__) + self.assertFalse(res) + def test_invalid_bytecode(self): def foo(): pass diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 0ef064fe80d173..4415a333452e29 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -945,6 +945,18 @@ iframe_getlasti(PyObject *self, PyObject *frame) return PyLong_FromLong(PyUnstable_InterpreterFrame_GetLasti(f)); } +static PyObject * +code_returns_only_none(PyObject *self, PyObject *arg) +{ + if (!PyCode_Check(arg)) { + PyErr_SetString(PyExc_TypeError, "argument must be a code object"); + return NULL; + } + PyCodeObject *code = (PyCodeObject *)arg; + int res = _PyCode_ReturnsOnlyNone(code); + return PyBool_FromLong(res); +} + static PyObject * get_co_framesize(PyObject *self, PyObject *arg) { @@ -2074,6 +2086,7 @@ static PyMethodDef module_functions[] = { {"iframe_getcode", iframe_getcode, METH_O, NULL}, {"iframe_getline", iframe_getline, METH_O, NULL}, {"iframe_getlasti", iframe_getlasti, METH_O, NULL}, + {"code_returns_only_none", code_returns_only_none, METH_O, NULL}, {"get_co_framesize", get_co_framesize, METH_O, NULL}, {"jit_enabled", jit_enabled, METH_NOARGS, NULL}, #ifdef _Py_TIER2 diff --git a/Objects/codeobject.c b/Objects/codeobject.c index 226c64f717dc82..bf24a4af445356 100644 --- a/Objects/codeobject.c +++ b/Objects/codeobject.c @@ -1689,6 +1689,49 @@ PyCode_GetFreevars(PyCodeObject *code) return _PyCode_GetFreevars(code); } + +/* Here "value" means a non-None value, since a bare return is identical + * to returning None explicitly. Likewise a missing return statement + * at the end of the function is turned into "return None". */ +int +_PyCode_ReturnsOnlyNone(PyCodeObject *co) +{ + // Look up None in co_consts. + Py_ssize_t nconsts = PyTuple_Size(co->co_consts); + int none_index = 0; + for (; none_index < nconsts; none_index++) { + if (PyTuple_GET_ITEM(co->co_consts, none_index) == Py_None) { + break; + } + } + if (none_index == nconsts) { + // None wasn't there, which means there was no implicit return, + // "return", or "return None". That means there must be + // an explicit return (non-None). + return 0; + } + + // Walk the bytecode, looking for RETURN_VALUE. + Py_ssize_t len = Py_SIZE(co); + for (int i = 0; i < len; i++) { + _Py_CODEUNIT inst = _Py_GetBaseCodeUnit(co, i); + if (IS_RETURN_OPCODE(inst.op.code)) { + assert(i != 0); + // Ignore it if it returns None. + _Py_CODEUNIT prev = _Py_GetBaseCodeUnit(co, i-1); + if (prev.op.code == LOAD_CONST) { + // We don't worry about EXTENDED_ARG for now. + if (prev.op.arg == none_index) { + continue; + } + } + return 0; + } + } + return 1; +} + + #ifdef _Py_TIER2 static void diff --git a/Python/flowgraph.c b/Python/flowgraph.c index 9e714bf6c4c54d..e0bb5615db3e0b 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -295,7 +295,7 @@ dump_instr(cfg_instr *i) static inline int basicblock_returns(const basicblock *b) { cfg_instr *last = basicblock_last_instr(b); - return last && last->i_opcode == RETURN_VALUE; + return last && IS_RETURN_OPCODE(last->i_opcode); } static void _______________________________________________ Python-checkins mailing list -- python-checkins@python.org To unsubscribe send an email to python-checkins-le...@python.org https://mail.python.org/mailman3/lists/python-checkins.python.org/ Member address: arch...@mail-archive.com