Skip to content

Commit 120941d

Browse files
committed
gh-159188: Do not immortalize constants in compile() of an AST
In the free-threaded build, compile() of a source string sets suppress_co_const_immortalization so that its output is the same as in the default build. The branch for AST objects returned before that, so constants were replaced by the immortal ones of the interpreter's table while a tuple of names (such as co_names) which was the same object was not, and the marshal output depended on the code objects created before in the process.
1 parent 627eabf commit 120941d

3 files changed

Lines changed: 57 additions & 29 deletions

File tree

‎Lib/test/test_compile.py‎

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2085,13 +2085,16 @@ def describe(obj):
20852085
def compile_and_describe(source, use_ast):
20862086
"""Compile source; return the code object and what depends on identity.
20872087
2088-
If use_ast is true, compile an AST in which the set displays are
2089-
frozenset constants.
2088+
If use_ast is true, compile an AST in which the set displays of
2089+
constants are frozenset constants.
20902090
"""
20912091
if use_ast:
20922092
class Transformer(ast.NodeTransformer):
20932093
def visit_Set(self, node):
2094-
value = frozenset(ast.literal_eval(node))
2094+
try:
2095+
value = frozenset(ast.literal_eval(node))
2096+
except ValueError:
2097+
return node
20952098
return ast.copy_location(ast.Constant(value), node)
20962099
source = Transformer().visit(ast.parse(source))
20972100
# The marshal format also depends on reference counts (gh-156504),
@@ -2158,10 +2161,6 @@ class Case:
21582161
# and the number of distinct objects among them.
21592162
frozensets_and_slices: int = 0
21602163
distinct: int = 0
2161-
# Compile an AST with frozenset constants. In the free-threaded
2162-
# build, the constants of code compiled from an AST are also
2163-
# interned.
2164-
use_ast: bool = False
21652164

21662165
@dataclasses.dataclass(frozen=True)
21672166
class Job:
@@ -2351,30 +2350,24 @@ def f(self):
23512350
self.ID_a = 1
23522351
""",
23532352
strings=['ID_a']),
2354-
'ast_constants': Case(
2355-
source="""
2356-
def f(x):
2357-
return "f", x in {"ID_a", ("ID_b", "ID_c")}
2358-
def g(x):
2359-
return "g", x in {"ID_a", ("ID_b", "ID_c")}
2360-
""",
2361-
strings=['ID_a', 'ID_b', 'ID_c'],
2362-
frozensets_and_slices=2, distinct=1,
2363-
use_ast=True),
23642353
}
23652354

23662355
@classmethod
23672356
def jobs(cls, tag):
2368-
# A Job for each of the cases. The prefix depends on tag: a
2369-
# process compiles a source with given strings only once.
2357+
# Two Jobs for each of the cases: compiled from the source and
2358+
# from an AST. The prefix depends on tag and on use_ast: a process
2359+
# compiles a source with given strings only once.
23702360
jobs = []
23712361
for name, case in cls.CASES.items():
2372-
prefix = f'gh129724{tag}_{name}'
2373-
jobs.append(cls.Job(
2374-
key=f'{tag}:{name}',
2375-
source=textwrap.dedent(case.source).replace('ID', prefix),
2376-
strings=[s.replace('ID', prefix) for s in case.strings],
2377-
use_ast=case.use_ast))
2362+
source = textwrap.dedent(case.source)
2363+
for use_ast in False, True:
2364+
kind = 'ast' if use_ast else 'str'
2365+
prefix = f'gh129724{tag}{kind}_{name}'
2366+
jobs.append(cls.Job(
2367+
key=f'{tag}:{kind}:{name}',
2368+
source=source.replace('ID', prefix),
2369+
strings=[s.replace('ID', prefix) for s in case.strings],
2370+
use_ast=use_ast))
23782371
return jobs
23792372

23802373
@classmethod
@@ -2439,9 +2432,11 @@ def check_child(self, mode):
24392432
def test_fresh(self):
24402433
# equal constants are the same object
24412434
for name, case in self.CASES.items():
2442-
with self.subTest(name):
2443-
self.assertEqual(self.fresh[f'c:{name}']['shared'],
2444-
[case.frozensets_and_slices, case.distinct])
2435+
expected = [case.frozensets_and_slices, case.distinct]
2436+
for kind in 'str', 'ast':
2437+
with self.subTest(name, kind=kind):
2438+
self.assertEqual(self.fresh[f'c:{kind}:{name}']['shared'],
2439+
expected)
24452440

24462441
def test_interned(self):
24472442
self.check_child('interned')
@@ -2478,6 +2473,22 @@ def test_same_process_interned(self):
24782473
code, result = compile_and_describe(job.source, job.use_ast)
24792474
self.check(job.key, result)
24802475

2476+
@support.cpython_only
2477+
@unittest.skipUnless(support.Py_GIL_DISABLED,
2478+
'requires the free-threaded build')
2479+
def test_constants_not_immortal(self):
2480+
# compile() does not replace the constants by the immortal ones
2481+
# of the code objects which were created before.
2482+
source = 'x = (129724.5, 129724.25)'
2483+
exec(source, {})
2484+
for use_ast in False, True:
2485+
with self.subTest(use_ast=use_ast):
2486+
code = compile(ast.parse(source) if use_ast else source,
2487+
'<test>', 'exec')
2488+
const, = [const for const in code.co_consts
2489+
if isinstance(const, tuple)]
2490+
self.assertFalse(sys._is_immortal(const))
2491+
24812492
def test_names_tuples(self):
24822493
# Code objects with equal names share one tuple, and a constant
24832494
# equal to the names is that same tuple, whatever was compiled
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
In the :term:`free-threaded build`, :func:`compile` no longer immortalizes
2+
the constants of the code objects when it is given an AST object, as was
3+
already the case for a source string. The output of :mod:`marshal` for
4+
such code no longer depends on the code objects which were created before
5+
in the process.

‎Python/bltinmodule.c‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -942,8 +942,17 @@ builtin_compile_impl(PyObject *module, PyObject *source, PyObject *filename,
942942
_PyArena_Free(arena);
943943
goto error;
944944
}
945+
#ifdef Py_GIL_DISABLED
946+
// Disable immortalization of code constants, as for a source
947+
// string below.
948+
_PyThreadStateImpl *tstate = (_PyThreadStateImpl *)_PyThreadState_GET();
949+
tstate->suppress_co_const_immortalization++;
950+
#endif
945951
result = (PyObject*)_PyAST_Compile(mod, filename,
946952
&cf, optimize, arena, modname);
953+
#ifdef Py_GIL_DISABLED
954+
tstate->suppress_co_const_immortalization--;
955+
#endif
947956
}
948957
_PyArena_Free(arena);
949958
goto finally;
@@ -956,7 +965,10 @@ builtin_compile_impl(PyObject *module, PyObject *source, PyObject *filename,
956965
#ifdef Py_GIL_DISABLED
957966
// Disable immortalization of code constants for explicit
958967
// compile() calls to get consistent frozen outputs between the default
959-
// and free-threaded builds.
968+
// and free-threaded builds. It also keeps the output independent of
969+
// what the process compiled before: a constant is otherwise replaced
970+
// by an equal one of an earlier code object, and is then no longer the
971+
// same object as, for example, the equal co_names of its code object.
960972
_PyThreadStateImpl *tstate = (_PyThreadStateImpl *)_PyThreadState_GET();
961973
tstate->suppress_co_const_immortalization++;
962974
#endif

0 commit comments

Comments
 (0)