diff --git a/python-multiplatform-ksp/src/main/python/typedpython/verify.py b/python-multiplatform-ksp/src/main/python/typedpython/verify.py index 350314f9..4905ee17 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/verify.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/verify.py @@ -1009,9 +1009,13 @@ def e_Call(self, e: Call, state: _State) -> ir.Expr: else: redo = True if not redo: - for k, (a, p) in enumerate(zip(args, callee.params)): - if type(p) is Param and p.cls is not None and self.exact_of(a) != p.cls: - self.can_deopt = True # the callee's entry guard `type(x) is C` may fail + for p in callee.params: + # The callee's entry guard `type(x) is C` may fail, even for an argument that is a + # just-allocated `New(C)` local: on free-threaded CPython another thread can find + # the object through gc.get_objects() and assign `__class__` (SPEC N-12). So + # exact_of must not exempt the guard; cgen's `_can_deopt` is equally conservative. + if type(p) is Param and p.cls is not None: + self.can_deopt = True if callee.may_deopt is True and not redo: self.can_deopt = True if not redo and not self.covers(callee): diff --git a/python-multiplatform-ksp/src/test/python/test_pipeline.py b/python-multiplatform-ksp/src/test/python/test_pipeline.py index 2e43816e..6efbeb14 100644 --- a/python-multiplatform-ksp/src/test/python/test_pipeline.py +++ b/python-multiplatform-ksp/src/test/python/test_pipeline.py @@ -135,3 +135,47 @@ def cb2(): ys[1] = 9.0 mod.fill_then(ys, cb2) assert ys == [1.0, 9.0, 3.0] + + +GC_REACHABLE = """\ +# typedpython: compiled + + +class Cell: + a: object + b: object + __slots__ = ("a", "b") + + def __init__(self, a: object, b: object) -> None: + self.a = a + self.b = b + + +def seta(c: Cell, v: object) -> None: + c.a = v + + +def fresh(a: object, n: int) -> object: + c = Cell(None, None) + seta(c, a) + s: object = None + for i in range(n): + s = c.a + return s +""" + + +def test_a_fresh_local_does_not_exempt_the_callee_class_guard(tmp_path): + # N-12: on free-threaded CPython another thread can reach even a just-allocated object + # through gc.get_objects() and assign __class__, so the callee's `type(x) is C` entry guard + # can fail. The impure caller must be skipped (verify/deopt-impure), never a CGenError. + path = tmp_path / "gcreach.py" + path.write_text(GC_REACHABLE) + result = pipeline.compile_module(path, tmp_path / "out", cost_rule=False) + assert "fresh" in result.skipped, result.skipped + assert "deopt" in result.skipped["fresh"], result.skipped["fresh"] + assert "seta" in [f.name for f in result.module.functions] + mod = pipeline.load(result) + c = mod.Cell(None, None) + mod.seta(c, 7) + assert c.a == 7