Skip to content

Commit 7baca3c

Browse files
GH-95150: Use position and exception tables for code hashing and equality (GH-95509)
(cherry picked from commit c7e5bba) Co-authored-by: Brandt Bucher <brandtbucher@gmail.com>
1 parent 76d83b1 commit 7baca3c

File tree

5 files changed

+68
-5
lines changed

5 files changed

+68
-5
lines changed

Lib/test/test_code.py

+21
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,27 @@ def func():
428428
self.assertIsNone(line)
429429
self.assertEqual(end_line, new_code.co_firstlineno + 1)
430430

431+
def test_code_equality(self):
432+
def f():
433+
try:
434+
a()
435+
except:
436+
b()
437+
else:
438+
c()
439+
finally:
440+
d()
441+
code_a = f.__code__
442+
code_b = code_a.replace(co_linetable=b"")
443+
code_c = code_a.replace(co_exceptiontable=b"")
444+
code_d = code_b.replace(co_exceptiontable=b"")
445+
self.assertNotEqual(code_a, code_b)
446+
self.assertNotEqual(code_a, code_c)
447+
self.assertNotEqual(code_a, code_d)
448+
self.assertNotEqual(code_b, code_c)
449+
self.assertNotEqual(code_b, code_d)
450+
self.assertNotEqual(code_c, code_d)
451+
431452

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

Lib/test/test_compile.py

+24-3
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,7 @@ def check_same_constant(const):
613613
exec(code, ns)
614614
f1 = ns['f1']
615615
f2 = ns['f2']
616-
self.assertIs(f1.__code__, f2.__code__)
616+
self.assertIs(f1.__code__.co_consts, f2.__code__.co_consts)
617617
self.check_constant(f1, const)
618618
self.assertEqual(repr(f1()), repr(const))
619619

@@ -626,7 +626,7 @@ def check_same_constant(const):
626626
# Note: "lambda: ..." emits "LOAD_CONST Ellipsis",
627627
# whereas "lambda: Ellipsis" emits "LOAD_GLOBAL Ellipsis"
628628
f1, f2 = lambda: ..., lambda: ...
629-
self.assertIs(f1.__code__, f2.__code__)
629+
self.assertIs(f1.__code__.co_consts, f2.__code__.co_consts)
630630
self.check_constant(f1, Ellipsis)
631631
self.assertEqual(repr(f1()), repr(Ellipsis))
632632

@@ -641,7 +641,7 @@ def check_same_constant(const):
641641
# {0} is converted to a constant frozenset({0}) by the peephole
642642
# optimizer
643643
f1, f2 = lambda x: x in {0}, lambda x: x in {0}
644-
self.assertIs(f1.__code__, f2.__code__)
644+
self.assertIs(f1.__code__.co_consts, f2.__code__.co_consts)
645645
self.check_constant(f1, frozenset({0}))
646646
self.assertTrue(f1(0))
647647

@@ -1264,6 +1264,27 @@ def f():
12641264
self.assertIsNotNone(end_column)
12651265
self.assertLessEqual((line, column), (end_line, end_column))
12661266

1267+
@support.cpython_only
1268+
def test_column_offset_deduplication(self):
1269+
# GH-95150: Code with different column offsets shouldn't be merged!
1270+
for source in [
1271+
"lambda: a",
1272+
"(a for b in c)",
1273+
"[a for b in c]",
1274+
"{a for b in c}",
1275+
"{a: b for c in d}",
1276+
]:
1277+
with self.subTest(source):
1278+
code = compile(f"{source}, {source}", "<test>", "eval")
1279+
self.assertEqual(len(code.co_consts), 2)
1280+
self.assertIsInstance(code.co_consts[0], types.CodeType)
1281+
self.assertIsInstance(code.co_consts[1], types.CodeType)
1282+
self.assertNotEqual(code.co_consts[0], code.co_consts[1])
1283+
self.assertNotEqual(
1284+
list(code.co_consts[0].co_positions()),
1285+
list(code.co_consts[1].co_positions()),
1286+
)
1287+
12671288

12681289
class TestExpressionStackSize(unittest.TestCase):
12691290
# These tests check that the computed stack size for a code object

Lib/test/test_syntax.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -2012,7 +2012,8 @@ def fib(n):
20122012
a, b = 0, 1
20132013
"""
20142014
try:
2015-
self.assertEqual(compile(s1, '<string>', 'exec'), compile(s2, '<string>', 'exec'))
2015+
compile(s1, '<string>', 'exec')
2016+
compile(s2, '<string>', 'exec')
20162017
except SyntaxError:
20172018
self.fail("Indented statement over multiple lines is valid")
20182019

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Update code object hashing and equality to consider all debugging and
2+
exception handling tables. This fixes an issue where certain non-identical
3+
code objects could be "deduplicated" during compilation.

Objects/codeobject.c

+18-1
Original file line numberDiff line numberDiff line change
@@ -1693,6 +1693,15 @@ code_richcompare(PyObject *self, PyObject *other, int op)
16931693
eq = PyObject_RichCompareBool(co->co_localsplusnames,
16941694
cp->co_localsplusnames, Py_EQ);
16951695
if (eq <= 0) goto unequal;
1696+
eq = PyObject_RichCompareBool(co->co_linetable, cp->co_linetable, Py_EQ);
1697+
if (eq <= 0) {
1698+
goto unequal;
1699+
}
1700+
eq = PyObject_RichCompareBool(co->co_exceptiontable,
1701+
cp->co_exceptiontable, Py_EQ);
1702+
if (eq <= 0) {
1703+
goto unequal;
1704+
}
16961705

16971706
if (op == Py_EQ)
16981707
res = Py_True;
@@ -1725,7 +1734,15 @@ code_hash(PyCodeObject *co)
17251734
if (h2 == -1) return -1;
17261735
h3 = PyObject_Hash(co->co_localsplusnames);
17271736
if (h3 == -1) return -1;
1728-
h = h0 ^ h1 ^ h2 ^ h3 ^
1737+
Py_hash_t h4 = PyObject_Hash(co->co_linetable);
1738+
if (h4 == -1) {
1739+
return -1;
1740+
}
1741+
Py_hash_t h5 = PyObject_Hash(co->co_exceptiontable);
1742+
if (h5 == -1) {
1743+
return -1;
1744+
}
1745+
h = h0 ^ h1 ^ h2 ^ h3 ^ h4 ^ h5 ^
17291746
co->co_argcount ^ co->co_posonlyargcount ^ co->co_kwonlyargcount ^
17301747
co->co_flags;
17311748
if (h == -1) h = -2;

0 commit comments

Comments
 (0)