Skip to content

Commit 9a23541

Browse files
authored
gh-159188: Do not immortalize constants in compile() of an AST (GH-159198)
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 ffe1315 commit 9a23541

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
@@ -2104,13 +2104,16 @@ def describe(obj):
21042104
def compile_and_describe(source, use_ast):
21052105
"""Compile source; return the code object and what depends on identity.
21062106
2107-
If use_ast is true, compile an AST in which the set displays are
2108-
frozenset constants.
2107+
If use_ast is true, compile an AST in which the set displays of
2108+
constants are frozenset constants.
21092109
"""
21102110
if use_ast:
21112111
class Transformer(ast.NodeTransformer):
21122112
def visit_Set(self, node):
2113-
value = frozenset(ast.literal_eval(node))
2113+
try:
2114+
value = frozenset(ast.literal_eval(node))
2115+
except ValueError:
2116+
return node
21142117
return ast.copy_location(ast.Constant(value), node)
21152118
source = Transformer().visit(ast.parse(source))
21162119
# The marshal format also depends on reference counts (gh-156504),
@@ -2177,10 +2180,6 @@ class Case:
21772180
# and the number of distinct objects among them.
21782181
frozensets_and_slices: int = 0
21792182
distinct: int = 0
2180-
# Compile an AST with frozenset constants. In the free-threaded
2181-
# build, the constants of code compiled from an AST are also
2182-
# interned.
2183-
use_ast: bool = False
21842183

21852184
@dataclasses.dataclass(frozen=True)
21862185
class Job:
@@ -2370,30 +2369,24 @@ def f(self):
23702369
self.ID_a = 1
23712370
""",
23722371
strings=['ID_a']),
2373-
'ast_constants': Case(
2374-
source="""
2375-
def f(x):
2376-
return "f", x in {"ID_a", ("ID_b", "ID_c")}
2377-
def g(x):
2378-
return "g", x in {"ID_a", ("ID_b", "ID_c")}
2379-
""",
2380-
strings=['ID_a', 'ID_b', 'ID_c'],
2381-
frozensets_and_slices=2, distinct=1,
2382-
use_ast=True),
23832372
}
23842373

23852374
@classmethod
23862375
def jobs(cls, tag):
2387-
# A Job for each of the cases. The prefix depends on tag: a
2388-
# process compiles a source with given strings only once.
2376+
# Two Jobs for each of the cases: compiled from the source and
2377+
# from an AST. The prefix depends on tag and on use_ast: a process
2378+
# compiles a source with given strings only once.
23892379
jobs = []
23902380
for name, case in cls.CASES.items():
2391-
prefix = f'gh129724{tag}_{name}'
2392-
jobs.append(cls.Job(
2393-
key=f'{tag}:{name}',
2394-
source=textwrap.dedent(case.source).replace('ID', prefix),
2395-
strings=[s.replace('ID', prefix) for s in case.strings],
2396-
use_ast=case.use_ast))
2381+
source = textwrap.dedent(case.source)
2382+
for use_ast in False, True:
2383+
kind = 'ast' if use_ast else 'str'
2384+
prefix = f'gh129724{tag}{kind}_{name}'
2385+
jobs.append(cls.Job(
2386+
key=f'{tag}:{kind}:{name}',
2387+
source=source.replace('ID', prefix),
2388+
strings=[s.replace('ID', prefix) for s in case.strings],
2389+
use_ast=use_ast))
23972390
return jobs
23982391

23992392
@classmethod
@@ -2458,9 +2451,11 @@ def check_child(self, mode):
24582451
def test_fresh(self):
24592452
# equal constants are the same object
24602453
for name, case in self.CASES.items():
2461-
with self.subTest(name):
2462-
self.assertEqual(self.fresh[f'c:{name}']['shared'],
2463-
[case.frozensets_and_slices, case.distinct])
2454+
expected = [case.frozensets_and_slices, case.distinct]
2455+
for kind in 'str', 'ast':
2456+
with self.subTest(name, kind=kind):
2457+
self.assertEqual(self.fresh[f'c:{kind}:{name}']['shared'],
2458+
expected)
24642459

24652460
def test_interned(self):
24662461
self.check_child('interned')
@@ -2497,6 +2492,22 @@ def test_same_process_interned(self):
24972492
code, result = compile_and_describe(job.source, job.use_ast)
24982493
self.check(job.key, result)
24992494

2495+
@support.cpython_only
2496+
@unittest.skipUnless(support.Py_GIL_DISABLED,
2497+
'requires the free-threaded build')
2498+
def test_constants_not_immortal(self):
2499+
# compile() does not replace the constants by the immortal ones
2500+
# of the code objects which were created before.
2501+
source = 'x = (129724.5, 129724.25)'
2502+
exec(source, {})
2503+
for use_ast in False, True:
2504+
with self.subTest(use_ast=use_ast):
2505+
code = compile(ast.parse(source) if use_ast else source,
2506+
'<test>', 'exec')
2507+
const, = [const for const in code.co_consts
2508+
if isinstance(const, tuple)]
2509+
self.assertFalse(sys._is_immortal(const))
2510+
25002511
def test_names_tuples(self):
25012512
# Code objects with equal names share one tuple, and a constant
25022513
# 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)