Skip to content

Commit c39bc81

Browse files
[3.14] gh-132775: Unrevert "Add _PyCode_VerifyStateless()" (gh-133625)
This reverts commit 3c73cf5 (gh-133497), which itself reverted the original commit d270bb5 (gh-133221). We reverted the original change due to failing android tests. The checks in _PyCode_CheckNoInternalState() were too strict, so we've relaxed them.
1 parent 54c3aa1 commit c39bc81

File tree

8 files changed

+358
-37
lines changed

8 files changed

+358
-37
lines changed

Include/internal/pycore_code.h

+41
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,47 @@ PyAPI_FUNC(int) _PyCode_SetUnboundVarCounts(
614614
PyObject *globalsns,
615615
PyObject *builtinsns);
616616

617+
618+
/* "Stateless" code is a function or code object which does not rely on
619+
* external state or internal state. It may rely on arguments and
620+
* builtins, but not globals or a closure. Thus it does not rely
621+
* on __globals__ or __closure__, and a stateless function
622+
* is equivalent to its code object.
623+
*
624+
* Stateless code also does not keep any persistent state
625+
* of its own, so it can't have any executors, monitoring,
626+
* instrumentation, or "extras" (i.e. co_extra).
627+
*
628+
* Stateless code may create nested functions, including closures.
629+
* However, nested functions must themselves be stateless, except they
630+
* *can* close on the enclosing locals.
631+
*
632+
* Stateless code may return any value, including nested functions and closures.
633+
*
634+
* Stateless code that takes no arguments and doesn't return anything
635+
* may be treated like a script.
636+
*
637+
* We consider stateless code to be "portable" if it does not return
638+
* any object that holds a reference to any of the code's locals. Thus
639+
* generators and coroutines are not portable. Likewise a function
640+
* that returns a closure is not portable. The concept of
641+
* portability is useful in cases where the code is run
642+
* in a different execution context than where
643+
* the return value will be used. */
644+
645+
PyAPI_FUNC(int) _PyCode_CheckNoInternalState(PyCodeObject *, const char **);
646+
PyAPI_FUNC(int) _PyCode_CheckNoExternalState(
647+
PyCodeObject *,
648+
_PyCode_var_counts_t *,
649+
const char **);
650+
PyAPI_FUNC(int) _PyCode_VerifyStateless(
651+
PyThreadState *,
652+
PyCodeObject *,
653+
PyObject *globalnames,
654+
PyObject *globalsns,
655+
PyObject *builtinsns);
656+
657+
PyAPI_FUNC(int) _PyCode_CheckPureFunction(PyCodeObject *, const char **);
617658
PyAPI_FUNC(int) _PyCode_ReturnsOnlyNone(PyCodeObject *);
618659

619660

Include/internal/pycore_function.h

+7
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ PyFunctionObject *_PyFunction_LookupByVersion(uint32_t version, PyObject **p_cod
3535
extern PyObject *_Py_set_function_type_params(
3636
PyThreadState* unused, PyObject *func, PyObject *type_params);
3737

38+
39+
/* See pycore_code.h for explanation about what "stateless" means. */
40+
41+
PyAPI_FUNC(int)
42+
_PyFunction_VerifyStateless(PyThreadState *, PyObject *);
43+
44+
3845
#ifdef __cplusplus
3946
}
4047
#endif

Include/internal/pycore_opcode_utils.h

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ extern "C" {
5656

5757
#define IS_RETURN_OPCODE(opcode) \
5858
(opcode == RETURN_VALUE)
59+
#define IS_RAISE_OPCODE(opcode) \
60+
(opcode == RAISE_VARARGS || opcode == RERAISE)
5961

6062

6163
/* Flags used in the oparg for MAKE_FUNCTION */

Lib/test/_code_definitions.py

+26
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,32 @@ def ham_C_closure(z):
178178
*NESTED_FUNCTIONS,
179179
]
180180

