https://github.com/python/cpython/commit/9a235416c19edb3ab6fd2808a8917f5831e5a5ba
commit: 9a235416c19edb3ab6fd2808a8917f5831e5a5ba
branch: main
author: Gregory P. Smith <[email protected]>
committer: gpshead <[email protected]>
date: 2026-10-24T23:59:27-07:00
summary:
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.
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-10-11-06-36-25.gh-issue-159188.Fq7wLm.rst
M Lib/test/test_compile.py
M Python/bltinmodule.c
diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py
index ff791d1b1d68bc..df766387dd4c72 100644
--- a/Lib/test/test_compile.py
+++ b/Lib/test/test_compile.py
@@ -2104,13 +2104,16 @@ def describe(obj):
def compile_and_describe(source, use_ast):
"""Compile source; return the code object and what depends on identity.
- If use_ast is true, compile an AST in which the set displays are
- frozenset constants.
+ If use_ast is true, compile an AST in which the set displays of
+ constants are frozenset constants.
"""
if use_ast:
class Transformer(ast.NodeTransformer):
def visit_Set(self, node):
- value = frozenset(ast.literal_eval(node))
+ try:
+ value = frozenset(ast.literal_eval(node))
+ except ValueError:
+ return node
return ast.copy_location(ast.Constant(value), node)
source = Transformer().visit(ast.parse(source))
# The marshal format also depends on reference counts (gh-156504),
@@ -2177,10 +2180,6 @@ class Case:
# and the number of distinct objects among them.
frozensets_and_slices: int = 0
distinct: int = 0
- # Compile an AST with frozenset constants. In the free-threaded
- # build, the constants of code compiled from an AST are also
- # interned.
- use_ast: bool = False
@dataclasses.dataclass(frozen=True)
class Job:
@@ -2370,30 +2369,24 @@ def f(self):
self.ID_a = 1
""",
strings=['ID_a']),
- 'ast_constants': Case(
- source="""
- def f(x):
- return "f", x in {"ID_a", ("ID_b", "ID_c")}
- def g(x):
- return "g", x in {"ID_a", ("ID_b", "ID_c")}
- """,
- strings=['ID_a', 'ID_b', 'ID_c'],
- frozensets_and_slices=2, distinct=1,
- use_ast=True),
}
@classmethod
def jobs(cls, tag):
- # A Job for each of the cases. The prefix depends on tag: a
- # process compiles a source with given strings only once.
+ # Two Jobs for each of the cases: compiled from the source and
+ # from an AST. The prefix depends on tag and on use_ast: a process
+ # compiles a source with given strings only once.
jobs = []
for name, case in cls.CASES.items():
- prefix = f'gh129724{tag}_{name}'
- jobs.append(cls.Job(
- key=f'{tag}:{name}',
- source=textwrap.dedent(case.source).replace('ID', prefix),
- strings=[s.replace('ID', prefix) for s in case.strings],
- use_ast=case.use_ast))
+ source = textwrap.dedent(case.source)
+ for use_ast in False, True:
+ kind = 'ast' if use_ast else 'str'
+ prefix = f'gh129724{tag}{kind}_{name}'
+ jobs.append(cls.Job(
+ key=f'{tag}:{kind}:{name}',
+ source=source.replace('ID', prefix),
+ strings=[s.replace('ID', prefix) for s in case.strings],
+ use_ast=use_ast))
return jobs
@classmethod
@@ -2458,9 +2451,11 @@ def check_child(self, mode):
def test_fresh(self):
# equal constants are the same object
for name, case in self.CASES.items():
- with self.subTest(name):
- self.assertEqual(self.fresh[f'c:{name}']['shared'],
- [case.frozensets_and_slices, case.distinct])
+ expected = [case.frozensets_and_slices, case.distinct]
+ for kind in 'str', 'ast':
+ with self.subTest(name, kind=kind):
+ self.assertEqual(self.fresh[f'c:{kind}:{name}']['shared'],
+ expected)
def test_interned(self):
self.check_child('interned')
@@ -2497,6 +2492,22 @@ def test_same_process_interned(self):
code, result = compile_and_describe(job.source, job.use_ast)
self.check(job.key, result)
+ @support.cpython_only
+ @unittest.skipUnless(support.Py_GIL_DISABLED,
+ 'requires the free-threaded build')
+ def test_constants_not_immortal(self):
+ # compile() does not replace the constants by the immortal ones
+ # of the code objects which were created before.
+ source = 'x = (129724.5, 129724.25)'
+ exec(source, {})
+ for use_ast in False, True:
+ with self.subTest(use_ast=use_ast):
+ code = compile(ast.parse(source) if use_ast else source,
+ '<test>', 'exec')
+ const, = [const for const in code.co_consts
+ if isinstance(const, tuple)]
+ self.assertFalse(sys._is_immortal(const))
+
def test_names_tuples(self):
# Code objects with equal names share one tuple, and a constant
# equal to the names is that same tuple, whatever was compiled
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-11-06-36-25.gh-issue-159188.Fq7wLm.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-11-06-36-25.gh-issue-159188.Fq7wLm.rst
new file mode 100644
index 00000000000000..2637fb8e5326e3
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-11-06-36-25.gh-issue-159188.Fq7wLm.rst
@@ -0,0 +1,5 @@
+In the :term:`free-threaded build`, :func:`compile` no longer immortalizes
+the constants of the code objects when it is given an AST object, as was
+already the case for a source string. The output of :mod:`marshal` for
+such code no longer depends on the code objects which were created before
+in the process.
diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c
index 10c5428159e66f..40471af7186ae3 100644
--- a/Python/bltinmodule.c
+++ b/Python/bltinmodule.c
@@ -942,8 +942,17 @@ builtin_compile_impl(PyObject *module, PyObject *source,
PyObject *filename,
_PyArena_Free(arena);
goto error;
}
+#ifdef Py_GIL_DISABLED
+ // Disable immortalization of code constants, as for a source
+ // string below.
+ _PyThreadStateImpl *tstate = (_PyThreadStateImpl
*)_PyThreadState_GET();
+ tstate->suppress_co_const_immortalization++;
+#endif
result = (PyObject*)_PyAST_Compile(mod, filename,
&cf, optimize, arena, modname);
+#ifdef Py_GIL_DISABLED
+ tstate->suppress_co_const_immortalization--;
+#endif
}
_PyArena_Free(arena);
goto finally;
@@ -956,7 +965,10 @@ builtin_compile_impl(PyObject *module, PyObject *source,
PyObject *filename,
#ifdef Py_GIL_DISABLED
// Disable immortalization of code constants for explicit
// compile() calls to get consistent frozen outputs between the default
- // and free-threaded builds.
+ // and free-threaded builds. It also keeps the output independent of
+ // what the process compiled before: a constant is otherwise replaced
+ // by an equal one of an earlier code object, and is then no longer the
+ // same object as, for example, the equal co_names of its code object.
_PyThreadStateImpl *tstate = (_PyThreadStateImpl *)_PyThreadState_GET();
tstate->suppress_co_const_immortalization++;
#endif
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]