Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 39 additions & 28 deletions Lib/test/test_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -2085,13 +2085,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),
Expand Down Expand Up @@ -2158,10 +2161,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:
Expand Down Expand Up @@ -2351,30 +2350,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
Expand Down Expand Up @@ -2439,9 +2432,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')
Expand Down Expand Up @@ -2478,6 +2473,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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 13 additions & 1 deletion Python/bltinmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
Loading