From daa2008113523b5a338570bcc1992df1baebb0bf Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Wed, 24 Dec 2025 10:34:40 +0800 Subject: [PATCH 1/2] added sync_fast_locals option to eval and exec --- .../pycore_global_objects_fini_generated.h | 1 + Include/internal/pycore_global_strings.h | 1 + .../internal/pycore_runtime_init_generated.h | 1 + .../internal/pycore_unicodeobject_generated.h | 4 ++ Python/bltinmodule.c | 21 +++++-- Python/ceval.c | 58 ++++++++++++++++++ Python/clinic/bltinmodule.c.h | 60 +++++++++++++------ 7 files changed, 124 insertions(+), 22 deletions(-) diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index e625bf2fef1912..89fa44212a363c 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -2080,6 +2080,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(sub_key)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(subcalls)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(symmetric_difference_update)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(sync_fast_locals)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(tabsize)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(tag)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(take_bytes)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 771f0f8cb4ad87..7637fc81b07f75 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -803,6 +803,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(sub_key) STRUCT_FOR_ID(subcalls) STRUCT_FOR_ID(symmetric_difference_update) + STRUCT_FOR_ID(sync_fast_locals) STRUCT_FOR_ID(tabsize) STRUCT_FOR_ID(tag) STRUCT_FOR_ID(take_bytes) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 499a2569b9a06c..05cf6434971f39 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -2078,6 +2078,7 @@ extern "C" { INIT_ID(sub_key), \ INIT_ID(subcalls), \ INIT_ID(symmetric_difference_update), \ + INIT_ID(sync_fast_locals), \ INIT_ID(tabsize), \ INIT_ID(tag), \ INIT_ID(take_bytes), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 1375f46018f943..dd0fa579bc4be2 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -2992,6 +2992,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(sync_fast_locals); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(tabsize); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index c2d780ac9b9270..d803d4bacd77ea 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -955,6 +955,8 @@ eval as builtin_eval / globals: object = None locals: object = None + * + sync_fast_locals: bool = False Evaluate the given source in the context of globals and locals. @@ -967,8 +969,8 @@ If only globals is given, locals defaults to it. static PyObject * builtin_eval_impl(PyObject *module, PyObject *source, PyObject *globals, - PyObject *locals) -/*[clinic end generated code: output=0a0824aa70093116 input=7c7bce5299a89062]*/ + PyObject *locals, int sync_fast_locals) +/*[clinic end generated code: output=a573401639e51347 input=440105eb08930503]*/ { PyThreadState *tstate = _PyThreadState_GET(); PyObject *result = NULL, *source_copy; @@ -1037,6 +1039,10 @@ builtin_eval_impl(PyObject *module, PyObject *source, PyObject *globals, "code object passed to eval() may not contain free variables"); goto error; } + if (!sync_fast_locals && ((PyCodeObject *)source)->co_flags & CO_OPTIMIZED) { + Py_DECREF(locals); + locals = NULL; + } result = PyEval_EvalCode(source, globals, locals); } else { @@ -1078,6 +1084,7 @@ exec as builtin_exec locals: object = None * closure: object(c_default="NULL") = None + sync_fast_locals: bool = False Execute the given source in the context of globals and locals. @@ -1092,8 +1099,8 @@ when source is a code object requiring exactly that many cellvars. static PyObject * builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals, - PyObject *locals, PyObject *closure) -/*[clinic end generated code: output=7579eb4e7646743d input=25e989b6d87a3a21]*/ + PyObject *locals, PyObject *closure, int sync_fast_locals) +/*[clinic end generated code: output=ceab303bd7575dcf input=3a4103a242b26356]*/ { PyThreadState *tstate = _PyThreadState_GET(); PyObject *v; @@ -1189,6 +1196,10 @@ builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals, goto error; } + if (!sync_fast_locals && ((PyCodeObject *)source)->co_flags & CO_OPTIMIZED) { + Py_DECREF(locals); + locals = NULL; + } if (!closure) { v = PyEval_EvalCode(source, globals, locals); } else { @@ -1225,7 +1236,7 @@ builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals, if (v == NULL) goto error; Py_DECREF(globals); - Py_DECREF(locals); + Py_XDECREF(locals); Py_DECREF(v); Py_RETURN_NONE; diff --git a/Python/ceval.c b/Python/ceval.c index 924afaa97443cb..7732fe8a6e2098 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1529,6 +1529,46 @@ typedef struct { _PyStackRef stack[1]; } _PyEntryFrame; +static int +_PyEval_SyncMappingToFast(_PyInterpreterFrame *frame) +{ + PyObject *mapping = frame->f_locals; + PyCodeObject *co = _PyFrame_GetCode(frame); + PyObject *names = co->co_localsplusnames; + + for (int i = 0; i < co->co_nlocalsplus; i++) { + PyObject *name = PyTuple_GET_ITEM(names, i); + PyObject *value = PyObject_GetItem(mapping, name); + if (value != NULL) { + frame->localsplus[i] = PyStackRef_FromPyObjectSteal(value); + } + else { + PyErr_Clear(); + } + } + return 0; +} + +static int +_PyEval_SyncFastToMapping(_PyInterpreterFrame *frame) +{ + PyObject *mapping = frame->f_locals; + PyCodeObject *co = _PyFrame_GetCode(frame); + PyObject *names = co->co_localsplusnames; + + for (int i = 0; i < co->co_nlocalsplus; i++) { + _PyStackRef sref = frame->localsplus[i]; + if (!PyStackRef_IsNull(sref)) { + PyObject *name = PyTuple_GET_ITEM(names, i); + PyObject *obj = PyStackRef_AsPyObjectSteal(sref); + if (PyObject_SetItem(mapping, name, obj) < 0) { + PyErr_Clear(); + } + } + } + return 0; +} + PyObject* _Py_HOT_FUNCTION DONT_SLP_VECTORIZE _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int throwflag) { @@ -1591,6 +1631,15 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int frame->previous = &entry.frame; tstate->current_frame = frame; entry.frame.localsplus[0] = PyStackRef_NULL; + PyCodeObject *co = _PyFrame_GetCode(frame); + if ((co->co_flags & CO_OPTIMIZED) && + frame->f_locals != NULL && + frame->f_locals != frame->f_globals) + { + if (_PyEval_SyncMappingToFast(frame) < 0) { + goto early_exit; + } + } #ifdef _Py_TIER2 if (tstate->current_executor != NULL) { entry.frame.localsplus[0] = PyStackRef_FromPyObjectNew(tstate->current_executor); @@ -2377,6 +2426,15 @@ clear_gen_frame(PyThreadState *tstate, _PyInterpreterFrame * frame) void _PyEval_FrameClearAndPop(PyThreadState *tstate, _PyInterpreterFrame * frame) { + PyCodeObject *co = _PyFrame_GetCode(frame); + if ((co->co_flags & CO_OPTIMIZED) && + frame->f_locals != NULL && + frame->f_locals != frame->f_globals) + { + if (_PyEval_SyncFastToMapping(frame) < 0) { + PyErr_WriteUnraisable(frame->f_locals); + } + } // Update last_profiled_frame for remote profiler frame caching. // By this point, tstate->current_frame is already set to the parent frame. // Only update if we're popping the exact frame that was last profiled. diff --git a/Python/clinic/bltinmodule.c.h b/Python/clinic/bltinmodule.c.h index f08e5847abe32a..7b9e438a2fe822 100644 --- a/Python/clinic/bltinmodule.c.h +++ b/Python/clinic/bltinmodule.c.h @@ -412,7 +412,8 @@ builtin_divmod(PyObject *module, PyObject *const *args, Py_ssize_t nargs) } PyDoc_STRVAR(builtin_eval__doc__, -"eval($module, source, /, globals=None, locals=None)\n" +"eval($module, source, /, globals=None, locals=None, *,\n" +" sync_fast_locals=False)\n" "--\n" "\n" "Evaluate the given source in the context of globals and locals.\n" @@ -428,7 +429,7 @@ PyDoc_STRVAR(builtin_eval__doc__, static PyObject * builtin_eval_impl(PyObject *module, PyObject *source, PyObject *globals, - PyObject *locals); + PyObject *locals, int sync_fast_locals); static PyObject * builtin_eval(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) @@ -436,7 +437,7 @@ builtin_eval(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - #define NUM_KEYWORDS 2 + #define NUM_KEYWORDS 3 static struct { PyGC_Head _this_is_not_used; PyObject_VAR_HEAD @@ -445,7 +446,7 @@ builtin_eval(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject } _kwtuple = { .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) .ob_hash = -1, - .ob_item = { &_Py_ID(globals), &_Py_ID(locals), }, + .ob_item = { &_Py_ID(globals), &_Py_ID(locals), &_Py_ID(sync_fast_locals), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -454,18 +455,19 @@ builtin_eval(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject # define KWTUPLE NULL #endif // !Py_BUILD_CORE - static const char * const _keywords[] = {"", "globals", "locals", NULL}; + static const char * const _keywords[] = {"", "globals", "locals", "sync_fast_locals", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, .fname = "eval", .kwtuple = KWTUPLE, }; #undef KWTUPLE - PyObject *argsbuf[3]; + PyObject *argsbuf[4]; Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; PyObject *source; PyObject *globals = Py_None; PyObject *locals = Py_None; + int sync_fast_locals = 0; args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, /*minpos*/ 1, /*maxpos*/ 3, /*minkw*/ 0, /*varpos*/ 0, argsbuf); @@ -482,16 +484,30 @@ builtin_eval(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject goto skip_optional_pos; } } - locals = args[2]; + if (args[2]) { + locals = args[2]; + if (!--noptargs) { + goto skip_optional_pos; + } + } skip_optional_pos: - return_value = builtin_eval_impl(module, source, globals, locals); + if (!noptargs) { + goto skip_optional_kwonly; + } + sync_fast_locals = PyObject_IsTrue(args[3]); + if (sync_fast_locals < 0) { + goto exit; + } +skip_optional_kwonly: + return_value = builtin_eval_impl(module, source, globals, locals, sync_fast_locals); exit: return return_value; } PyDoc_STRVAR(builtin_exec__doc__, -"exec($module, source, /, globals=None, locals=None, *, closure=None)\n" +"exec($module, source, /, globals=None, locals=None, *, closure=None,\n" +" sync_fast_locals=False)\n" "--\n" "\n" "Execute the given source in the context of globals and locals.\n" @@ -509,7 +525,7 @@ PyDoc_STRVAR(builtin_exec__doc__, static PyObject * builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals, - PyObject *locals, PyObject *closure); + PyObject *locals, PyObject *closure, int sync_fast_locals); static PyObject * builtin_exec(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) @@ -517,7 +533,7 @@ builtin_exec(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - #define NUM_KEYWORDS 3 + #define NUM_KEYWORDS 4 static struct { PyGC_Head _this_is_not_used; PyObject_VAR_HEAD @@ -526,7 +542,7 @@ builtin_exec(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject } _kwtuple = { .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) .ob_hash = -1, - .ob_item = { &_Py_ID(globals), &_Py_ID(locals), &_Py_ID(closure), }, + .ob_item = { &_Py_ID(globals), &_Py_ID(locals), &_Py_ID(closure), &_Py_ID(sync_fast_locals), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -535,19 +551,20 @@ builtin_exec(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject # define KWTUPLE NULL #endif // !Py_BUILD_CORE - static const char * const _keywords[] = {"", "globals", "locals", "closure", NULL}; + static const char * const _keywords[] = {"", "globals", "locals", "closure", "sync_fast_locals", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, .fname = "exec", .kwtuple = KWTUPLE, }; #undef KWTUPLE - PyObject *argsbuf[4]; + PyObject *argsbuf[5]; Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; PyObject *source; PyObject *globals = Py_None; PyObject *locals = Py_None; PyObject *closure = NULL; + int sync_fast_locals = 0; args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, /*minpos*/ 1, /*maxpos*/ 3, /*minkw*/ 0, /*varpos*/ 0, argsbuf); @@ -574,9 +591,18 @@ builtin_exec(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject if (!noptargs) { goto skip_optional_kwonly; } - closure = args[3]; + if (args[3]) { + closure = args[3]; + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + sync_fast_locals = PyObject_IsTrue(args[4]); + if (sync_fast_locals < 0) { + goto exit; + } skip_optional_kwonly: - return_value = builtin_exec_impl(module, source, globals, locals, closure); + return_value = builtin_exec_impl(module, source, globals, locals, closure, sync_fast_locals); exit: return return_value; @@ -1285,4 +1311,4 @@ builtin_issubclass(PyObject *module, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } -/*[clinic end generated code: output=06500bcc9a341e68 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=3870be11257135c3 input=a9049054013a1b77]*/ From 1137510984736c6678d9276cf4efe4cdc8261498 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Wed, 24 Dec 2025 18:06:08 +0800 Subject: [PATCH 2/2] added unit tests --- Lib/test/test_builtin.py | 17 ++++++++++++++++- Python/bltinmodule.c | 2 +- Python/ceval.c | 29 +++++++++++------------------ 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index ce60a5d095dd52..1bdc7ca62510a6 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -1111,7 +1111,22 @@ def test_exec_filter_syntax_warnings_by_module(self): self.assertEqual(wm.filename, '') self.assertIs(wm.category, SyntaxWarning) - + def test_eval_exec_sync_fast_locals(self): + def func_assign(): + a = 1 + + def func_read(): + b = a + 1 + + for executor in eval, exec: + with self.subTest(executor=executor.__name__): + ns = {} + executor(func_assign.__code__, {}, ns, sync_fast_locals=True) + self.assertEqual(ns, {'a': 1}) + ns = {'a': 1} + executor(func_read.__code__, {}, ns, sync_fast_locals=True) + self.assertEqual(ns, {'a': 1, 'b': 2}) + def test_filter(self): self.assertEqual(list(filter(lambda c: 'a' <= c <= 'z', 'Hello World')), list('elloorld')) self.assertEqual(list(filter(None, [1, 'hello', [], [3], '', None, 9, 0])), [1, 'hello', [3], 9]) diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index d803d4bacd77ea..f383d0bbf84966 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -1041,7 +1041,7 @@ builtin_eval_impl(PyObject *module, PyObject *source, PyObject *globals, } if (!sync_fast_locals && ((PyCodeObject *)source)->co_flags & CO_OPTIMIZED) { Py_DECREF(locals); - locals = NULL; + locals = globals; } result = PyEval_EvalCode(source, globals, locals); } diff --git a/Python/ceval.c b/Python/ceval.c index 7732fe8a6e2098..16df5e39567eaa 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -1530,7 +1530,7 @@ typedef struct { } _PyEntryFrame; static int -_PyEval_SyncMappingToFast(_PyInterpreterFrame *frame) +_PyEval_SyncLocalsToFast(_PyInterpreterFrame *frame) { PyObject *mapping = frame->f_locals; PyCodeObject *co = _PyFrame_GetCode(frame); @@ -1543,14 +1543,14 @@ _PyEval_SyncMappingToFast(_PyInterpreterFrame *frame) frame->localsplus[i] = PyStackRef_FromPyObjectSteal(value); } else { - PyErr_Clear(); + return -1; } } return 0; } static int -_PyEval_SyncFastToMapping(_PyInterpreterFrame *frame) +_PyEval_SyncFastToLocals(_PyInterpreterFrame *frame) { PyObject *mapping = frame->f_locals; PyCodeObject *co = _PyFrame_GetCode(frame); @@ -1562,7 +1562,7 @@ _PyEval_SyncFastToMapping(_PyInterpreterFrame *frame) PyObject *name = PyTuple_GET_ITEM(names, i); PyObject *obj = PyStackRef_AsPyObjectSteal(sref); if (PyObject_SetItem(mapping, name, obj) < 0) { - PyErr_Clear(); + return -1; } } } @@ -1632,13 +1632,9 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int tstate->current_frame = frame; entry.frame.localsplus[0] = PyStackRef_NULL; PyCodeObject *co = _PyFrame_GetCode(frame); - if ((co->co_flags & CO_OPTIMIZED) && - frame->f_locals != NULL && - frame->f_locals != frame->f_globals) - { - if (_PyEval_SyncMappingToFast(frame) < 0) { - goto early_exit; - } + if ((co->co_flags & CO_OPTIMIZED) && frame->f_locals != NULL && + frame->f_locals != frame->f_globals && _PyEval_SyncLocalsToFast(frame) < 0) { + goto early_exit; } #ifdef _Py_TIER2 if (tstate->current_executor != NULL) { @@ -2427,13 +2423,10 @@ void _PyEval_FrameClearAndPop(PyThreadState *tstate, _PyInterpreterFrame * frame) { PyCodeObject *co = _PyFrame_GetCode(frame); - if ((co->co_flags & CO_OPTIMIZED) && - frame->f_locals != NULL && - frame->f_locals != frame->f_globals) - { - if (_PyEval_SyncFastToMapping(frame) < 0) { - PyErr_WriteUnraisable(frame->f_locals); - } + if ((co->co_flags & CO_OPTIMIZED) && frame->f_locals != NULL && + frame->f_locals != frame->f_globals && _PyEval_SyncFastToLocals(frame) < 0) { + /* Swallow the error while the frame is in a teardown state */ + PyErr_WriteUnraisable(frame->f_locals); } // Update last_profiled_frame for remote profiler frame caching. // By this point, tstate->current_frame is already set to the parent frame.