-
-
Notifications
You must be signed in to change notification settings - Fork 35.3k
gh-156664: Fix interaction between free variables and comprehensions #156691
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
829258e
65116d5
9c49003
c93c1d8
f2c0db4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| Fix scope analysis for a name bound in one inlined comprehension and used as | ||
| a free variable by a sibling comprehension or nested function. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -120,6 +120,7 @@ framelocalsproxy_getkeyindex(PyFrameObject *frame, PyObject *key, bool read, PyO | |
| } | ||
|
|
||
| bool found = false; | ||
| int write_fallback = -1; | ||
|
|
||
| // We do 2 loops here because it's highly possible the key is interned | ||
| // and we can do a pointer comparison. | ||
|
|
@@ -139,7 +140,12 @@ framelocalsproxy_getkeyindex(PyFrameObject *frame, PyObject *key, bool read, PyO | |
| } | ||
| } else { | ||
| if (!(_PyLocals_GetKind(co->co_localspluskinds, i) & CO_FAST_HIDDEN)) { | ||
| return i; | ||
| if (framelocalsproxy_hasval(frame->f_frame, co, i)) { | ||
| return i; | ||
| } | ||
| if (write_fallback < 0) { | ||
| write_fallback = i; | ||
| } | ||
| } | ||
| } | ||
| found = true; | ||
|
|
@@ -148,7 +154,7 @@ framelocalsproxy_getkeyindex(PyFrameObject *frame, PyObject *key, bool read, PyO | |
| if (found) { | ||
| // This is an attempt to read an unset local variable or | ||
| // write to a variable that is hidden from regular write operations | ||
| return -1; | ||
| return read ? -1 : write_fallback; | ||
| } | ||
| // This is unlikely, but we need to make sure. This means the key | ||
| // is not interned. | ||
|
|
@@ -177,13 +183,52 @@ framelocalsproxy_getkeyindex(PyFrameObject *frame, PyObject *key, bool read, PyO | |
| } | ||
| } else { | ||
| if (!(_PyLocals_GetKind(co->co_localspluskinds, i) & CO_FAST_HIDDEN)) { | ||
| return i; | ||
| if (framelocalsproxy_hasval(frame->f_frame, co, i)) { | ||
| return i; | ||
| } | ||
| if (write_fallback < 0) { | ||
| write_fallback = i; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return -1; | ||
| return read ? -1 : write_fallback; | ||
| } | ||
|
|
||
| static PyObject * | ||
| framelocalsproxy_snapshot(PyFrameObject *frame) | ||
| { | ||
| PyCodeObject *co = _PyFrame_GetCode(frame->f_frame); | ||
| PyObject *snapshot = PyDict_New(); | ||
| if (snapshot == NULL) { | ||
| return NULL; | ||
| } | ||
|
|
||
| for (int i = 0; i < co->co_nlocalsplus; i++) { | ||
| PyObject *value = framelocalsproxy_getval(frame->f_frame, co, i); | ||
| if (value == NULL) { | ||
| continue; | ||
| } | ||
| PyObject *name = PyTuple_GET_ITEM(co->co_localsplusnames, i); | ||
| // Match direct lookup by keeping the first live slot when a | ||
| // comprehension cell and a free variable have the same name. | ||
| if (PyDict_SetDefaultRef(snapshot, name, value, NULL) < 0) { | ||
| Py_DECREF(value); | ||
| Py_DECREF(snapshot); | ||
| return NULL; | ||
| } | ||
| Py_DECREF(value); | ||
| } | ||
|
|
||
| if (frame->f_extra_locals != NULL && | ||
| PyDict_Merge(snapshot, frame->f_extra_locals, 0) < 0) | ||
| { | ||
| Py_DECREF(snapshot); | ||
| return NULL; | ||
| } | ||
| return snapshot; | ||
| } | ||
|
|
||
| static PyObject * | ||
|
|
@@ -375,38 +420,12 @@ static PyObject * | |
| framelocalsproxy_keys(PyObject *self, PyObject *Py_UNUSED(ignored)) | ||
| { | ||
| PyFrameObject *frame = PyFrameLocalsProxyObject_CAST(self)->frame; | ||
| PyCodeObject *co = _PyFrame_GetCode(frame->f_frame); | ||
| PyObject *names = PyList_New(0); | ||
| if (names == NULL) { | ||
| PyObject *snapshot = framelocalsproxy_snapshot(frame); | ||
| if (snapshot == NULL) { | ||
| return NULL; | ||
| } | ||
|
|
||
| for (int i = 0; i < co->co_nlocalsplus; i++) { | ||
| if (framelocalsproxy_hasval(frame->f_frame, co, i)) { | ||
| PyObject *name = PyTuple_GET_ITEM(co->co_localsplusnames, i); | ||
| if (PyList_Append(names, name) < 0) { | ||
| Py_DECREF(names); | ||
| return NULL; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Iterate through the extra locals | ||
| if (frame->f_extra_locals) { | ||
| assert(PyDict_Check(frame->f_extra_locals)); | ||
|
|
||
| Py_ssize_t i = 0; | ||
| PyObject *key = NULL; | ||
| PyObject *value = NULL; | ||
|
|
||
| while (PyDict_Next(frame->f_extra_locals, &i, &key, &value)) { | ||
| if (PyList_Append(names, key) < 0) { | ||
| Py_DECREF(names); | ||
| return NULL; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| PyObject *names = PyDict_Keys(snapshot); | ||
| Py_DECREF(snapshot); | ||
| return names; | ||
| } | ||
|
|
||
|
|
@@ -584,107 +603,38 @@ static PyObject * | |
| framelocalsproxy_values(PyObject *self, PyObject *Py_UNUSED(ignored)) | ||
| { | ||
| PyFrameObject *frame = PyFrameLocalsProxyObject_CAST(self)->frame; | ||
| PyCodeObject *co = _PyFrame_GetCode(frame->f_frame); | ||
| PyObject *values = PyList_New(0); | ||
| if (values == NULL) { | ||
| PyObject *snapshot = framelocalsproxy_snapshot(frame); | ||
| if (snapshot == NULL) { | ||
| return NULL; | ||
| } | ||
|
|
||
| for (int i = 0; i < co->co_nlocalsplus; i++) { | ||
| PyObject *value = framelocalsproxy_getval(frame->f_frame, co, i); | ||
| if (value) { | ||
| if (PyList_Append(values, value) < 0) { | ||
| Py_DECREF(values); | ||
| Py_DECREF(value); | ||
| return NULL; | ||
| } | ||
| Py_DECREF(value); | ||
| } | ||
| } | ||
|
|
||
| // Iterate through the extra locals | ||
| if (frame->f_extra_locals) { | ||
| Py_ssize_t j = 0; | ||
| PyObject *key = NULL; | ||
| PyObject *value = NULL; | ||
| while (PyDict_Next(frame->f_extra_locals, &j, &key, &value)) { | ||
| if (PyList_Append(values, value) < 0) { | ||
| Py_DECREF(values); | ||
| return NULL; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| PyObject *values = PyDict_Values(snapshot); | ||
| Py_DECREF(snapshot); | ||
| return values; | ||
| } | ||
|
|
||
| static PyObject * | ||
| framelocalsproxy_items(PyObject *self, PyObject *Py_UNUSED(ignored)) | ||
| { | ||
| PyFrameObject *frame = PyFrameLocalsProxyObject_CAST(self)->frame; | ||
| PyCodeObject *co = _PyFrame_GetCode(frame->f_frame); | ||
| PyObject *items = PyList_New(0); | ||
| if (items == NULL) { | ||
| PyObject *snapshot = framelocalsproxy_snapshot(frame); | ||
| if (snapshot == NULL) { | ||
| return NULL; | ||
| } | ||
|
|
||
| for (int i = 0; i < co->co_nlocalsplus; i++) { | ||
| PyObject *name = PyTuple_GET_ITEM(co->co_localsplusnames, i); | ||
| PyObject *value = framelocalsproxy_getval(frame->f_frame, co, i); | ||
|
|
||
| if (value) { | ||
| PyObject *pair = _PyTuple_FromPairSteal(Py_NewRef(name), value); | ||
| if (pair == NULL) { | ||
| goto error; | ||
| } | ||
|
|
||
| if (_PyList_AppendTakeRef((PyListObject *)items, pair) < 0) { | ||
| goto error; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Iterate through the extra locals | ||
| if (frame->f_extra_locals) { | ||
| Py_ssize_t j = 0; | ||
| PyObject *key = NULL; | ||
| PyObject *value = NULL; | ||
| while (PyDict_Next(frame->f_extra_locals, &j, &key, &value)) { | ||
| PyObject *pair = _PyTuple_FromPair(key, value); | ||
| if (pair == NULL) { | ||
| goto error; | ||
| } | ||
|
|
||
| if (_PyList_AppendTakeRef((PyListObject *)items, pair) < 0) { | ||
| goto error; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| PyObject *items = PyDict_Items(snapshot); | ||
| Py_DECREF(snapshot); | ||
| return items; | ||
|
|
||
| error: | ||
| Py_DECREF(items); | ||
| return NULL; | ||
| } | ||
|
|
||
| static Py_ssize_t | ||
| framelocalsproxy_length(PyObject *self) | ||
| { | ||
| PyFrameObject *frame = PyFrameLocalsProxyObject_CAST(self)->frame; | ||
| PyCodeObject *co = _PyFrame_GetCode(frame->f_frame); | ||
| Py_ssize_t size = 0; | ||
|
|
||
| if (frame->f_extra_locals != NULL) { | ||
| assert(PyDict_Check(frame->f_extra_locals)); | ||
| size += PyDict_Size(frame->f_extra_locals); | ||
| } | ||
|
|
||
| for (int i = 0; i < co->co_nlocalsplus; i++) { | ||
| if (framelocalsproxy_hasval(frame->f_frame, co, i)) { | ||
| size++; | ||
| } | ||
| PyObject *snapshot = framelocalsproxy_snapshot(frame); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure how much performance of In matching If this matters, we could maintain a fast path when duplicate bindings are impossible, especially for length, which previously required no container allocation? |
||
| if (snapshot == NULL) { | ||
| return -1; | ||
| } | ||
| Py_ssize_t size = PyDict_GET_SIZE(snapshot); | ||
| Py_DECREF(snapshot); | ||
| return size; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comprehension's cell can be active but still empty before the first target assignment. In that state, this
hasval()check skips it and directs a proxy write into the enclosing free-variable slot:Main and
9c49003print7, whilef2c0db42prints42. The write during iterator advancement escapes the comprehension and changes the enclosing variable.