181+
STATELESS_FUNCTIONS = [
182+
spam,
183+
spam_minimal,
184+
spam_with_builtins,
185+
spam_args_attrs_and_builtins,
186+
spam_returns_arg,
187+
spam_annotated,
188+
spam_with_inner_not_closure,
189+
spam_with_inner_closure,
190+
spam_N,
191+
spam_C,
192+
spam_NN,
193+
spam_NC,
194+
spam_CN,
195+
spam_CC,
196+
eggs_nested,
197+
eggs_nested_N,
198+
ham_nested,
199+
ham_C_nested
200+
]
201+
STATELESS_CODE = [
202+
*STATELESS_FUNCTIONS,
203+
spam_with_globals_and_builtins,
204+
spam_full,
205+
]
206+
181207

182208
# generators
183209

Lib/test/test_code.py

+34-20
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@
220220
import _testinternalcapi
221221
except ModuleNotFoundError:
222222
_testinternalcapi = None
223+
import test._code_definitions as defs
223224

224225
COPY_FREE_VARS = opmap['COPY_FREE_VARS']
225226

@@ -671,7 +672,6 @@ def test_local_kinds(self):
671672
VARARGS = CO_FAST_LOCAL | CO_FAST_ARG_VAR | CO_FAST_ARG_POS
672673
VARKWARGS = CO_FAST_LOCAL | CO_FAST_ARG_VAR | CO_FAST_ARG_KW
673674

