From 32acb54b6bfb9336252099a3b3cd55ce525b3aec Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 10 Jul 2026 13:04:36 +0100 Subject: [PATCH] [mypyc] Use safe borrowing for augmented assignment under free-threading Work on mypyc/mypyc#1203. --- mypyc/irbuild/ast_helpers.py | 14 +++++++++++-- mypyc/test-data/irbuild-classes.test | 30 +++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/mypyc/irbuild/ast_helpers.py b/mypyc/irbuild/ast_helpers.py index 8fff4b19b33d1..57b5e153c7701 100644 --- a/mypyc/irbuild/ast_helpers.py +++ b/mypyc/irbuild/ast_helpers.py @@ -20,8 +20,9 @@ UnaryExpr, Var, ) +from mypyc.common import IS_FREE_THREADED from mypyc.ir.ops import BasicBlock -from mypyc.ir.rtypes import is_fixed_width_rtype, is_tagged +from mypyc.ir.rtypes import RVec, is_fixed_width_rtype, is_tagged from mypyc.irbuild.builder import IRBuilder from mypyc.irbuild.constant_fold import constant_fold_expr @@ -119,5 +120,14 @@ def is_borrow_friendly_expr(self: IRBuilder, expr: Expression) -> bool: # Local variable reference can be borrowed return True if isinstance(expr, MemberExpr) and self.is_native_attr_ref(expr): - return True + # Borrowing a native attribute read is unsafe on free-threaded builds, since another + # thread could concurrently reassign the attribute and free the old value (this is the + # same rule transform_member_expr applies). We still borrow in two cases: + # - Native Final attributes are read-only at runtime, so they can never be reassigned. + # - Vec-typed attributes require manual synchronization, so we borrow them liberally. + return ( + not IS_FREE_THREADED + or isinstance(self.node_type(expr), RVec) + or self.is_final_native_attr_ref(expr) + ) return False diff --git a/mypyc/test-data/irbuild-classes.test b/mypyc/test-data/irbuild-classes.test index 8eaa63e4583b5..e9c2d92431722 100644 --- a/mypyc/test-data/irbuild-classes.test +++ b/mypyc/test-data/irbuild-classes.test @@ -156,7 +156,7 @@ L0: self.y = 60 return 1 -[case testAttrLvalue] +[case testAttrLvalue_withgil] class O(object): def __init__(self) -> None: self.x = 1 @@ -180,6 +180,34 @@ L0: o.x = r1; r2 = is_error return o +[case testAttrLvalue_nogil] +# On free-threaded builds an operator assignment such as 'o.x += 1' must not borrow +# the native attribute read: a concurrent store could free the old value between the +# borrowed load and its use. Contrast testAttrLvalue_withgil, where the read is +# borrowed. is_borrow_friendly_expr applies the same rule as transform_member_expr. +class O(object): + def __init__(self) -> None: + self.x = 1 + +def increment(o: O) -> O: + o.x += 1 + return o +[out] +def O.__init__(self): + self :: __main__.O +L0: + self.x = 2 + return 1 +def increment(o): + o :: __main__.O + r0, r1 :: int + r2 :: bool +L0: + r0 = o.x + r1 = CPyTagged_Add(r0, 2) + o.x = r1; r2 = is_error + return o + [case testSubclass_withgil_toplevel] from typing import TypeVar, Generic from mypy_extensions import trait