Skip to content

Commit c7065ce

Browse files
authored
GH-93143: Don't turn LOAD_FAST into LOAD_FAST_CHECK (GH-99075)
1 parent e56e33d commit c7065ce

File tree

3 files changed

+131
-58
lines changed

3 files changed

+131
-58
lines changed

Lib/test/test_peepholer.py

Lines changed: 91 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -815,15 +815,15 @@ def f():
815815
self.assertInBytecode(f, 'LOAD_FAST_CHECK', "a73")
816816
self.assertInBytecode(f, 'LOAD_FAST', "a73")
817817

818-
def test_setting_lineno_adds_check(self):
819-
code = textwrap.dedent("""\
818+
def test_setting_lineno_no_undefined(self):
819+
code = textwrap.dedent(f"""\
820820
def f():
821-
x = 2
822-
L = 3
823-
L = 4
821+
x = y = 2
822+
if not x:
823+
return 4
824824
for i in range(55):
825825
x + 6
826-
del x
826+
L = 7
827827
L = 8
828828
L = 9
829829
L = 10
@@ -832,15 +832,88 @@ def f():
832832
exec(code, ns)
833833
f = ns['f']
834834
self.assertInBytecode(f, "LOAD_FAST")
835+
self.assertNotInBytecode(f, "LOAD_FAST_CHECK")
836+
co_code = f.__code__.co_code
835837
def trace(frame, event, arg):
836838
if event == 'line' and frame.f_lineno == 9:
837-
frame.f_lineno = 2
839+
frame.f_lineno = 3
838840
sys.settrace(None)
839841
return None
840842
return trace
841843
sys.settrace(trace)
842-
f()
843-
self.assertNotInBytecode(f, "LOAD_FAST")
844+
result = f()
845+
self.assertIsNone(result)
846+
self.assertInBytecode(f, "LOAD_FAST")
847+
self.assertNotInBytecode(f, "LOAD_FAST_CHECK")
848+
self.assertEqual(f.__code__.co_code, co_code)
849+
850+
def test_setting_lineno_one_undefined(self):
851+
code = textwrap.dedent(f"""\
852+
def f():
853+
x = y = 2
854+
if not x:
855+
return 4
856+
for i in range(55):
857+
x + 6
858+
del x
859+
L = 8
860+
L = 9
861+
L = 10
862+
""")
863+
ns = {}
864+
exec(code, ns)
865+
f = ns['f']
866+
self.assertInBytecode(f, "LOAD_FAST")
867+
self.assertNotInBytecode(f, "LOAD_FAST_CHECK")
868+
co_code = f.__code__.co_code
869+
def trace(frame, event, arg):
870+
if event == 'line' and frame.f_lineno == 9:
871+
frame.f_lineno = 3
872+
sys.settrace(None)
873+
return None
874+
return trace
875+
e = r"assigning None to 1 unbound local"
876+
with self.assertWarnsRegex(RuntimeWarning, e):
877+
sys.settrace(trace)
878+
result = f()
879+
self.assertEqual(result, 4)
880+
self.assertInBytecode(f, "LOAD_FAST")
881+
self.assertNotInBytecode(f, "LOAD_FAST_CHECK")
882+
self.assertEqual(f.__code__.co_code, co_code)
883+
884+
def test_setting_lineno_two_undefined(self):
885+
code = textwrap.dedent(f"""\
886+
def f():
887+
x = y = 2
888+
if not x:
889+
return 4
890+
for i in range(55):
891+
x + 6
892+
del x, y
893+
L = 8
894+
L = 9
895+
L = 10
896+
""")
897+
ns = {}
898+
exec(code, ns)
899+
f = ns['f']
900+
self.assertInBytecode(f, "LOAD_FAST")
901+
self.assertNotInBytecode(f, "LOAD_FAST_CHECK")
902+
co_code = f.__code__.co_code
903+
def trace(frame, event, arg):
904+
if event == 'line' and frame.f_lineno == 9:
905+
frame.f_lineno = 3
906+
sys.settrace(None)
907+
return None
908+
return trace
909+
e = r"assigning None to 2 unbound locals"
910+
with self.assertWarnsRegex(RuntimeWarning, e):
911+
sys.settrace(trace)
912+
result = f()
913+
self.assertEqual(result, 4)
914+
self.assertInBytecode(f, "LOAD_FAST")
915+
self.assertNotInBytecode(f, "LOAD_FAST_CHECK")
916+
self.assertEqual(f.__code__.co_code, co_code)
844917

845918
def make_function_with_no_checks(self):
846919
code = textwrap.dedent("""\
@@ -860,18 +933,22 @@ def f():
860933
self.assertNotInBytecode(f, "LOAD_FAST_CHECK")
861934
return f
862935

863-
def test_deleting_local_adds_check(self):
936+
def test_deleting_local_warns_and_assigns_none(self):
864937
f = self.make_function_with_no_checks()
938+
co_code = f.__code__.co_code
865939
def trace(frame, event, arg):
866940
if event == 'line' and frame.f_lineno == 4:
867941
del frame.f_locals["x"]
868942
sys.settrace(None)
869943
return None
870944
return trace
871-
sys.settrace(trace)
872-
f()
873-
self.assertNotInBytecode(f, "LOAD_FAST")
874-
self.assertInBytecode(f, "LOAD_FAST_CHECK")
945+
e = r"assigning None to unbound local 'x'"
946+
with self.assertWarnsRegex(RuntimeWarning, e):
947+
sys.settrace(trace)
948+
f()
949+
self.assertInBytecode(f, "LOAD_FAST")
950+
self.assertNotInBytecode(f, "LOAD_FAST_CHECK")
951+
self.assertEqual(f.__code__.co_code, co_code)
875952

876953
def test_modifying_local_does_not_add_check(self):
877954
f = self.make_function_with_no_checks()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Rather than changing :attr:`~types.CodeType.co_code`, the interpreter will
2+
now display a :exc:`RuntimeWarning` and assign :const:`None` to any fast
3+
locals that are left unbound after jumps or :keyword:`del`
4+
statements executed while tracing.

Objects/frameobject.c

Lines changed: 36 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -620,42 +620,6 @@ _PyFrame_GetState(PyFrameObject *frame)
620620
Py_UNREACHABLE();
621621
}
622622

623-
static void
624-
add_load_fast_null_checks(PyCodeObject *co)
625-
{
626-
int changed = 0;
627-
_Py_CODEUNIT *instructions = _PyCode_CODE(co);
628-
for (Py_ssize_t i = 0; i < Py_SIZE(co); i++) {
629-
int opcode = _Py_OPCODE(instructions[i]);
630-
switch (opcode) {
631-
case LOAD_FAST:
632-
case LOAD_FAST__LOAD_FAST:
633-
case LOAD_FAST__LOAD_CONST:
634-
changed = 1;
635-
_Py_SET_OPCODE(instructions[i], LOAD_FAST_CHECK);
636-
break;
637-
case LOAD_CONST__LOAD_FAST:
638-
changed = 1;
639-
_Py_SET_OPCODE(instructions[i], LOAD_CONST);
640-
break;
641-
case STORE_FAST__LOAD_FAST:
642-
changed = 1;
643-
_Py_SET_OPCODE(instructions[i], STORE_FAST);
644-
break;
645-
}
646-
i += _PyOpcode_Caches[_PyOpcode_Deopt[opcode]];
647-
}
648-
if (changed && co->_co_cached != NULL) {
649-
// invalidate cached co_code object
650-
Py_CLEAR(co->_co_cached->_co_code);
651-
Py_CLEAR(co->_co_cached->_co_cellvars);
652-
Py_CLEAR(co->_co_cached->_co_freevars);
653-
Py_CLEAR(co->_co_cached->_co_varnames);
654-
PyMem_Free(co->_co_cached);
655-
co->_co_cached = NULL;
656-
}
657-
}
658-
659623
/* Setter for f_lineno - you can set f_lineno from within a trace function in
660624
* order to jump to a given line of code, subject to some restrictions. Most
661625
* lines are OK to jump to because they don't make any assumptions about the
@@ -745,8 +709,6 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno, void *Py_UNUSED(ignore
745709
return -1;
746710
}
747711

748-
add_load_fast_null_checks(f->f_frame->f_code);
749-
750712
/* PyCode_NewWithPosOnlyArgs limits co_code to be under INT_MAX so this
751713
* should never overflow. */
752714
int len = (int)Py_SIZE(f->f_frame->f_code);
@@ -805,6 +767,31 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno, void *Py_UNUSED(ignore
805767
PyErr_SetString(PyExc_ValueError, msg);
806768
return -1;
807769
}
770+
// Populate any NULL locals that the compiler might have "proven" to exist
771+
// in the new location. Rather than crashing or changing co_code, just bind
772+
// None instead:
773+
int unbound = 0;
774+
for (int i = 0; i < f->f_frame->f_code->co_nlocalsplus; i++) {
775+
// Counting every unbound local is overly-cautious, but a full flow
776+
// analysis (like we do in the compiler) is probably too expensive:
777+
unbound += f->f_frame->localsplus[i] == NULL;
778+
}
779+
if (unbound) {
780+
const char *e = "assigning None to %d unbound local%s";
781+
const char *s = (unbound == 1) ? "" : "s";
782+
if (PyErr_WarnFormat(PyExc_RuntimeWarning, 0, e, unbound, s)) {
783+
return -1;
784+
}
785+
// Do this in a second pass to avoid writing a bunch of Nones when
786+
// warnings are being treated as errors and the previous bit raises:
787+
for (int i = 0; i < f->f_frame->f_code->co_nlocalsplus; i++) {
788+
if (f->f_frame->localsplus[i] == NULL) {
789+
f->f_frame->localsplus[i] = Py_NewRef(Py_None);
790+
unbound--;
791+
}
792+
}
793+
assert(unbound == 0);
794+
}
808795
if (state == FRAME_SUSPENDED) {
809796
/* Account for value popped by yield */
810797
start_stack = pop_value(start_stack);
@@ -1269,7 +1256,6 @@ _PyFrame_LocalsToFast(_PyInterpreterFrame *frame, int clear)
12691256
}
12701257
fast = _PyFrame_GetLocalsArray(frame);
12711258
co = frame->f_code;
1272-
bool added_null_checks = false;
12731259

12741260
PyErr_Fetch(&error_type, &error_value, &error_traceback);
12751261
for (int i = 0; i < co->co_nlocalsplus; i++) {
@@ -1289,10 +1275,6 @@ _PyFrame_LocalsToFast(_PyInterpreterFrame *frame, int clear)
12891275
}
12901276
}
12911277
PyObject *oldvalue = fast[i];
1292-
if (!added_null_checks && oldvalue != NULL && value == NULL) {
1293-
add_load_fast_null_checks(co);
1294-
added_null_checks = true;
1295-
}
12961278
PyObject *cell = NULL;
12971279
if (kind == CO_FAST_FREE) {
12981280
// The cell was set when the frame was created from
@@ -1319,7 +1301,17 @@ _PyFrame_LocalsToFast(_PyInterpreterFrame *frame, int clear)
13191301
}
13201302
}
13211303
else if (value != oldvalue) {
1322-
Py_XINCREF(value);
1304+
if (value == NULL) {
1305+
// Probably can't delete this, since the compiler's flow
1306+
// analysis may have already "proven" that it exists here:
1307+
const char *e = "assigning None to unbound local %R";
1308+
if (PyErr_WarnFormat(PyExc_RuntimeWarning, 0, e, name)) {
1309+
// It's okay if frame_obj is NULL, just try anyways:
1310+
PyErr_WriteUnraisable((PyObject *)frame->frame_obj);
1311+
}
1312+
value = Py_NewRef(Py_None);
1313+
}
1314+
Py_INCREF(value);
13231315
Py_XSETREF(fast[i], value);
13241316
}
13251317
Py_XDECREF(value);

0 commit comments

Comments
 (0)