674-
import test._code_definitions as defs
675675
funcs = {
676676
defs.spam_minimal: {},
677677
defs.spam_with_builtins: {
@@ -897,7 +897,6 @@ def new_var_counts(*,
897897
},
898898
}
899899

900-
import test._code_definitions as defs
901900
funcs = {
902901
defs.spam_minimal: new_var_counts(),
903902
defs.spam_with_builtins: new_var_counts(
@@ -1025,55 +1024,70 @@ def new_var_counts(*,
10251024
counts = _testinternalcapi.get_code_var_counts(func.__code__)
10261025
self.assertEqual(counts, expected)
10271026

1028-
def func_with_globals_and_builtins():
1029-
mod1 = _testinternalcapi
1030-
mod2 = dis
1031-
mods = (mod1, mod2)
1032-
checks = tuple(callable(m) for m in mods)
1033-
return callable(mod2), tuple(mods), list(mods), checks
1034-
1035-
func = func_with_globals_and_builtins
1027+
func = defs.spam_with_globals_and_builtins
10361028
with self.subTest(f'{func} code'):
10371029
expected = new_var_counts(
1038-
purelocals=4,
1039-
globalvars=5,
1030+
purelocals=5,
1031+
globalvars=6,
10401032
)
10411033
counts = _testinternalcapi.get_code_var_counts(func.__code__)
10421034
self.assertEqual(counts, expected)
10431035

10441036
with self.subTest(f'{func} with own globals and builtins'):
10451037
expected = new_var_counts(
1046-
purelocals=4,
1047-
globalvars=(2, 3),
1038+
purelocals=5,
1039+
globalvars=(2, 4),
10481040
)
10491041
counts = _testinternalcapi.get_code_var_counts(func)
10501042
self.assertEqual(counts, expected)
10511043

10521044
with self.subTest(f'{func} without globals'):
10531045
expected = new_var_counts(
1054-
purelocals=4,
1055-
globalvars=(0, 3, 2),
1046+
purelocals=5,
1047+
globalvars=(0, 4, 2),
10561048
)
10571049
counts = _testinternalcapi.get_code_var_counts(func, globalsns={})
10581050
self.assertEqual(counts, expected)
10591051

10601052
with self.subTest(f'{func} without both'):
10611053
expected = new_var_counts(
1062-
purelocals=4,
1063-
globalvars=5,
1054+
purelocals=5,
1055+
globalvars=6,
10641056
)
10651057
counts = _testinternalcapi.get_code_var_counts(func, globalsns={},
10661058
builtinsns={})
10671059
self.assertEqual(counts, expected)
10681060

10691061
with self.subTest(f'{func} without builtins'):
10701062
expected = new_var_counts(
1071-
purelocals=4,
1072-
globalvars=(2, 0, 3),
1063+
purelocals=5,
1064+
globalvars=(2, 0, 4),
10731065
)
10741066
counts = _testinternalcapi.get_code_var_counts(func, builtinsns={})
10751067
self.assertEqual(counts, expected)
10761068

1069+
@unittest.skipIf(_testinternalcapi is None, "missing _testinternalcapi")
1070+
def test_stateless(self):
1071+
self.maxDiff = None
1072+
1073+
for func in defs.STATELESS_CODE:
1074+
with self.subTest((func, '(code)')):
1075+
_testinternalcapi.verify_stateless_code(func.__code__)
1076+
for func in defs.STATELESS_FUNCTIONS:
1077+
with self.subTest((func, '(func)')):
1078+
_testinternalcapi.verify_stateless_code(func)
1079+
1080+
for func in defs.FUNCTIONS:
1081+
if func not in defs.STATELESS_CODE:
1082+
with self.subTest((func, '(code)')):
1083+
with self.assertRaises(Exception):
1084+
_testinternalcapi.verify_stateless_code(func.__code__)
1085+
1086+
if func not in defs.STATELESS_FUNCTIONS:
1087+
with self.subTest((func, '(func)')):
1088+
with self.assertRaises(Exception):
1089+
_testinternalcapi.verify_stateless_code(func)
1090+
10771091

10781092
def isinterned(s):
10791093
return s is sys.intern(('_' + s + '_')[1:-1])

Modules/_testinternalcapi.c

+43
Original file line numberDiff line numberDiff line change
@@ -1165,6 +1165,47 @@ get_code_var_counts(PyObject *self, PyObject *_args, PyObject *_kwargs)
11651165
return NULL;
11661166
}
11671167

1168+
static PyObject *
1169+
verify_stateless_code(PyObject *self, PyObject *args, PyObject *kwargs)
1170+
{
1171+
PyThreadState *tstate = _PyThreadState_GET();
1172+
PyObject *codearg;
1173+
PyObject *globalnames = NULL;
1174+
PyObject *globalsns = NULL;
1175+
PyObject *builtinsns = NULL;
1176+
static char *kwlist[] = {"code", "globalnames",
1177+
"globalsns", "builtinsns", NULL};
1178+
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
1179+
"O|O!O!O!:get_code_var_counts", kwlist,
1180+
&codearg, &PySet_Type, &globalnames,
1181+
&PyDict_Type, &globalsns, &PyDict_Type, &builtinsns))
1182+
{
1183+
return NULL;
1184+
}
1185+
if (PyFunction_Check(codearg)) {
1186+
if (globalsns == NULL) {
1187+
globalsns = PyFunction_GET_GLOBALS(codearg);
1188+
}
1189+
if (builtinsns == NULL) {
1190+
builtinsns = PyFunction_GET_BUILTINS(codearg);
1191+
}
1192+
codearg = PyFunction_GET_CODE(codearg);
1193+
}
1194+
else if (!PyCode_Check(codearg)) {
1195+
PyErr_SetString(PyExc_TypeError,
1196+
"argument must be a code object or a function");
1197+
return NULL;
1198+
}
1199+
PyCodeObject *code = (PyCodeObject *)codearg;
1200+
1201+
if (_PyCode_VerifyStateless(
1202+
tstate, code, globalnames, globalsns, builtinsns) < 0)
1203+
{
1204+
return NULL;
1205+
}
1206+
Py_RETURN_NONE;
1207+
}
1208+
11681209
#ifdef _Py_TIER2
11691210

11701211
static PyObject *
@@ -2292,6 +2333,8 @@ static PyMethodDef module_functions[] = {
22922333
{"get_co_localskinds", get_co_localskinds, METH_O, NULL},
22932334
{"get_code_var_counts", _PyCFunction_CAST(get_code_var_counts),
22942335
METH_VARARGS | METH_KEYWORDS, NULL},
2336+
{"verify_stateless_code", _PyCFunction_CAST(verify_stateless_code),
2337+
METH_VARARGS | METH_KEYWORDS, NULL},
22952338
#ifdef _Py_TIER2
22962339
{"add_executor_dependency", add_executor_dependency, METH_VARARGS, NULL},
22972340
{"invalidate_executors", invalidate_executors, METH_O, NULL},

0 commit comments

Comments
 (0)